mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2024-12-11 23:58:55 +00:00
Merge branch 'yt-dlp:master' into pr/2475
This commit is contained in:
commit
cb02b582a8
|
@ -650,3 +650,6 @@ DinhHuy2010
|
||||||
exterrestris
|
exterrestris
|
||||||
harbhim
|
harbhim
|
||||||
LeSuisse
|
LeSuisse
|
||||||
|
DunnesH
|
||||||
|
iancmy
|
||||||
|
mokrueger
|
||||||
|
|
48
Changelog.md
48
Changelog.md
|
@ -4,6 +4,54 @@ # Changelog
|
||||||
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
### 2024.07.25
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **abematv**: [Adapt key retrieval to request handler framework](https://github.com/yt-dlp/yt-dlp/commit/a3bab4752a2b3d56e5a59b4e0411bb8f695c010b) ([#10491](https://github.com/yt-dlp/yt-dlp/issues/10491)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **facebook**: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/1a34a802f44a1dab8f642c79c3cc810e21541d3b) ([#10531](https://github.com/yt-dlp/yt-dlp/issues/10531)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **mlbtv**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/f0993391e6052ec8f7aacc286609564f226943b9) ([#10515](https://github.com/yt-dlp/yt-dlp/issues/10515)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **tiktok**: [Fix and deprioritize JSON subtitles](https://github.com/yt-dlp/yt-dlp/commit/2f97779f335ac069ecccd9c7bf81abf4a83cfe7a) ([#10516](https://github.com/yt-dlp/yt-dlp/issues/10516)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **vimeo**: [Fix chapters extraction](https://github.com/yt-dlp/yt-dlp/commit/a0a1bc3d8d8e3bb9a48a06e835815a0460e90e77) ([#10544](https://github.com/yt-dlp/yt-dlp/issues/10544)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **youtube**: [Fix `n` function name extraction for player `3400486c`](https://github.com/yt-dlp/yt-dlp/commit/713b4cd18f00556771af8cfdd9cea6cc1a09e948) ([#10542](https://github.com/yt-dlp/yt-dlp/issues/10542)) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **build**: [Pin `setuptools` version](https://github.com/yt-dlp/yt-dlp/commit/e046db8a116b1c320d4785daadd48ea0b22a3987) ([#10493](https://github.com/yt-dlp/yt-dlp/issues/10493)) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
### 2024.07.16
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- [Fix `noprogress` if `test=True` with `--quiet` and `--verbose`](https://github.com/yt-dlp/yt-dlp/commit/66ce3d76d87af3f81cc9dfec4be4704016cb1cdb) ([#10454](https://github.com/yt-dlp/yt-dlp/issues/10454)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
- [Support `auto-tty` and `no_color-tty` for `--color`](https://github.com/yt-dlp/yt-dlp/commit/d9cbced493cae2008508d94a2db5dd98be7c01fc) ([#10453](https://github.com/yt-dlp/yt-dlp/issues/10453)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
- **update**: [Fix network error handling](https://github.com/yt-dlp/yt-dlp/commit/ed1b9ed93dd90d2cc960c0d8eaa9d919db224203) ([#10486](https://github.com/yt-dlp/yt-dlp/issues/10486)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **utils**: `parse_codecs`: [Fix parsing of mixed case codec strings](https://github.com/yt-dlp/yt-dlp/commit/cc0070f6496e501d77352bad475fb02d6a86846a) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **adn**: [Adjust for .com domain change](https://github.com/yt-dlp/yt-dlp/commit/959b7a379b8e5da059d110a63339c964b6265736) ([#10399](https://github.com/yt-dlp/yt-dlp/issues/10399)) by [infanf](https://github.com/infanf)
|
||||||
|
- **afreecatv**: [Fix login and use `legacy_ssl`](https://github.com/yt-dlp/yt-dlp/commit/4cd41469243624d90b7a2009b95cbe0609343efe) ([#10440](https://github.com/yt-dlp/yt-dlp/issues/10440)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **box**: [Support enterprise URLs](https://github.com/yt-dlp/yt-dlp/commit/705f5b84dec75cc7af97f42fd1530e8062735970) ([#10419](https://github.com/yt-dlp/yt-dlp/issues/10419)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **digitalconcerthall**: [Extract HEVC and FLAC formats](https://github.com/yt-dlp/yt-dlp/commit/e62fa6b0e0186f8c5666c2c5ab64cf191abdafc1) ([#10470](https://github.com/yt-dlp/yt-dlp/issues/10470)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **dplay**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/39e6c4cb44b9292e89ac0afec3cd0afc2ae8775f) ([#10471](https://github.com/yt-dlp/yt-dlp/issues/10471)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **epidemicsound**: [Support sound effects URLs](https://github.com/yt-dlp/yt-dlp/commit/8531d2b03bac9cc746f2ee8098aaf8f115505f5b) ([#10436](https://github.com/yt-dlp/yt-dlp/issues/10436)) by [iancmy](https://github.com/iancmy)
|
||||||
|
- **generic**: [Fix direct video link extensions](https://github.com/yt-dlp/yt-dlp/commit/b9afb99e7c34d0eb15ddc6689cd7d20eebfda68e) ([#10468](https://github.com/yt-dlp/yt-dlp/issues/10468)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **picarto**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/bacd18b7df08b4995644fd12cee1f8c8e8636bc7) ([#10414](https://github.com/yt-dlp/yt-dlp/issues/10414)) by [Frankgoji](https://github.com/Frankgoji)
|
||||||
|
- **soundcloud**: permalink, user: [Extract tracks only](https://github.com/yt-dlp/yt-dlp/commit/22870b81bad97dfa6307a7add44753b2dffc76a9) ([#10463](https://github.com/yt-dlp/yt-dlp/issues/10463)) by [DunnesH](https://github.com/DunnesH)
|
||||||
|
- **tiktok**: live: [Fix room ID extraction](https://github.com/yt-dlp/yt-dlp/commit/d2189d3d36987ebeac426fd70a60a5fe86325a2b) ([#10408](https://github.com/yt-dlp/yt-dlp/issues/10408)) by [mokrueger](https://github.com/mokrueger)
|
||||||
|
- **tv5monde**: [Support browser impersonation](https://github.com/yt-dlp/yt-dlp/commit/9b95a6765a5f6325af99c4aca961587f0c426e8c) ([#10417](https://github.com/yt-dlp/yt-dlp/issues/10417)) by [bashonly](https://github.com/bashonly) (With fixes in [cc1a309](https://github.com/yt-dlp/yt-dlp/commit/cc1a3098c00995c6aebc2a16bd1050a66bad64db))
|
||||||
|
- **youtube**
|
||||||
|
- [Avoid poToken experiment player responses](https://github.com/yt-dlp/yt-dlp/commit/8b8b442cb005a8d85315f301615f83fb736b967a) ([#10456](https://github.com/yt-dlp/yt-dlp/issues/10456)) by [seproDev](https://github.com/seproDev) (With fixes in [16da8ef](https://github.com/yt-dlp/yt-dlp/commit/16da8ef9937ff76632dfef02e5062c5ba99c8ea2))
|
||||||
|
- [Invalidate nsig cache from < 2024.07.09](https://github.com/yt-dlp/yt-dlp/commit/04e17ba20a139f1b3e30ec4bafa3fba26888f0b3) ([#10401](https://github.com/yt-dlp/yt-dlp/issues/10401)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Reduce android client priority](https://github.com/yt-dlp/yt-dlp/commit/b85eef0a615a01304f88a3847309c667e09a20df) ([#10467](https://github.com/yt-dlp/yt-dlp/issues/10467)) by [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
#### Networking changes
|
||||||
|
- [Add `legacy_ssl` request extension](https://github.com/yt-dlp/yt-dlp/commit/150ecc45d9cacc919550c13b04fd998ac5103a6b) ([#10448](https://github.com/yt-dlp/yt-dlp/issues/10448)) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
- **Request Handler**: curl_cffi: [Support `curl_cffi` 0.7.X](https://github.com/yt-dlp/yt-dlp/commit/42bfca00a6b460fc053514cdd7ac6f5b5daddf0c) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **build**
|
||||||
|
- [Include `curl_cffi` in `yt-dlp_linux`](https://github.com/yt-dlp/yt-dlp/commit/4521f30d1479315cd5c3bf4abdad19391952df98) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Pin `curl-cffi` to 0.5.10 for Windows](https://github.com/yt-dlp/yt-dlp/commit/ac30941ae682f71eab010877c9a977736a61d3cf) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **cleanup**: Miscellaneous: [89a161e](https://github.com/yt-dlp/yt-dlp/commit/89a161e8c62569a662deda1c948664152efcb6b4) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
### 2024.07.09
|
### 2024.07.09
|
||||||
|
|
||||||
#### Core changes
|
#### Core changes
|
||||||
|
|
|
@ -368,7 +368,9 @@ ## General Options:
|
||||||
stderr) to apply the setting to. Can be one
|
stderr) to apply the setting to. Can be one
|
||||||
of "always", "auto" (default), "never", or
|
of "always", "auto" (default), "never", or
|
||||||
"no_color" (use non color terminal
|
"no_color" (use non color terminal
|
||||||
sequences). Can be used multiple times
|
sequences). Use "auto-tty" or "no_color-tty"
|
||||||
|
to decide based on terminal support only.
|
||||||
|
Can be used multiple times
|
||||||
--compat-options OPTS Options that can help keep compatibility
|
--compat-options OPTS Options that can help keep compatibility
|
||||||
with youtube-dl or youtube-dlc
|
with youtube-dl or youtube-dlc
|
||||||
configurations by reverting some of the
|
configurations by reverting some of the
|
||||||
|
@ -1859,6 +1861,9 @@ #### orfon (orf:on)
|
||||||
#### bilibili
|
#### bilibili
|
||||||
* `prefer_multi_flv`: Prefer extracting flv formats over mp4 for older videos that still provide legacy formats
|
* `prefer_multi_flv`: Prefer extracting flv formats over mp4 for older videos that still provide legacy formats
|
||||||
|
|
||||||
|
#### digitalconcerthall
|
||||||
|
* `prefer_combined_hls`: Prefer extracting combined/pre-merged video and audio HLS formats. This will exclude 4K/HEVC video and lossless/FLAC audio formats, which are only available as split video/audio HLS formats
|
||||||
|
|
||||||
**Note**: These options may be changed/removed in the future without concern for backward compatibility
|
**Note**: These options may be changed/removed in the future without concern for backward compatibility
|
||||||
|
|
||||||
<!-- MANPAGE: MOVE "INSTALLATION" SECTION HERE -->
|
<!-- MANPAGE: MOVE "INSTALLATION" SECTION HERE -->
|
||||||
|
|
|
@ -9,6 +9,7 @@ maintainers = [
|
||||||
{name = "Grub4K", email = "contact@grub4k.xyz"},
|
{name = "Grub4K", email = "contact@grub4k.xyz"},
|
||||||
{name = "bashonly", email = "bashonly@protonmail.com"},
|
{name = "bashonly", email = "bashonly@protonmail.com"},
|
||||||
{name = "coletdjnz", email = "coletdjnz@protonmail.com"},
|
{name = "coletdjnz", email = "coletdjnz@protonmail.com"},
|
||||||
|
{name = "sepro", email = "sepro@sepr0.com"},
|
||||||
]
|
]
|
||||||
description = "A feature-rich command-line audio/video downloader"
|
description = "A feature-rich command-line audio/video downloader"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -65,7 +66,7 @@ build = [
|
||||||
"build",
|
"build",
|
||||||
"hatchling",
|
"hatchling",
|
||||||
"pip",
|
"pip",
|
||||||
"setuptools",
|
"setuptools>=71.0.2", # 71.0.0 broke pyinstaller
|
||||||
"wheel",
|
"wheel",
|
||||||
]
|
]
|
||||||
dev = [
|
dev = [
|
||||||
|
|
|
@ -354,7 +354,6 @@ # Supported sites
|
||||||
- **DigitallySpeaking**
|
- **DigitallySpeaking**
|
||||||
- **Digiteka**
|
- **Digiteka**
|
||||||
- **DiscogsReleasePlaylist**
|
- **DiscogsReleasePlaylist**
|
||||||
- **Discovery**
|
|
||||||
- **DiscoveryLife**
|
- **DiscoveryLife**
|
||||||
- **DiscoveryNetworksDe**
|
- **DiscoveryNetworksDe**
|
||||||
- **DiscoveryPlus**
|
- **DiscoveryPlus**
|
||||||
|
@ -363,7 +362,6 @@ # Supported sites
|
||||||
- **DiscoveryPlusItaly**
|
- **DiscoveryPlusItaly**
|
||||||
- **DiscoveryPlusItalyShow**
|
- **DiscoveryPlusItalyShow**
|
||||||
- **Disney**
|
- **Disney**
|
||||||
- **DIYNetwork**
|
|
||||||
- **dlf**
|
- **dlf**
|
||||||
- **dlf:corpus**: DLF Multi-feed Archives
|
- **dlf:corpus**: DLF Multi-feed Archives
|
||||||
- **dlive:stream**
|
- **dlive:stream**
|
||||||
|
@ -516,7 +514,6 @@ # Supported sites
|
||||||
- **GlattvisionTVLive**: [*glattvisiontv*](## "netrc machine")
|
- **GlattvisionTVLive**: [*glattvisiontv*](## "netrc machine")
|
||||||
- **GlattvisionTVRecordings**: [*glattvisiontv*](## "netrc machine")
|
- **GlattvisionTVRecordings**: [*glattvisiontv*](## "netrc machine")
|
||||||
- **Glide**: Glide mobile video messages (glide.me)
|
- **Glide**: Glide mobile video messages (glide.me)
|
||||||
- **GlobalCyclingNetworkPlus**
|
|
||||||
- **GlobalPlayerAudio**
|
- **GlobalPlayerAudio**
|
||||||
- **GlobalPlayerAudioEpisode**
|
- **GlobalPlayerAudioEpisode**
|
||||||
- **GlobalPlayerLive**
|
- **GlobalPlayerLive**
|
||||||
|
@ -820,8 +817,6 @@ # Supported sites
|
||||||
- **MotherlessGroup**
|
- **MotherlessGroup**
|
||||||
- **MotherlessUploader**
|
- **MotherlessUploader**
|
||||||
- **Motorsport**: motorsport.com (**Currently broken**)
|
- **Motorsport**: motorsport.com (**Currently broken**)
|
||||||
- **MotorTrend**
|
|
||||||
- **MotorTrendOnDemand**
|
|
||||||
- **MovieFap**
|
- **MovieFap**
|
||||||
- **Moviepilot**
|
- **Moviepilot**
|
||||||
- **MoviewPlay**
|
- **MoviewPlay**
|
||||||
|
|
|
@ -929,6 +929,11 @@ def test_parse_codecs(self):
|
||||||
'acodec': 'none',
|
'acodec': 'none',
|
||||||
'dynamic_range': 'DV',
|
'dynamic_range': 'DV',
|
||||||
})
|
})
|
||||||
|
self.assertEqual(parse_codecs('fLaC'), {
|
||||||
|
'vcodec': 'none',
|
||||||
|
'acodec': 'flac',
|
||||||
|
'dynamic_range': None,
|
||||||
|
})
|
||||||
self.assertEqual(parse_codecs('theora, vorbis'), {
|
self.assertEqual(parse_codecs('theora, vorbis'), {
|
||||||
'vcodec': 'theora',
|
'vcodec': 'theora',
|
||||||
'acodec': 'vorbis',
|
'acodec': 'vorbis',
|
||||||
|
|
|
@ -171,6 +171,10 @@
|
||||||
'https://www.youtube.com/s/player/b22ef6e7/player_ias.vflset/en_US/base.js',
|
'https://www.youtube.com/s/player/b22ef6e7/player_ias.vflset/en_US/base.js',
|
||||||
'b6HcntHGkvBLk_FRf', 'kNPW6A7FyP2l8A',
|
'b6HcntHGkvBLk_FRf', 'kNPW6A7FyP2l8A',
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/3400486c/player_ias.vflset/en_US/base.js',
|
||||||
|
'lL46g3XifCKUZn1Xfw', 'z767lhet6V2Skl',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -452,7 +452,8 @@ class YoutubeDL:
|
||||||
Can also just be a single color policy,
|
Can also just be a single color policy,
|
||||||
in which case it applies to all outputs.
|
in which case it applies to all outputs.
|
||||||
Valid stream names are 'stdout' and 'stderr'.
|
Valid stream names are 'stdout' and 'stderr'.
|
||||||
Valid color policies are one of 'always', 'auto', 'no_color' or 'never'.
|
Valid color policies are one of 'always', 'auto',
|
||||||
|
'no_color', 'never', 'auto-tty' or 'no_color-tty'.
|
||||||
geo_bypass: Bypass geographic restriction via faking X-Forwarded-For
|
geo_bypass: Bypass geographic restriction via faking X-Forwarded-For
|
||||||
HTTP header
|
HTTP header
|
||||||
geo_bypass_country:
|
geo_bypass_country:
|
||||||
|
@ -659,12 +660,15 @@ def __init__(self, params=None, auto_init=True):
|
||||||
self.params['color'] = 'no_color'
|
self.params['color'] = 'no_color'
|
||||||
|
|
||||||
term_allow_color = os.getenv('TERM', '').lower() != 'dumb'
|
term_allow_color = os.getenv('TERM', '').lower() != 'dumb'
|
||||||
no_color = bool(os.getenv('NO_COLOR'))
|
base_no_color = bool(os.getenv('NO_COLOR'))
|
||||||
|
|
||||||
def process_color_policy(stream):
|
def process_color_policy(stream):
|
||||||
stream_name = {sys.stdout: 'stdout', sys.stderr: 'stderr'}[stream]
|
stream_name = {sys.stdout: 'stdout', sys.stderr: 'stderr'}[stream]
|
||||||
policy = traverse_obj(self.params, ('color', (stream_name, None), {str}), get_all=False)
|
policy = traverse_obj(self.params, ('color', (stream_name, None), {str}, any)) or 'auto'
|
||||||
if policy in ('auto', None):
|
if policy in ('auto', 'auto-tty', 'no_color-tty'):
|
||||||
|
no_color = base_no_color
|
||||||
|
if policy.endswith('tty'):
|
||||||
|
no_color = policy.startswith('no_color')
|
||||||
if term_allow_color and supports_terminal_sequences(stream):
|
if term_allow_color and supports_terminal_sequences(stream):
|
||||||
return 'no_color' if no_color else True
|
return 'no_color' if no_color else True
|
||||||
return False
|
return False
|
||||||
|
@ -3168,11 +3172,12 @@ def dl(self, name, info, subtitle=False, test=False):
|
||||||
|
|
||||||
if test:
|
if test:
|
||||||
verbose = self.params.get('verbose')
|
verbose = self.params.get('verbose')
|
||||||
|
quiet = self.params.get('quiet') or not verbose
|
||||||
params = {
|
params = {
|
||||||
'test': True,
|
'test': True,
|
||||||
'quiet': self.params.get('quiet') or not verbose,
|
'quiet': quiet,
|
||||||
'verbose': verbose,
|
'verbose': verbose,
|
||||||
'noprogress': not verbose,
|
'noprogress': quiet,
|
||||||
'nopart': True,
|
'nopart': True,
|
||||||
'skip_unavailable_fragments': False,
|
'skip_unavailable_fragments': False,
|
||||||
'keep_fragments': False,
|
'keep_fragments': False,
|
||||||
|
|
|
@ -468,7 +468,7 @@ def metadataparser_actions(f):
|
||||||
default_downloader = ed.get_basename()
|
default_downloader = ed.get_basename()
|
||||||
|
|
||||||
for policy in opts.color.values():
|
for policy in opts.color.values():
|
||||||
if policy not in ('always', 'auto', 'no_color', 'never'):
|
if policy not in ('always', 'auto', 'auto-tty', 'no_color', 'no_color-tty', 'never'):
|
||||||
raise ValueError(f'"{policy}" is not a valid color policy')
|
raise ValueError(f'"{policy}" is not a valid color policy')
|
||||||
|
|
||||||
warnings, deprecation_warnings = [], []
|
warnings, deprecation_warnings = [], []
|
||||||
|
|
|
@ -504,7 +504,6 @@
|
||||||
from .digitalconcerthall import DigitalConcertHallIE
|
from .digitalconcerthall import DigitalConcertHallIE
|
||||||
from .digiteka import DigitekaIE
|
from .digiteka import DigitekaIE
|
||||||
from .discogs import DiscogsReleasePlaylistIE
|
from .discogs import DiscogsReleasePlaylistIE
|
||||||
from .discovery import DiscoveryIE
|
|
||||||
from .disney import DisneyIE
|
from .disney import DisneyIE
|
||||||
from .dispeak import DigitallySpeakingIE
|
from .dispeak import DigitallySpeakingIE
|
||||||
from .dlf import (
|
from .dlf import (
|
||||||
|
@ -532,16 +531,12 @@
|
||||||
DiscoveryPlusIndiaShowIE,
|
DiscoveryPlusIndiaShowIE,
|
||||||
DiscoveryPlusItalyIE,
|
DiscoveryPlusItalyIE,
|
||||||
DiscoveryPlusItalyShowIE,
|
DiscoveryPlusItalyShowIE,
|
||||||
DIYNetworkIE,
|
|
||||||
DPlayIE,
|
DPlayIE,
|
||||||
FoodNetworkIE,
|
FoodNetworkIE,
|
||||||
GlobalCyclingNetworkPlusIE,
|
|
||||||
GoDiscoveryIE,
|
GoDiscoveryIE,
|
||||||
HGTVDeIE,
|
HGTVDeIE,
|
||||||
HGTVUsaIE,
|
HGTVUsaIE,
|
||||||
InvestigationDiscoveryIE,
|
InvestigationDiscoveryIE,
|
||||||
MotorTrendIE,
|
|
||||||
MotorTrendOnDemandIE,
|
|
||||||
ScienceChannelIE,
|
ScienceChannelIE,
|
||||||
TravelChannelIE,
|
TravelChannelIE,
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,12 +9,12 @@
|
||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
|
||||||
import urllib.response
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..aes import aes_ecb_decrypt
|
from ..aes import aes_ecb_decrypt
|
||||||
|
from ..networking import RequestHandler, Response
|
||||||
|
from ..networking.exceptions import TransportError
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
OnDemandPagedList,
|
OnDemandPagedList,
|
||||||
|
@ -26,37 +26,36 @@
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
)
|
)
|
||||||
from ..utils.networking import clean_proxies
|
|
||||||
|
|
||||||
|
|
||||||
def add_opener(ydl, handler): # FIXME: Create proper API in .networking
|
class AbemaLicenseRH(RequestHandler):
|
||||||
"""Add a handler for opening URLs, like _download_webpage"""
|
_SUPPORTED_URL_SCHEMES = ('abematv-license',)
|
||||||
# https://github.com/python/cpython/blob/main/Lib/urllib/request.py#L426
|
_SUPPORTED_PROXY_SCHEMES = None
|
||||||
# https://github.com/python/cpython/blob/main/Lib/urllib/request.py#L605
|
_SUPPORTED_FEATURES = None
|
||||||
rh = ydl._request_director.handlers['Urllib']
|
RH_NAME = 'abematv_license'
|
||||||
if 'abematv-license' in rh._SUPPORTED_URL_SCHEMES:
|
|
||||||
return
|
|
||||||
headers = ydl.params['http_headers'].copy()
|
|
||||||
proxies = ydl.proxies.copy()
|
|
||||||
clean_proxies(proxies, headers)
|
|
||||||
opener = rh._get_instance(cookiejar=ydl.cookiejar, proxies=proxies)
|
|
||||||
assert isinstance(opener, urllib.request.OpenerDirector)
|
|
||||||
opener.add_handler(handler)
|
|
||||||
rh._SUPPORTED_URL_SCHEMES = (*rh._SUPPORTED_URL_SCHEMES, 'abematv-license')
|
|
||||||
|
|
||||||
|
_STRTABLE = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||||
|
_HKEY = b'3AF0298C219469522A313570E8583005A642E73EDD58E3EA2FB7339D3DF1597E'
|
||||||
|
|
||||||
class AbemaLicenseHandler(urllib.request.BaseHandler):
|
def __init__(self, *, ie: 'AbemaTVIE', **kwargs):
|
||||||
handler_order = 499
|
super().__init__(**kwargs)
|
||||||
STRTABLE = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
||||||
HKEY = b'3AF0298C219469522A313570E8583005A642E73EDD58E3EA2FB7339D3DF1597E'
|
|
||||||
|
|
||||||
def __init__(self, ie: 'AbemaTVIE'):
|
|
||||||
# the protocol that this should really handle is 'abematv-license://'
|
|
||||||
# abematv_license_open is just a placeholder for development purposes
|
|
||||||
# ref. https://github.com/python/cpython/blob/f4c03484da59049eb62a9bf7777b963e2267d187/Lib/urllib/request.py#L510
|
|
||||||
setattr(self, 'abematv-license_open', getattr(self, 'abematv_license_open', None))
|
|
||||||
self.ie = ie
|
self.ie = ie
|
||||||
|
|
||||||
|
def _send(self, request):
|
||||||
|
url = request.url
|
||||||
|
ticket = urllib.parse.urlparse(url).netloc
|
||||||
|
|
||||||
|
try:
|
||||||
|
response_data = self._get_videokey_from_ticket(ticket)
|
||||||
|
except ExtractorError as e:
|
||||||
|
raise TransportError(cause=e.cause) from e
|
||||||
|
except (IndexError, KeyError, TypeError) as e:
|
||||||
|
raise TransportError(cause=repr(e)) from e
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
io.BytesIO(response_data), url,
|
||||||
|
headers={'Content-Length': str(len(response_data))})
|
||||||
|
|
||||||
def _get_videokey_from_ticket(self, ticket):
|
def _get_videokey_from_ticket(self, ticket):
|
||||||
to_show = self.ie.get_param('verbose', False)
|
to_show = self.ie.get_param('verbose', False)
|
||||||
media_token = self.ie._get_media_token(to_show=to_show)
|
media_token = self.ie._get_media_token(to_show=to_show)
|
||||||
|
@ -72,25 +71,17 @@ def _get_videokey_from_ticket(self, ticket):
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
})
|
})
|
||||||
|
|
||||||
res = decode_base_n(license_response['k'], table=self.STRTABLE)
|
res = decode_base_n(license_response['k'], table=self._STRTABLE)
|
||||||
encvideokey = bytes_to_intlist(struct.pack('>QQ', res >> 64, res & 0xffffffffffffffff))
|
encvideokey = bytes_to_intlist(struct.pack('>QQ', res >> 64, res & 0xffffffffffffffff))
|
||||||
|
|
||||||
h = hmac.new(
|
h = hmac.new(
|
||||||
binascii.unhexlify(self.HKEY),
|
binascii.unhexlify(self._HKEY),
|
||||||
(license_response['cid'] + self.ie._DEVICE_ID).encode(),
|
(license_response['cid'] + self.ie._DEVICE_ID).encode(),
|
||||||
digestmod=hashlib.sha256)
|
digestmod=hashlib.sha256)
|
||||||
enckey = bytes_to_intlist(h.digest())
|
enckey = bytes_to_intlist(h.digest())
|
||||||
|
|
||||||
return intlist_to_bytes(aes_ecb_decrypt(encvideokey, enckey))
|
return intlist_to_bytes(aes_ecb_decrypt(encvideokey, enckey))
|
||||||
|
|
||||||
def abematv_license_open(self, url):
|
|
||||||
url = url.get_full_url() if isinstance(url, urllib.request.Request) else url
|
|
||||||
ticket = urllib.parse.urlparse(url).netloc
|
|
||||||
response_data = self._get_videokey_from_ticket(ticket)
|
|
||||||
return urllib.response.addinfourl(io.BytesIO(response_data), headers={
|
|
||||||
'Content-Length': str(len(response_data)),
|
|
||||||
}, url=url, code=200)
|
|
||||||
|
|
||||||
|
|
||||||
class AbemaTVBaseIE(InfoExtractor):
|
class AbemaTVBaseIE(InfoExtractor):
|
||||||
_NETRC_MACHINE = 'abematv'
|
_NETRC_MACHINE = 'abematv'
|
||||||
|
@ -139,7 +130,7 @@ def _get_device_token(self):
|
||||||
if self._USERTOKEN:
|
if self._USERTOKEN:
|
||||||
return self._USERTOKEN
|
return self._USERTOKEN
|
||||||
|
|
||||||
add_opener(self._downloader, AbemaLicenseHandler(self))
|
self._downloader._request_director.add_handler(AbemaLicenseRH(ie=self, logger=None))
|
||||||
|
|
||||||
username, _ = self._get_login_info()
|
username, _ = self._get_login_info()
|
||||||
auth_cache = username and self.cache.load(self._NETRC_MACHINE, username, min_ver='2024.01.19')
|
auth_cache = username and self.cache.load(self._NETRC_MACHINE, username, min_ver='2024.01.19')
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
intlist_to_bytes,
|
intlist_to_bytes,
|
||||||
|
join_nonempty,
|
||||||
long_to_bytes,
|
long_to_bytes,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
pkcs1pad,
|
pkcs1pad,
|
||||||
|
@ -48,9 +49,9 @@ class ADNBaseIE(InfoExtractor):
|
||||||
|
|
||||||
|
|
||||||
class ADNIE(ADNBaseIE):
|
class ADNIE(ADNBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?:animation|anime)digitalnetwork\.(?P<lang>fr|de)/video/[^/?#]+/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?(?:animation|anime)digitalnetwork\.com/(?:(?P<lang>de)/)?video/[^/?#]+/(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://animationdigitalnetwork.fr/video/fruits-basket/9841-episode-1-a-ce-soir',
|
'url': 'https://animationdigitalnetwork.com/video/fruits-basket/9841-episode-1-a-ce-soir',
|
||||||
'md5': '1c9ef066ceb302c86f80c2b371615261',
|
'md5': '1c9ef066ceb302c86f80c2b371615261',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '9841',
|
'id': '9841',
|
||||||
|
@ -70,10 +71,10 @@ class ADNIE(ADNBaseIE):
|
||||||
},
|
},
|
||||||
'skip': 'Only available in French and German speaking Europe',
|
'skip': 'Only available in French and German speaking Europe',
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://animedigitalnetwork.fr/video/blue-exorcist-kyoto-saga/7778-episode-1-debut-des-hostilites',
|
'url': 'http://animedigitalnetwork.com/video/blue-exorcist-kyoto-saga/7778-episode-1-debut-des-hostilites',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://animationdigitalnetwork.de/video/the-eminence-in-shadow/23550-folge-1',
|
'url': 'https://animationdigitalnetwork.com/de/video/the-eminence-in-shadow/23550-folge-1',
|
||||||
'md5': '5c5651bf5791fa6fcd7906012b9d94e8',
|
'md5': '5c5651bf5791fa6fcd7906012b9d94e8',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '23550',
|
'id': '23550',
|
||||||
|
@ -217,7 +218,7 @@ def _real_extract(self, url):
|
||||||
links_data = self._download_json(
|
links_data = self._download_json(
|
||||||
links_url, video_id, 'Downloading links JSON metadata', headers={
|
links_url, video_id, 'Downloading links JSON metadata', headers={
|
||||||
'X-Player-Token': authorization,
|
'X-Player-Token': authorization,
|
||||||
'X-Target-Distribution': lang,
|
'X-Target-Distribution': lang or 'fr',
|
||||||
**self._HEADERS,
|
**self._HEADERS,
|
||||||
}, query={
|
}, query={
|
||||||
'freeWithAds': 'true',
|
'freeWithAds': 'true',
|
||||||
|
@ -298,9 +299,9 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
|
|
||||||
class ADNSeasonIE(ADNBaseIE):
|
class ADNSeasonIE(ADNBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?:animation|anime)digitalnetwork\.(?P<lang>fr|de)/video/(?P<id>[^/?#]+)/?(?:$|[#?])'
|
_VALID_URL = r'https?://(?:www\.)?(?:animation|anime)digitalnetwork\.com/(?:(?P<lang>de)/)?video/(?P<id>[^/?#]+)/?(?:$|[#?])'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://animationdigitalnetwork.fr/video/tokyo-mew-mew-new',
|
'url': 'https://animationdigitalnetwork.com/video/tokyo-mew-mew-new',
|
||||||
'playlist_count': 12,
|
'playlist_count': 12,
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '911',
|
'id': '911',
|
||||||
|
@ -318,7 +319,7 @@ def _real_extract(self, url):
|
||||||
episodes = self._download_json(
|
episodes = self._download_json(
|
||||||
f'{self._API_BASE_URL}video/show/{show_id}', video_show_slug,
|
f'{self._API_BASE_URL}video/show/{show_id}', video_show_slug,
|
||||||
'Downloading episode list', headers={
|
'Downloading episode list', headers={
|
||||||
'X-Target-Distribution': lang,
|
'X-Target-Distribution': lang or 'fr',
|
||||||
**self._HEADERS,
|
**self._HEADERS,
|
||||||
}, query={
|
}, query={
|
||||||
'order': 'asc',
|
'order': 'asc',
|
||||||
|
@ -327,8 +328,8 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
def entries():
|
def entries():
|
||||||
for episode_id in traverse_obj(episodes, ('videos', ..., 'id', {str_or_none})):
|
for episode_id in traverse_obj(episodes, ('videos', ..., 'id', {str_or_none})):
|
||||||
yield self.url_result(
|
yield self.url_result(join_nonempty(
|
||||||
f'https://animationdigitalnetwork.{lang}/video/{video_show_slug}/{episode_id}',
|
'https://animationdigitalnetwork.com', lang, 'video',
|
||||||
ADNIE, episode_id)
|
video_show_slug, episode_id, delim='/'), ADNIE, episode_id)
|
||||||
|
|
||||||
return self.playlist_result(entries(), show_id, show.get('title'))
|
return self.playlist_result(entries(), show_id, show.get('title'))
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..networking.exceptions import HTTPError
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
parse_codecs,
|
||||||
try_get,
|
try_get,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
|
@ -12,6 +14,7 @@ class DigitalConcertHallIE(InfoExtractor):
|
||||||
IE_DESC = 'DigitalConcertHall extractor'
|
IE_DESC = 'DigitalConcertHall extractor'
|
||||||
_VALID_URL = r'https?://(?:www\.)?digitalconcerthall\.com/(?P<language>[a-z]+)/(?P<type>film|concert|work)/(?P<id>[0-9]+)-?(?P<part>[0-9]+)?'
|
_VALID_URL = r'https?://(?:www\.)?digitalconcerthall\.com/(?P<language>[a-z]+)/(?P<type>film|concert|work)/(?P<id>[0-9]+)-?(?P<part>[0-9]+)?'
|
||||||
_OAUTH_URL = 'https://api.digitalconcerthall.com/v2/oauth2/token'
|
_OAUTH_URL = 'https://api.digitalconcerthall.com/v2/oauth2/token'
|
||||||
|
_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15'
|
||||||
_ACCESS_TOKEN = None
|
_ACCESS_TOKEN = None
|
||||||
_NETRC_MACHINE = 'digitalconcerthall'
|
_NETRC_MACHINE = 'digitalconcerthall'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
@ -68,33 +71,42 @@ class DigitalConcertHallIE(InfoExtractor):
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _perform_login(self, username, password):
|
def _perform_login(self, username, password):
|
||||||
token_response = self._download_json(
|
login_token = self._download_json(
|
||||||
self._OAUTH_URL,
|
self._OAUTH_URL,
|
||||||
None, 'Obtaining token', errnote='Unable to obtain token', data=urlencode_postdata({
|
None, 'Obtaining token', errnote='Unable to obtain token', data=urlencode_postdata({
|
||||||
'affiliate': 'none',
|
'affiliate': 'none',
|
||||||
'grant_type': 'device',
|
'grant_type': 'device',
|
||||||
'device_vendor': 'unknown',
|
'device_vendor': 'unknown',
|
||||||
|
# device_model 'Safari' gets split streams of 4K/HEVC video and lossless/FLAC audio
|
||||||
|
'device_model': 'unknown' if self._configuration_arg('prefer_combined_hls') else 'Safari',
|
||||||
'app_id': 'dch.webapp',
|
'app_id': 'dch.webapp',
|
||||||
'app_version': '1.0.0',
|
'app_distributor': 'berlinphil',
|
||||||
|
'app_version': '1.84.0',
|
||||||
'client_secret': '2ySLN+2Fwb',
|
'client_secret': '2ySLN+2Fwb',
|
||||||
}), headers={
|
}), headers={
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Accept': 'application/json',
|
||||||
})
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||||
self._ACCESS_TOKEN = token_response['access_token']
|
'User-Agent': self._USER_AGENT,
|
||||||
|
})['access_token']
|
||||||
try:
|
try:
|
||||||
self._download_json(
|
login_response = self._download_json(
|
||||||
self._OAUTH_URL,
|
self._OAUTH_URL,
|
||||||
None, note='Logging in', errnote='Unable to login', data=urlencode_postdata({
|
None, note='Logging in', errnote='Unable to login', data=urlencode_postdata({
|
||||||
'grant_type': 'password',
|
'grant_type': 'password',
|
||||||
'username': username,
|
'username': username,
|
||||||
'password': password,
|
'password': password,
|
||||||
}), headers={
|
}), headers={
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||||
'Referer': 'https://www.digitalconcerthall.com',
|
'Referer': 'https://www.digitalconcerthall.com',
|
||||||
'Authorization': f'Bearer {self._ACCESS_TOKEN}',
|
'Authorization': f'Bearer {login_token}',
|
||||||
|
'User-Agent': self._USER_AGENT,
|
||||||
})
|
})
|
||||||
except ExtractorError:
|
except ExtractorError as error:
|
||||||
self.raise_login_required(msg='Login info incorrect')
|
if isinstance(error.cause, HTTPError) and error.cause.status == 401:
|
||||||
|
raise ExtractorError('Invalid username or password', expected=True)
|
||||||
|
raise
|
||||||
|
self._ACCESS_TOKEN = login_response['access_token']
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
if not self._ACCESS_TOKEN:
|
if not self._ACCESS_TOKEN:
|
||||||
|
@ -108,11 +120,15 @@ def _entries(self, items, language, type_, **kwargs):
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Authorization': f'Bearer {self._ACCESS_TOKEN}',
|
'Authorization': f'Bearer {self._ACCESS_TOKEN}',
|
||||||
'Accept-Language': language,
|
'Accept-Language': language,
|
||||||
|
'User-Agent': self._USER_AGENT,
|
||||||
})
|
})
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for m3u8_url in traverse_obj(stream_info, ('channel', ..., 'stream', ..., 'url', {url_or_none})):
|
for m3u8_url in traverse_obj(stream_info, ('channel', ..., 'stream', ..., 'url', {url_or_none})):
|
||||||
formats.extend(self._extract_m3u8_formats(m3u8_url, video_id, 'mp4', fatal=False))
|
formats.extend(self._extract_m3u8_formats(m3u8_url, video_id, 'mp4', m3u8_id='hls', fatal=False))
|
||||||
|
for fmt in formats:
|
||||||
|
if fmt.get('format_note') and fmt.get('vcodec') == 'none':
|
||||||
|
fmt.update(parse_codecs(fmt['format_note']))
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
@ -140,13 +156,15 @@ def _real_extract(self, url):
|
||||||
f'https://api.digitalconcerthall.com/v2/{api_type}/{video_id}', video_id, headers={
|
f'https://api.digitalconcerthall.com/v2/{api_type}/{video_id}', video_id, headers={
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Accept-Language': language,
|
'Accept-Language': language,
|
||||||
|
'User-Agent': self._USER_AGENT,
|
||||||
|
'Authorization': f'Bearer {self._ACCESS_TOKEN}',
|
||||||
})
|
})
|
||||||
album_artists = traverse_obj(vid_info, ('_links', 'artist', ..., 'name'))
|
|
||||||
videos = [vid_info] if type_ == 'film' else traverse_obj(vid_info, ('_embedded', ..., ...))
|
videos = [vid_info] if type_ == 'film' else traverse_obj(vid_info, ('_embedded', ..., ...))
|
||||||
|
|
||||||
if type_ == 'work':
|
if type_ == 'work':
|
||||||
videos = [videos[int(part) - 1]]
|
videos = [videos[int(part) - 1]]
|
||||||
|
|
||||||
|
album_artists = traverse_obj(vid_info, ('_links', 'artist', ..., 'name', {str}))
|
||||||
thumbnail = traverse_obj(vid_info, (
|
thumbnail = traverse_obj(vid_info, (
|
||||||
'image', ..., {self._proto_relative_url}, {url_or_none},
|
'image', ..., {self._proto_relative_url}, {url_or_none},
|
||||||
{lambda x: x.format(width=0, height=0)}, any)) # NB: 0x0 is the original size
|
{lambda x: x.format(width=0, height=0)}, any)) # NB: 0x0 is the original size
|
||||||
|
|
|
@ -1,115 +0,0 @@
|
||||||
import random
|
|
||||||
import string
|
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
from .discoverygo import DiscoveryGoBaseIE
|
|
||||||
from ..networking.exceptions import HTTPError
|
|
||||||
from ..utils import ExtractorError
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryIE(DiscoveryGoBaseIE):
|
|
||||||
_VALID_URL = r'''(?x)https?://
|
|
||||||
(?P<site>
|
|
||||||
go\.discovery|
|
|
||||||
www\.
|
|
||||||
(?:
|
|
||||||
investigationdiscovery|
|
|
||||||
discoverylife|
|
|
||||||
animalplanet|
|
|
||||||
ahctv|
|
|
||||||
destinationamerica|
|
|
||||||
sciencechannel|
|
|
||||||
tlc
|
|
||||||
)|
|
|
||||||
watch\.
|
|
||||||
(?:
|
|
||||||
hgtv|
|
|
||||||
foodnetwork|
|
|
||||||
travelchannel|
|
|
||||||
diynetwork|
|
|
||||||
cookingchanneltv|
|
|
||||||
motortrend
|
|
||||||
)
|
|
||||||
)\.com/tv-shows/(?P<show_slug>[^/]+)/(?:video|full-episode)s/(?P<id>[^./?#]+)'''
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://go.discovery.com/tv-shows/cash-cab/videos/riding-with-matthew-perry',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '5a2f35ce6b66d17a5026e29e',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Riding with Matthew Perry',
|
|
||||||
'description': 'md5:a34333153e79bc4526019a5129e7f878',
|
|
||||||
'duration': 84,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True, # requires ffmpeg
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.investigationdiscovery.com/tv-shows/final-vision/full-episodes/final-vision',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://go.discovery.com/tv-shows/alaskan-bush-people/videos/follow-your-own-road',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
# using `show_slug` is important to get the correct video data
|
|
||||||
'url': 'https://www.sciencechannel.com/tv-shows/mythbusters-on-science/full-episodes/christmas-special',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
_GEO_COUNTRIES = ['US']
|
|
||||||
_GEO_BYPASS = False
|
|
||||||
_API_BASE_URL = 'https://api.discovery.com/v1/'
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
site, show_slug, display_id = self._match_valid_url(url).groups()
|
|
||||||
|
|
||||||
access_token = None
|
|
||||||
cookies = self._get_cookies(url)
|
|
||||||
|
|
||||||
# prefer Affiliate Auth Token over Anonymous Auth Token
|
|
||||||
auth_storage_cookie = cookies.get('eosAf') or cookies.get('eosAn')
|
|
||||||
if auth_storage_cookie and auth_storage_cookie.value:
|
|
||||||
auth_storage = self._parse_json(urllib.parse.unquote(
|
|
||||||
urllib.parse.unquote(auth_storage_cookie.value)),
|
|
||||||
display_id, fatal=False) or {}
|
|
||||||
access_token = auth_storage.get('a') or auth_storage.get('access_token')
|
|
||||||
|
|
||||||
if not access_token:
|
|
||||||
access_token = self._download_json(
|
|
||||||
f'https://{site}.com/anonymous', display_id,
|
|
||||||
'Downloading token JSON metadata', query={
|
|
||||||
'authRel': 'authorization',
|
|
||||||
'client_id': '3020a40c2356a645b4b4',
|
|
||||||
'nonce': ''.join(random.choices(string.ascii_letters, k=32)),
|
|
||||||
'redirectUri': 'https://www.discovery.com/',
|
|
||||||
})['access_token']
|
|
||||||
|
|
||||||
headers = self.geo_verification_headers()
|
|
||||||
headers['Authorization'] = 'Bearer ' + access_token
|
|
||||||
|
|
||||||
try:
|
|
||||||
video = self._download_json(
|
|
||||||
self._API_BASE_URL + 'content/videos',
|
|
||||||
display_id, 'Downloading content JSON metadata',
|
|
||||||
headers=headers, query={
|
|
||||||
'embed': 'show.name',
|
|
||||||
'fields': 'authenticated,description.detailed,duration,episodeNumber,id,name,parental.rating,season.number,show,tags',
|
|
||||||
'slug': display_id,
|
|
||||||
'show_slug': show_slug,
|
|
||||||
})[0]
|
|
||||||
video_id = video['id']
|
|
||||||
stream = self._download_json(
|
|
||||||
self._API_BASE_URL + 'streaming/video/' + video_id,
|
|
||||||
display_id, 'Downloading streaming JSON metadata', headers=headers)
|
|
||||||
except ExtractorError as e:
|
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status in (401, 403):
|
|
||||||
e_description = self._parse_json(
|
|
||||||
e.cause.response.read().decode(), display_id)['description']
|
|
||||||
if 'resource not available for country' in e_description:
|
|
||||||
self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
|
|
||||||
if 'Authorized Networks' in e_description:
|
|
||||||
raise ExtractorError(
|
|
||||||
'This video is only available via cable service provider subscription that'
|
|
||||||
' is not currently supported. You may want to use --cookies.', expected=True)
|
|
||||||
raise ExtractorError(e_description)
|
|
||||||
raise
|
|
||||||
|
|
||||||
return self._extract_video_info(video, stream, display_id)
|
|
|
@ -1,171 +0,0 @@
|
||||||
import re
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import (
|
|
||||||
ExtractorError,
|
|
||||||
determine_ext,
|
|
||||||
extract_attributes,
|
|
||||||
int_or_none,
|
|
||||||
parse_age_limit,
|
|
||||||
remove_end,
|
|
||||||
unescapeHTML,
|
|
||||||
url_or_none,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryGoBaseIE(InfoExtractor):
|
|
||||||
_VALID_URL_TEMPLATE = r'''(?x)https?://(?:www\.)?(?:
|
|
||||||
discovery|
|
|
||||||
investigationdiscovery|
|
|
||||||
discoverylife|
|
|
||||||
animalplanet|
|
|
||||||
ahctv|
|
|
||||||
destinationamerica|
|
|
||||||
sciencechannel|
|
|
||||||
tlc|
|
|
||||||
velocitychannel
|
|
||||||
)go\.com/%s(?P<id>[^/?#&]+)'''
|
|
||||||
|
|
||||||
def _extract_video_info(self, video, stream, display_id):
|
|
||||||
title = video['name']
|
|
||||||
|
|
||||||
if not stream:
|
|
||||||
if video.get('authenticated') is True:
|
|
||||||
raise ExtractorError(
|
|
||||||
'This video is only available via cable service provider subscription that'
|
|
||||||
' is not currently supported. You may want to use --cookies.', expected=True)
|
|
||||||
else:
|
|
||||||
raise ExtractorError('Unable to find stream')
|
|
||||||
STREAM_URL_SUFFIX = 'streamUrl'
|
|
||||||
formats = []
|
|
||||||
for stream_kind in ('', 'hds'):
|
|
||||||
suffix = STREAM_URL_SUFFIX.capitalize() if stream_kind else STREAM_URL_SUFFIX
|
|
||||||
stream_url = stream.get(f'{stream_kind}{suffix}')
|
|
||||||
if not stream_url:
|
|
||||||
continue
|
|
||||||
if stream_kind == '':
|
|
||||||
formats.extend(self._extract_m3u8_formats(
|
|
||||||
stream_url, display_id, 'mp4', entry_protocol='m3u8_native',
|
|
||||||
m3u8_id='hls', fatal=False))
|
|
||||||
elif stream_kind == 'hds':
|
|
||||||
formats.extend(self._extract_f4m_formats(
|
|
||||||
stream_url, display_id, f4m_id=stream_kind, fatal=False))
|
|
||||||
|
|
||||||
video_id = video.get('id') or display_id
|
|
||||||
description = video.get('description', {}).get('detailed')
|
|
||||||
duration = int_or_none(video.get('duration'))
|
|
||||||
|
|
||||||
series = video.get('show', {}).get('name')
|
|
||||||
season_number = int_or_none(video.get('season', {}).get('number'))
|
|
||||||
episode_number = int_or_none(video.get('episodeNumber'))
|
|
||||||
|
|
||||||
tags = video.get('tags')
|
|
||||||
age_limit = parse_age_limit(video.get('parental', {}).get('rating'))
|
|
||||||
|
|
||||||
subtitles = {}
|
|
||||||
captions = stream.get('captions')
|
|
||||||
if isinstance(captions, list):
|
|
||||||
for caption in captions:
|
|
||||||
subtitle_url = url_or_none(caption.get('fileUrl'))
|
|
||||||
if not subtitle_url or not subtitle_url.startswith('http'):
|
|
||||||
continue
|
|
||||||
lang = caption.get('fileLang', 'en')
|
|
||||||
ext = determine_ext(subtitle_url)
|
|
||||||
subtitles.setdefault(lang, []).append({
|
|
||||||
'url': subtitle_url,
|
|
||||||
'ext': 'ttml' if ext == 'xml' else ext,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'display_id': display_id,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'duration': duration,
|
|
||||||
'series': series,
|
|
||||||
'season_number': season_number,
|
|
||||||
'episode_number': episode_number,
|
|
||||||
'tags': tags,
|
|
||||||
'age_limit': age_limit,
|
|
||||||
'formats': formats,
|
|
||||||
'subtitles': subtitles,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryGoIE(DiscoveryGoBaseIE):
|
|
||||||
_VALID_URL = DiscoveryGoBaseIE._VALID_URL_TEMPLATE % r'(?:[^/]+/)+'
|
|
||||||
_GEO_COUNTRIES = ['US']
|
|
||||||
_TEST = {
|
|
||||||
'url': 'https://www.discoverygo.com/bering-sea-gold/reaper-madness/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '58c167d86b66d12f2addeb01',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Reaper Madness',
|
|
||||||
'description': 'md5:09f2c625c99afb8946ed4fb7865f6e78',
|
|
||||||
'duration': 2519,
|
|
||||||
'series': 'Bering Sea Gold',
|
|
||||||
'season_number': 8,
|
|
||||||
'episode_number': 6,
|
|
||||||
'age_limit': 14,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
display_id = self._match_id(url)
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, display_id)
|
|
||||||
|
|
||||||
container = extract_attributes(
|
|
||||||
self._search_regex(
|
|
||||||
r'(<div[^>]+class=["\']video-player-container[^>]+>)',
|
|
||||||
webpage, 'video container'))
|
|
||||||
|
|
||||||
video = self._parse_json(
|
|
||||||
container.get('data-video') or container.get('data-json'),
|
|
||||||
display_id)
|
|
||||||
|
|
||||||
stream = video.get('stream')
|
|
||||||
|
|
||||||
return self._extract_video_info(video, stream, display_id)
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryGoPlaylistIE(DiscoveryGoBaseIE):
|
|
||||||
_VALID_URL = DiscoveryGoBaseIE._VALID_URL_TEMPLATE % ''
|
|
||||||
_TEST = {
|
|
||||||
'url': 'https://www.discoverygo.com/bering-sea-gold/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'bering-sea-gold',
|
|
||||||
'title': 'Bering Sea Gold',
|
|
||||||
'description': 'md5:cc5c6489835949043c0cc3ad66c2fa0e',
|
|
||||||
},
|
|
||||||
'playlist_mincount': 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def suitable(cls, url):
|
|
||||||
return False if DiscoveryGoIE.suitable(url) else super().suitable(url)
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
display_id = self._match_id(url)
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, display_id)
|
|
||||||
|
|
||||||
entries = []
|
|
||||||
for mobj in re.finditer(r'data-json=(["\'])(?P<json>{.+?})\1', webpage):
|
|
||||||
data = self._parse_json(
|
|
||||||
mobj.group('json'), display_id,
|
|
||||||
transform_source=unescapeHTML, fatal=False)
|
|
||||||
if not isinstance(data, dict) or data.get('type') != 'episode':
|
|
||||||
continue
|
|
||||||
episode_url = data.get('socialUrl')
|
|
||||||
if not episode_url:
|
|
||||||
continue
|
|
||||||
entries.append(self.url_result(
|
|
||||||
episode_url, ie=DiscoveryGoIE.ie_key(),
|
|
||||||
video_id=data.get('id')))
|
|
||||||
|
|
||||||
return self.playlist_result(
|
|
||||||
entries, display_id,
|
|
||||||
remove_end(self._og_search_title(
|
|
||||||
webpage, fatal=False), ' | Discovery GO'),
|
|
||||||
self._og_search_description(webpage))
|
|
|
@ -346,8 +346,16 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryPlusBaseIE(DPlayBaseIE):
|
class DiscoveryPlusBaseIE(DPlayBaseIE):
|
||||||
|
"""Subclasses must set _PRODUCT, _DISCO_API_PARAMS"""
|
||||||
|
|
||||||
|
_DISCO_CLIENT_VER = '27.43.0'
|
||||||
|
|
||||||
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
||||||
headers['x-disco-client'] = f'WEB:UNKNOWN:{self._PRODUCT}:25.2.6'
|
headers.update({
|
||||||
|
'x-disco-params': f'realm={realm},siteLookupKey={self._PRODUCT}',
|
||||||
|
'x-disco-client': f'WEB:UNKNOWN:{self._PRODUCT}:{self._DISCO_CLIENT_VER}',
|
||||||
|
'Authorization': self._get_auth(disco_base, display_id, realm),
|
||||||
|
})
|
||||||
|
|
||||||
def _download_video_playback_info(self, disco_base, video_id, headers):
|
def _download_video_playback_info(self, disco_base, video_id, headers):
|
||||||
return self._download_json(
|
return self._download_json(
|
||||||
|
@ -368,6 +376,26 @@ def _real_extract(self, url):
|
||||||
class GoDiscoveryIE(DiscoveryPlusBaseIE):
|
class GoDiscoveryIE(DiscoveryPlusBaseIE):
|
||||||
_VALID_URL = r'https?://(?:go\.)?discovery\.com/video' + DPlayBaseIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:go\.)?discovery\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://go.discovery.com/video/in-the-eye-of-the-storm-discovery-atve-us/trapped-in-a-twister',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5352642',
|
||||||
|
'display_id': 'in-the-eye-of-the-storm-discovery-atve-us/trapped-in-a-twister',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Trapped in a Twister',
|
||||||
|
'description': 'Twisters destroy Midwest towns, trapping spotters in the eye of the storm.',
|
||||||
|
'episode_number': 1,
|
||||||
|
'episode': 'Episode 1',
|
||||||
|
'season_number': 1,
|
||||||
|
'season': 'Season 1',
|
||||||
|
'series': 'In The Eye Of The Storm',
|
||||||
|
'duration': 2490.237,
|
||||||
|
'upload_date': '20240715',
|
||||||
|
'timestamp': 1721008800,
|
||||||
|
'tags': [],
|
||||||
|
'creators': ['Discovery'],
|
||||||
|
'thumbnail': 'https://us1-prod-images.disco-api.com/2024/07/10/5e39637d-cabf-3ab3-8e9a-f4e9d37bc036.jpeg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'https://go.discovery.com/video/dirty-jobs-discovery-atve-us/rodbuster-galvanizer',
|
'url': 'https://go.discovery.com/video/dirty-jobs-discovery-atve-us/rodbuster-galvanizer',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '4164906',
|
'id': '4164906',
|
||||||
|
@ -395,6 +423,26 @@ class GoDiscoveryIE(DiscoveryPlusBaseIE):
|
||||||
class TravelChannelIE(DiscoveryPlusBaseIE):
|
class TravelChannelIE(DiscoveryPlusBaseIE):
|
||||||
_VALID_URL = r'https?://(?:watch\.)?travelchannel\.com/video' + DPlayBaseIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:watch\.)?travelchannel\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://watch.travelchannel.com/video/the-dead-files-travel-channel/protect-the-children',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4710177',
|
||||||
|
'display_id': 'the-dead-files-travel-channel/protect-the-children',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Protect the Children',
|
||||||
|
'description': 'An evil presence threatens an Ohio woman\'s children and marriage.',
|
||||||
|
'season_number': 14,
|
||||||
|
'season': 'Season 14',
|
||||||
|
'episode_number': 10,
|
||||||
|
'episode': 'Episode 10',
|
||||||
|
'series': 'The Dead Files',
|
||||||
|
'duration': 2550.481,
|
||||||
|
'timestamp': 1664510400,
|
||||||
|
'upload_date': '20220930',
|
||||||
|
'tags': [],
|
||||||
|
'creators': ['Travel Channel'],
|
||||||
|
'thumbnail': 'https://us1-prod-images.disco-api.com/2022/03/17/5e45eace-de5d-343a-9293-f400a2aa77d5.jpeg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'https://watch.travelchannel.com/video/ghost-adventures-travel-channel/ghost-train-of-ely',
|
'url': 'https://watch.travelchannel.com/video/ghost-adventures-travel-channel/ghost-train-of-ely',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2220256',
|
'id': '2220256',
|
||||||
|
@ -422,6 +470,26 @@ class TravelChannelIE(DiscoveryPlusBaseIE):
|
||||||
class CookingChannelIE(DiscoveryPlusBaseIE):
|
class CookingChannelIE(DiscoveryPlusBaseIE):
|
||||||
_VALID_URL = r'https?://(?:watch\.)?cookingchanneltv\.com/video' + DPlayBaseIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:watch\.)?cookingchanneltv\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://watch.cookingchanneltv.com/video/bobbys-triple-threat-food-network-atve-us/titans-vs-marcus-samuelsson',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5350005',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'display_id': 'bobbys-triple-threat-food-network-atve-us/titans-vs-marcus-samuelsson',
|
||||||
|
'title': 'Titans vs Marcus Samuelsson',
|
||||||
|
'description': 'Marcus Samuelsson throws his legendary global tricks at the Titans.',
|
||||||
|
'episode_number': 1,
|
||||||
|
'episode': 'Episode 1',
|
||||||
|
'season_number': 3,
|
||||||
|
'season': 'Season 3',
|
||||||
|
'series': 'Bobby\'s Triple Threat',
|
||||||
|
'duration': 2520.851,
|
||||||
|
'upload_date': '20240710',
|
||||||
|
'timestamp': 1720573200,
|
||||||
|
'tags': [],
|
||||||
|
'creators': ['Food Network'],
|
||||||
|
'thumbnail': 'https://us1-prod-images.disco-api.com/2024/07/04/529cd095-27ec-35c5-84e9-90ebd3e5d2da.jpeg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'https://watch.cookingchanneltv.com/video/carnival-eats-cooking-channel/the-postman-always-brings-rice-2348634',
|
'url': 'https://watch.cookingchanneltv.com/video/carnival-eats-cooking-channel/the-postman-always-brings-rice-2348634',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2348634',
|
'id': '2348634',
|
||||||
|
@ -449,6 +517,22 @@ class CookingChannelIE(DiscoveryPlusBaseIE):
|
||||||
class HGTVUsaIE(DiscoveryPlusBaseIE):
|
class HGTVUsaIE(DiscoveryPlusBaseIE):
|
||||||
_VALID_URL = r'https?://(?:watch\.)?hgtv\.com/video' + DPlayBaseIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:watch\.)?hgtv\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://watch.hgtv.com/video/flip-or-flop-the-final-flip-hgtv-atve-us/flip-or-flop-the-final-flip',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5025585',
|
||||||
|
'display_id': 'flip-or-flop-the-final-flip-hgtv-atve-us/flip-or-flop-the-final-flip',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Flip or Flop: The Final Flip',
|
||||||
|
'description': 'Tarek and Christina are going their separate ways after one last flip!',
|
||||||
|
'series': 'Flip or Flop: The Final Flip',
|
||||||
|
'duration': 2580.644,
|
||||||
|
'upload_date': '20231101',
|
||||||
|
'timestamp': 1698811200,
|
||||||
|
'tags': [],
|
||||||
|
'creators': ['HGTV'],
|
||||||
|
'thumbnail': 'https://us1-prod-images.disco-api.com/2022/11/27/455caa6c-1462-3f14-b63d-a026d7a5e6d3.jpeg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'https://watch.hgtv.com/video/home-inspector-joe-hgtv-atve-us/this-mold-house',
|
'url': 'https://watch.hgtv.com/video/home-inspector-joe-hgtv-atve-us/this-mold-house',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '4289736',
|
'id': '4289736',
|
||||||
|
@ -476,6 +560,26 @@ class HGTVUsaIE(DiscoveryPlusBaseIE):
|
||||||
class FoodNetworkIE(DiscoveryPlusBaseIE):
|
class FoodNetworkIE(DiscoveryPlusBaseIE):
|
||||||
_VALID_URL = r'https?://(?:watch\.)?foodnetwork\.com/video' + DPlayBaseIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:watch\.)?foodnetwork\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://watch.foodnetwork.com/video/guys-grocery-games-food-network/wild-in-the-aisles',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2152549',
|
||||||
|
'display_id': 'guys-grocery-games-food-network/wild-in-the-aisles',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Wild in the Aisles',
|
||||||
|
'description': 'The chefs make spaghetti and meatballs with "Out of Stock" ingredients.',
|
||||||
|
'season_number': 1,
|
||||||
|
'season': 'Season 1',
|
||||||
|
'episode_number': 1,
|
||||||
|
'episode': 'Episode 1',
|
||||||
|
'series': 'Guy\'s Grocery Games',
|
||||||
|
'tags': [],
|
||||||
|
'creators': ['Food Network'],
|
||||||
|
'duration': 2520.651,
|
||||||
|
'upload_date': '20230623',
|
||||||
|
'timestamp': 1687492800,
|
||||||
|
'thumbnail': 'https://us1-prod-images.disco-api.com/2022/06/15/37fb5333-cad2-3dbb-af7c-c20ec77c89c6.jpeg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'https://watch.foodnetwork.com/video/kids-baking-championship-food-network/float-like-a-butterfly',
|
'url': 'https://watch.foodnetwork.com/video/kids-baking-championship-food-network/float-like-a-butterfly',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '4116449',
|
'id': '4116449',
|
||||||
|
@ -503,6 +607,26 @@ class FoodNetworkIE(DiscoveryPlusBaseIE):
|
||||||
class DestinationAmericaIE(DiscoveryPlusBaseIE):
|
class DestinationAmericaIE(DiscoveryPlusBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?destinationamerica\.com/video' + DPlayBaseIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:www\.)?destinationamerica\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://www.destinationamerica.com/video/bbq-pit-wars-destination-america/smoke-on-the-water',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2218409',
|
||||||
|
'display_id': 'bbq-pit-wars-destination-america/smoke-on-the-water',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Smoke on the Water',
|
||||||
|
'description': 'The pitmasters head to Georgia for the Smoke on the Water BBQ Festival.',
|
||||||
|
'season_number': 2,
|
||||||
|
'season': 'Season 2',
|
||||||
|
'episode_number': 1,
|
||||||
|
'episode': 'Episode 1',
|
||||||
|
'series': 'BBQ Pit Wars',
|
||||||
|
'tags': [],
|
||||||
|
'creators': ['Destination America'],
|
||||||
|
'duration': 2614.878,
|
||||||
|
'upload_date': '20230623',
|
||||||
|
'timestamp': 1687492800,
|
||||||
|
'thumbnail': 'https://us1-prod-images.disco-api.com/2020/05/11/c0f8e85d-9a10-3e6f-8e43-f6faafa81ba2.jpeg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'https://www.destinationamerica.com/video/alaska-monsters-destination-america-atve-us/central-alaskas-bigfoot',
|
'url': 'https://www.destinationamerica.com/video/alaska-monsters-destination-america-atve-us/central-alaskas-bigfoot',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '4210904',
|
'id': '4210904',
|
||||||
|
@ -530,6 +654,26 @@ class DestinationAmericaIE(DiscoveryPlusBaseIE):
|
||||||
class InvestigationDiscoveryIE(DiscoveryPlusBaseIE):
|
class InvestigationDiscoveryIE(DiscoveryPlusBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?investigationdiscovery\.com/video' + DPlayBaseIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:www\.)?investigationdiscovery\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://www.investigationdiscovery.com/video/deadly-influence-the-social-media-murders-investigation-discovery-atve-us/rip-bianca',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5341132',
|
||||||
|
'display_id': 'deadly-influence-the-social-media-murders-investigation-discovery-atve-us/rip-bianca',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'RIP Bianca',
|
||||||
|
'description': 'A teenage influencer discovers an online world of threat, harm and danger.',
|
||||||
|
'season_number': 1,
|
||||||
|
'season': 'Season 1',
|
||||||
|
'episode_number': 3,
|
||||||
|
'episode': 'Episode 3',
|
||||||
|
'series': 'Deadly Influence: The Social Media Murders',
|
||||||
|
'creators': ['Investigation Discovery'],
|
||||||
|
'tags': [],
|
||||||
|
'duration': 2490.888,
|
||||||
|
'upload_date': '20240618',
|
||||||
|
'timestamp': 1718672400,
|
||||||
|
'thumbnail': 'https://us1-prod-images.disco-api.com/2024/06/15/b567c774-9e44-3c6c-b0ba-db860a73e812.jpeg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'https://www.investigationdiscovery.com/video/unmasked-investigation-discovery/the-killer-clown',
|
'url': 'https://www.investigationdiscovery.com/video/unmasked-investigation-discovery/the-killer-clown',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2139409',
|
'id': '2139409',
|
||||||
|
@ -557,6 +701,26 @@ class InvestigationDiscoveryIE(DiscoveryPlusBaseIE):
|
||||||
class AmHistoryChannelIE(DiscoveryPlusBaseIE):
|
class AmHistoryChannelIE(DiscoveryPlusBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?ahctv\.com/video' + DPlayBaseIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:www\.)?ahctv\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://www.ahctv.com/video/blood-and-fury-americas-civil-war-ahc/battle-of-bull-run',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2139199',
|
||||||
|
'display_id': 'blood-and-fury-americas-civil-war-ahc/battle-of-bull-run',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Battle of Bull Run',
|
||||||
|
'description': 'Two untested armies clash in the first real battle of the Civil War.',
|
||||||
|
'season_number': 1,
|
||||||
|
'season': 'Season 1',
|
||||||
|
'episode_number': 1,
|
||||||
|
'episode': 'Episode 1',
|
||||||
|
'series': 'Blood and Fury: America\'s Civil War',
|
||||||
|
'duration': 2612.509,
|
||||||
|
'upload_date': '20220923',
|
||||||
|
'timestamp': 1663905600,
|
||||||
|
'creators': ['AHC'],
|
||||||
|
'tags': [],
|
||||||
|
'thumbnail': 'https://us1-prod-images.disco-api.com/2020/05/11/4af61bd7-d705-3108-82c4-1a6e541e20fa.jpeg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'https://www.ahctv.com/video/modern-sniper-ahc/army',
|
'url': 'https://www.ahctv.com/video/modern-sniper-ahc/army',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2309730',
|
'id': '2309730',
|
||||||
|
@ -584,6 +748,26 @@ class AmHistoryChannelIE(DiscoveryPlusBaseIE):
|
||||||
class ScienceChannelIE(DiscoveryPlusBaseIE):
|
class ScienceChannelIE(DiscoveryPlusBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?sciencechannel\.com/video' + DPlayBaseIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:www\.)?sciencechannel\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://www.sciencechannel.com/video/spaces-deepest-secrets-science-atve-us/mystery-of-the-dead-planets',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2347335',
|
||||||
|
'display_id': 'spaces-deepest-secrets-science-atve-us/mystery-of-the-dead-planets',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Mystery of the Dead Planets',
|
||||||
|
'description': 'Astronomers unmask the truly destructive nature of the cosmos.',
|
||||||
|
'season_number': 7,
|
||||||
|
'season': 'Season 7',
|
||||||
|
'episode_number': 1,
|
||||||
|
'episode': 'Episode 1',
|
||||||
|
'series': 'Space\'s Deepest Secrets',
|
||||||
|
'duration': 2524.989,
|
||||||
|
'upload_date': '20230128',
|
||||||
|
'timestamp': 1674882000,
|
||||||
|
'creators': ['Science'],
|
||||||
|
'tags': [],
|
||||||
|
'thumbnail': 'https://us1-prod-images.disco-api.com/2021/03/30/3796829d-aead-3f9a-bd8d-e49048b3cdca.jpeg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'https://www.sciencechannel.com/video/strangest-things-science-atve-us/nazi-mystery-machine',
|
'url': 'https://www.sciencechannel.com/video/strangest-things-science-atve-us/nazi-mystery-machine',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2842849',
|
'id': '2842849',
|
||||||
|
@ -608,36 +792,29 @@ class ScienceChannelIE(DiscoveryPlusBaseIE):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DIYNetworkIE(DiscoveryPlusBaseIE):
|
|
||||||
_VALID_URL = r'https?://(?:watch\.)?diynetwork\.com/video' + DPlayBaseIE._PATH_REGEX
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://watch.diynetwork.com/video/pool-kings-diy-network/bringing-beach-life-to-texas',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '2309730',
|
|
||||||
'display_id': 'pool-kings-diy-network/bringing-beach-life-to-texas',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Bringing Beach Life to Texas',
|
|
||||||
'description': 'The Pool Kings give a family a day at the beach in their own backyard.',
|
|
||||||
'season_number': 10,
|
|
||||||
'episode_number': 2,
|
|
||||||
},
|
|
||||||
'skip': 'Available for Premium users',
|
|
||||||
}, {
|
|
||||||
'url': 'https://watch.diynetwork.com/video/pool-kings-diy-network/bringing-beach-life-to-texas',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
_PRODUCT = 'diy'
|
|
||||||
_DISCO_API_PARAMS = {
|
|
||||||
'disco_host': 'us1-prod-direct.watch.diynetwork.com',
|
|
||||||
'realm': 'go',
|
|
||||||
'country': 'us',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryLifeIE(DiscoveryPlusBaseIE):
|
class DiscoveryLifeIE(DiscoveryPlusBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?discoverylife\.com/video' + DPlayBaseIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:www\.)?discoverylife\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://www.discoverylife.com/video/er-files-discovery-life-atve-us/sweet-charity',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2347614',
|
||||||
|
'display_id': 'er-files-discovery-life-atve-us/sweet-charity',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Sweet Charity',
|
||||||
|
'description': 'The staff at Charity Hospital treat a serious foot infection.',
|
||||||
|
'season_number': 1,
|
||||||
|
'season': 'Season 1',
|
||||||
|
'episode_number': 1,
|
||||||
|
'episode': 'Episode 1',
|
||||||
|
'series': 'ER Files',
|
||||||
|
'duration': 2364.261,
|
||||||
|
'upload_date': '20230721',
|
||||||
|
'timestamp': 1689912000,
|
||||||
|
'creators': ['Discovery Life'],
|
||||||
|
'tags': [],
|
||||||
|
'thumbnail': 'https://us1-prod-images.disco-api.com/2021/03/16/4b6f0124-360b-3546-b6a4-5552db886b86.jpeg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'https://www.discoverylife.com/video/surviving-death-discovery-life-atve-us/bodily-trauma',
|
'url': 'https://www.discoverylife.com/video/surviving-death-discovery-life-atve-us/bodily-trauma',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2218238',
|
'id': '2218238',
|
||||||
|
@ -665,6 +842,26 @@ class DiscoveryLifeIE(DiscoveryPlusBaseIE):
|
||||||
class AnimalPlanetIE(DiscoveryPlusBaseIE):
|
class AnimalPlanetIE(DiscoveryPlusBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?animalplanet\.com/video' + DPlayBaseIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:www\.)?animalplanet\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://www.animalplanet.com/video/mysterious-creatures-with-forrest-galante-animal-planet-atve-us/the-demon-of-peru',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4650835',
|
||||||
|
'display_id': 'mysterious-creatures-with-forrest-galante-animal-planet-atve-us/the-demon-of-peru',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'The Demon of Peru',
|
||||||
|
'description': 'In Peru, a farming village is being terrorized by a “man-like beast.”',
|
||||||
|
'season_number': 1,
|
||||||
|
'season': 'Season 1',
|
||||||
|
'episode_number': 4,
|
||||||
|
'episode': 'Episode 4',
|
||||||
|
'series': 'Mysterious Creatures with Forrest Galante',
|
||||||
|
'duration': 2490.488,
|
||||||
|
'upload_date': '20230111',
|
||||||
|
'timestamp': 1673413200,
|
||||||
|
'creators': ['Animal Planet'],
|
||||||
|
'tags': [],
|
||||||
|
'thumbnail': 'https://us1-prod-images.disco-api.com/2022/03/01/6dbaa833-9a2e-3fee-9381-c19eddf67c0c.jpeg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'https://www.animalplanet.com/video/north-woods-law-animal-planet/squirrel-showdown',
|
'url': 'https://www.animalplanet.com/video/north-woods-law-animal-planet/squirrel-showdown',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '3338923',
|
'id': '3338923',
|
||||||
|
@ -692,6 +889,26 @@ class AnimalPlanetIE(DiscoveryPlusBaseIE):
|
||||||
class TLCIE(DiscoveryPlusBaseIE):
|
class TLCIE(DiscoveryPlusBaseIE):
|
||||||
_VALID_URL = r'https?://(?:go\.)?tlc\.com/video' + DPlayBaseIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:go\.)?tlc\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://go.tlc.com/video/90-day-the-last-resort-tlc-atve-us/the-last-chance',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5186422',
|
||||||
|
'display_id': '90-day-the-last-resort-tlc-atve-us/the-last-chance',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'The Last Chance',
|
||||||
|
'description': 'Infidelity shakes Kalani and Asuelu\'s world, and Angela threatens divorce.',
|
||||||
|
'season_number': 1,
|
||||||
|
'season': 'Season 1',
|
||||||
|
'episode_number': 1,
|
||||||
|
'episode': 'Episode 1',
|
||||||
|
'series': '90 Day: The Last Resort',
|
||||||
|
'duration': 5123.91,
|
||||||
|
'upload_date': '20230815',
|
||||||
|
'timestamp': 1692061200,
|
||||||
|
'creators': ['TLC'],
|
||||||
|
'tags': [],
|
||||||
|
'thumbnail': 'https://us1-prod-images.disco-api.com/2023/08/08/0ee367e2-ac76-334d-bf23-dbf796696a24.jpeg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'https://go.tlc.com/video/my-600-lb-life-tlc/melissas-story-part-1',
|
'url': 'https://go.tlc.com/video/my-600-lb-life-tlc/melissas-story-part-1',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2206540',
|
'id': '2206540',
|
||||||
|
@ -716,93 +933,8 @@ class TLCIE(DiscoveryPlusBaseIE):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class MotorTrendIE(DiscoveryPlusBaseIE):
|
|
||||||
_VALID_URL = r'https?://(?:watch\.)?motortrend\.com/video' + DPlayBaseIE._PATH_REGEX
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://watch.motortrend.com/video/car-issues-motortrend-atve-us/double-dakotas',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '"4859182"',
|
|
||||||
'display_id': 'double-dakotas',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Double Dakotas',
|
|
||||||
'description': 'Tylers buy-one-get-one Dakota deal has the Wizard pulling double duty.',
|
|
||||||
'season_number': 2,
|
|
||||||
'episode_number': 3,
|
|
||||||
},
|
|
||||||
'skip': 'Available for Premium users',
|
|
||||||
}, {
|
|
||||||
'url': 'https://watch.motortrend.com/video/car-issues-motortrend-atve-us/double-dakotas',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
_PRODUCT = 'vel'
|
|
||||||
_DISCO_API_PARAMS = {
|
|
||||||
'disco_host': 'us1-prod-direct.watch.motortrend.com',
|
|
||||||
'realm': 'go',
|
|
||||||
'country': 'us',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class MotorTrendOnDemandIE(DiscoveryPlusBaseIE):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?motortrend(?:ondemand\.com|\.com/plus)/detail' + DPlayBaseIE._PATH_REGEX
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.motortrendondemand.com/detail/wheelstanding-dump-truck-stubby-bobs-comeback/37699/784',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '37699',
|
|
||||||
'display_id': 'wheelstanding-dump-truck-stubby-bobs-comeback/37699',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Wheelstanding Dump Truck! Stubby Bob’s Comeback',
|
|
||||||
'description': 'md5:996915abe52a1c3dfc83aecea3cce8e7',
|
|
||||||
'season_number': 5,
|
|
||||||
'episode_number': 52,
|
|
||||||
'episode': 'Episode 52',
|
|
||||||
'season': 'Season 5',
|
|
||||||
'thumbnail': r're:^https?://.+\.jpe?g$',
|
|
||||||
'timestamp': 1388534401,
|
|
||||||
'duration': 1887.345,
|
|
||||||
'creator': 'Originals',
|
|
||||||
'series': 'Roadkill',
|
|
||||||
'upload_date': '20140101',
|
|
||||||
'tags': [],
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.motortrend.com/plus/detail/roadworthy-rescues-teaser-trailer/4922860/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '4922860',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Roadworthy Rescues | Teaser Trailer',
|
|
||||||
'description': 'Derek Bieri helps Freiburger and Finnegan with their \'68 big-block Dart.',
|
|
||||||
'display_id': 'roadworthy-rescues-teaser-trailer/4922860',
|
|
||||||
'creator': 'Originals',
|
|
||||||
'series': 'Roadworthy Rescues',
|
|
||||||
'thumbnail': r're:^https?://.+\.jpe?g$',
|
|
||||||
'upload_date': '20220907',
|
|
||||||
'timestamp': 1662523200,
|
|
||||||
'duration': 1066.356,
|
|
||||||
'tags': [],
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.motortrend.com/plus/detail/ugly-duckling/2450033/12439',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
_PRODUCT = 'MTOD'
|
|
||||||
_DISCO_API_PARAMS = {
|
|
||||||
'disco_host': 'us1-prod-direct.motortrendondemand.com',
|
|
||||||
'realm': 'motortrend',
|
|
||||||
'country': 'us',
|
|
||||||
}
|
|
||||||
|
|
||||||
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
|
||||||
headers.update({
|
|
||||||
'x-disco-params': f'realm={realm}',
|
|
||||||
'x-disco-client': f'WEB:UNKNOWN:{self._PRODUCT}:4.39.1-gi1',
|
|
||||||
'Authorization': self._get_auth(disco_base, display_id, realm),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryPlusIE(DiscoveryPlusBaseIE):
|
class DiscoveryPlusIE(DiscoveryPlusBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.com/(?!it/)(?:\w{2}/)?video' + DPlayBaseIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.com/(?!it/)(?:(?P<country>[a-z]{2})/)?video(?:/sport)?' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.discoveryplus.com/video/property-brothers-forever-home/food-and-family',
|
'url': 'https://www.discoveryplus.com/video/property-brothers-forever-home/food-and-family',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
@ -823,14 +955,42 @@ class DiscoveryPlusIE(DiscoveryPlusBaseIE):
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://discoveryplus.com/ca/video/bering-sea-gold-discovery-ca/goldslingers',
|
'url': 'https://discoveryplus.com/ca/video/bering-sea-gold-discovery-ca/goldslingers',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.discoveryplus.com/gb/video/sport/eurosport-1-british-eurosport-1-british-sport/6-hours-of-spa-review',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_PRODUCT = 'dplus_us'
|
_PRODUCT = None
|
||||||
_DISCO_API_PARAMS = {
|
_DISCO_API_PARAMS = None
|
||||||
'disco_host': 'us1-prod-direct.discoveryplus.com',
|
|
||||||
'realm': 'go',
|
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
||||||
'country': 'us',
|
headers.update({
|
||||||
}
|
'x-disco-params': f'realm={realm},siteLookupKey={self._PRODUCT}',
|
||||||
|
'x-disco-client': f'WEB:UNKNOWN:dplus_us:{self._DISCO_CLIENT_VER}',
|
||||||
|
'Authorization': self._get_auth(disco_base, display_id, realm),
|
||||||
|
})
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id, country = self._match_valid_url(url).group('id', 'country')
|
||||||
|
if not country:
|
||||||
|
country = 'us'
|
||||||
|
|
||||||
|
self._PRODUCT = f'dplus_{country}'
|
||||||
|
|
||||||
|
if country in ('br', 'ca', 'us'):
|
||||||
|
self._DISCO_API_PARAMS = {
|
||||||
|
'disco_host': 'us1-prod-direct.discoveryplus.com',
|
||||||
|
'realm': 'go',
|
||||||
|
'country': country,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
self._DISCO_API_PARAMS = {
|
||||||
|
'disco_host': 'eu1-prod-direct.discoveryplus.com',
|
||||||
|
'realm': 'dplay',
|
||||||
|
'country': country,
|
||||||
|
}
|
||||||
|
|
||||||
|
return self._get_disco_api_info(url, video_id, **self._DISCO_API_PARAMS)
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryPlusIndiaIE(DiscoveryPlusBaseIE):
|
class DiscoveryPlusIndiaIE(DiscoveryPlusBaseIE):
|
||||||
|
@ -993,7 +1153,7 @@ class DiscoveryPlusItalyIE(DiscoveryPlusBaseIE):
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_PRODUCT = 'dplus_us'
|
_PRODUCT = 'dplus_it'
|
||||||
_DISCO_API_PARAMS = {
|
_DISCO_API_PARAMS = {
|
||||||
'disco_host': 'eu1-prod-direct.discoveryplus.com',
|
'disco_host': 'eu1-prod-direct.discoveryplus.com',
|
||||||
'realm': 'dplay',
|
'realm': 'dplay',
|
||||||
|
@ -1002,8 +1162,8 @@ class DiscoveryPlusItalyIE(DiscoveryPlusBaseIE):
|
||||||
|
|
||||||
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
||||||
headers.update({
|
headers.update({
|
||||||
'x-disco-params': f'realm={realm}',
|
'x-disco-params': f'realm={realm},siteLookupKey={self._PRODUCT}',
|
||||||
'x-disco-client': f'WEB:UNKNOWN:{self._PRODUCT}:25.2.6',
|
'x-disco-client': f'WEB:UNKNOWN:dplus_us:{self._DISCO_CLIENT_VER}',
|
||||||
'Authorization': self._get_auth(disco_base, display_id, realm),
|
'Authorization': self._get_auth(disco_base, display_id, realm),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1044,39 +1204,3 @@ class DiscoveryPlusIndiaShowIE(DiscoveryPlusShowBaseIE):
|
||||||
_SHOW_STR = 'show'
|
_SHOW_STR = 'show'
|
||||||
_INDEX = 4
|
_INDEX = 4
|
||||||
_VIDEO_IE = DiscoveryPlusIndiaIE
|
_VIDEO_IE = DiscoveryPlusIndiaIE
|
||||||
|
|
||||||
|
|
||||||
class GlobalCyclingNetworkPlusIE(DiscoveryPlusBaseIE):
|
|
||||||
_VALID_URL = r'https?://plus\.globalcyclingnetwork\.com/watch/(?P<id>\d+)'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://plus.globalcyclingnetwork.com/watch/1397691',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '1397691',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'The Athertons: Mountain Biking\'s Fastest Family',
|
|
||||||
'description': 'md5:75a81937fcd8b989eec6083a709cd837',
|
|
||||||
'thumbnail': 'https://us1-prod-images.disco-api.com/2021/03/04/eb9e3026-4849-3001-8281-9356466f0557.png',
|
|
||||||
'series': 'gcn',
|
|
||||||
'creator': 'Gcn',
|
|
||||||
'upload_date': '20210309',
|
|
||||||
'timestamp': 1615248000,
|
|
||||||
'duration': 2531.0,
|
|
||||||
'tags': [],
|
|
||||||
},
|
|
||||||
'skip': 'Subscription required',
|
|
||||||
'params': {'skip_download': 'm3u8'},
|
|
||||||
}]
|
|
||||||
|
|
||||||
_PRODUCT = 'web'
|
|
||||||
_DISCO_API_PARAMS = {
|
|
||||||
'disco_host': 'disco-api-prod.globalcyclingnetwork.com',
|
|
||||||
'realm': 'gcn',
|
|
||||||
'country': 'us',
|
|
||||||
}
|
|
||||||
|
|
||||||
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
|
||||||
headers.update({
|
|
||||||
'x-disco-params': f'realm={realm}',
|
|
||||||
'x-disco-client': f'WEB:UNKNOWN:{self._PRODUCT}:27.3.2',
|
|
||||||
'Authorization': self._get_auth(disco_base, display_id, realm),
|
|
||||||
})
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
join_nonempty,
|
||||||
orderedSet,
|
orderedSet,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
parse_qs,
|
parse_qs,
|
||||||
|
@ -13,7 +14,7 @@
|
||||||
|
|
||||||
|
|
||||||
class EpidemicSoundIE(InfoExtractor):
|
class EpidemicSoundIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?epidemicsound\.com/track/(?P<id>[0-9a-zA-Z]+)'
|
_VALID_URL = r'https?://(?:www\.)?epidemicsound\.com/(?:(?P<sfx>sound-effects/tracks)|track)/(?P<id>[0-9a-zA-Z-]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.epidemicsound.com/track/yFfQVRpSPz/',
|
'url': 'https://www.epidemicsound.com/track/yFfQVRpSPz/',
|
||||||
'md5': 'd98ff2ddb49e8acab9716541cbc9dfac',
|
'md5': 'd98ff2ddb49e8acab9716541cbc9dfac',
|
||||||
|
@ -47,6 +48,20 @@ class EpidemicSoundIE(InfoExtractor):
|
||||||
'release_timestamp': 1700535606,
|
'release_timestamp': 1700535606,
|
||||||
'release_date': '20231121',
|
'release_date': '20231121',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.epidemicsound.com/sound-effects/tracks/2f02f54b-9faa-4daf-abac-1cfe9e9cef69/',
|
||||||
|
'md5': '35d7cf05bd8b614a84f0495a05de9388',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '208931',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'upload_date': '20240603',
|
||||||
|
'timestamp': 1717436529,
|
||||||
|
'categories': ['appliance'],
|
||||||
|
'display_id': '6b2NXLURPr',
|
||||||
|
'duration': 1.0,
|
||||||
|
'title': 'Oven, Grill, Door Open 01',
|
||||||
|
'thumbnail': 'https://cdn.epidemicsound.com/curation-assets/commercial-release-cover-images/default-sfx/3000x3000.jpg',
|
||||||
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -77,8 +92,10 @@ def _epidemic_fmt_or_none(f):
|
||||||
return f
|
return f
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id, is_sfx = self._match_valid_url(url).group('id', 'sfx')
|
||||||
json_data = self._download_json(f'https://www.epidemicsound.com/json/track/{video_id}', video_id)
|
json_data = self._download_json(join_nonempty(
|
||||||
|
'https://www.epidemicsound.com/json/track',
|
||||||
|
is_sfx and 'kosmos-id', video_id, delim='/'), video_id)
|
||||||
|
|
||||||
thumbnails = traverse_obj(json_data, [('imageUrl', 'cover')])
|
thumbnails = traverse_obj(json_data, [('imageUrl', 'cover')])
|
||||||
thumb_base_url = traverse_obj(json_data, ('coverArt', 'baseUrl', {url_or_none}))
|
thumb_base_url = traverse_obj(json_data, ('coverArt', 'baseUrl', {url_or_none}))
|
||||||
|
|
|
@ -571,16 +571,21 @@ def process_formats(info):
|
||||||
# Formats larger than ~500MB will return error 403 unless chunk size is regulated
|
# Formats larger than ~500MB will return error 403 unless chunk size is regulated
|
||||||
f.setdefault('downloader_options', {})['http_chunk_size'] = 250 << 20
|
f.setdefault('downloader_options', {})['http_chunk_size'] = 250 << 20
|
||||||
|
|
||||||
def extract_relay_data(_filter):
|
def yield_all_relay_data(_filter):
|
||||||
return self._parse_json(self._search_regex(
|
for relay_data in re.findall(rf'data-sjs>({{.*?{_filter}.*?}})</script>', webpage):
|
||||||
rf'data-sjs>({{.*?{_filter}.*?}})</script>',
|
yield self._parse_json(relay_data, video_id, fatal=False) or {}
|
||||||
webpage, 'replay data', default='{}'), video_id, fatal=False) or {}
|
|
||||||
|
|
||||||
def extract_relay_prefetched_data(_filter):
|
def extract_relay_data(_filter):
|
||||||
return traverse_obj(extract_relay_data(_filter), (
|
return next(filter(None, yield_all_relay_data(_filter)), {})
|
||||||
'require', (None, (..., ..., ..., '__bbox', 'require')),
|
|
||||||
|
def extract_relay_prefetched_data(_filter, target_keys=None):
|
||||||
|
path = 'data'
|
||||||
|
if target_keys is not None:
|
||||||
|
path = lambda k, v: k == 'data' and any(target in v for target in variadic(target_keys))
|
||||||
|
return traverse_obj(yield_all_relay_data(_filter), (
|
||||||
|
..., 'require', (None, (..., ..., ..., '__bbox', 'require')),
|
||||||
lambda _, v: any(key.startswith('RelayPrefetchedStreamCache') for key in v),
|
lambda _, v: any(key.startswith('RelayPrefetchedStreamCache') for key in v),
|
||||||
..., ..., '__bbox', 'result', 'data', {dict}), get_all=False) or {}
|
..., ..., '__bbox', 'result', path, {dict}), get_all=False) or {}
|
||||||
|
|
||||||
if not video_data:
|
if not video_data:
|
||||||
server_js_data = self._parse_json(self._search_regex([
|
server_js_data = self._parse_json(self._search_regex([
|
||||||
|
@ -591,7 +596,8 @@ def extract_relay_prefetched_data(_filter):
|
||||||
|
|
||||||
if not video_data:
|
if not video_data:
|
||||||
data = extract_relay_prefetched_data(
|
data = extract_relay_prefetched_data(
|
||||||
r'"(?:dash_manifest|playable_url(?:_quality_hd)?)')
|
r'"(?:dash_manifest|playable_url(?:_quality_hd)?)',
|
||||||
|
target_keys=('video', 'event', 'nodes', 'node', 'mediaset'))
|
||||||
if data:
|
if data:
|
||||||
entries = []
|
entries = []
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
xpath_text,
|
xpath_text,
|
||||||
xpath_with_ns,
|
xpath_with_ns,
|
||||||
)
|
)
|
||||||
|
from ..utils._utils import _UnsafeExtensionError
|
||||||
|
|
||||||
|
|
||||||
class GenericIE(InfoExtractor):
|
class GenericIE(InfoExtractor):
|
||||||
|
@ -2446,9 +2447,13 @@ def _real_extract(self, url):
|
||||||
if not is_html(first_bytes):
|
if not is_html(first_bytes):
|
||||||
self.report_warning(
|
self.report_warning(
|
||||||
'URL could be a direct video link, returning it as such.')
|
'URL could be a direct video link, returning it as such.')
|
||||||
|
ext = determine_ext(url)
|
||||||
|
if ext not in _UnsafeExtensionError.ALLOWED_EXTENSIONS:
|
||||||
|
ext = 'unknown_video'
|
||||||
info_dict.update({
|
info_dict.update({
|
||||||
'direct': True,
|
'direct': True,
|
||||||
'url': url,
|
'url': url,
|
||||||
|
'ext': ext,
|
||||||
})
|
})
|
||||||
return info_dict
|
return info_dict
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
import urllib.parse
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..networking.exceptions import HTTPError
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
determine_ext,
|
determine_ext,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
join_nonempty,
|
join_nonempty,
|
||||||
|
jwt_decode_hs256,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
try_get,
|
try_get,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
|
urlencode_postdata,
|
||||||
)
|
)
|
||||||
from ..utils.traversal import traverse_obj
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
@ -276,81 +281,213 @@ def _download_video_data(self, display_id):
|
||||||
class MLBTVIE(InfoExtractor):
|
class MLBTVIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?mlb\.com/tv/g(?P<id>\d{6})'
|
_VALID_URL = r'https?://(?:www\.)?mlb\.com/tv/g(?P<id>\d{6})'
|
||||||
_NETRC_MACHINE = 'mlb'
|
_NETRC_MACHINE = 'mlb'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.mlb.com/tv/g661581/vee2eff5f-a7df-4c20-bdb4-7b926fa12638',
|
'url': 'https://www.mlb.com/tv/g661581/vee2eff5f-a7df-4c20-bdb4-7b926fa12638',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '661581',
|
'id': '661581',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '2022-07-02 - St. Louis Cardinals @ Philadelphia Phillies',
|
'title': '2022-07-02 - St. Louis Cardinals @ Philadelphia Phillies',
|
||||||
|
'release_date': '20220702',
|
||||||
|
'release_timestamp': 1656792300,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
_GRAPHQL_INIT_QUERY = '''\
|
||||||
|
mutation initSession($device: InitSessionInput!, $clientType: ClientType!, $experience: ExperienceTypeInput) {
|
||||||
|
initSession(device: $device, clientType: $clientType, experience: $experience) {
|
||||||
|
deviceId
|
||||||
|
sessionId
|
||||||
|
entitlements {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
location {
|
||||||
|
countryCode
|
||||||
|
regionName
|
||||||
|
zipCode
|
||||||
|
latitude
|
||||||
|
longitude
|
||||||
|
}
|
||||||
|
clientExperience
|
||||||
|
features
|
||||||
|
}
|
||||||
|
}'''
|
||||||
|
_GRAPHQL_PLAYBACK_QUERY = '''\
|
||||||
|
mutation initPlaybackSession(
|
||||||
|
$adCapabilities: [AdExperienceType]
|
||||||
|
$mediaId: String!
|
||||||
|
$deviceId: String!
|
||||||
|
$sessionId: String!
|
||||||
|
$quality: PlaybackQuality
|
||||||
|
) {
|
||||||
|
initPlaybackSession(
|
||||||
|
adCapabilities: $adCapabilities
|
||||||
|
mediaId: $mediaId
|
||||||
|
deviceId: $deviceId
|
||||||
|
sessionId: $sessionId
|
||||||
|
quality: $quality
|
||||||
|
) {
|
||||||
|
playbackSessionId
|
||||||
|
playback {
|
||||||
|
url
|
||||||
|
token
|
||||||
|
expiration
|
||||||
|
cdn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'''
|
||||||
|
_APP_VERSION = '7.8.2'
|
||||||
|
_device_id = None
|
||||||
|
_session_id = None
|
||||||
_access_token = None
|
_access_token = None
|
||||||
|
_token_expiry = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _api_headers(self):
|
||||||
|
if (self._token_expiry - 120) <= time.time():
|
||||||
|
self.write_debug('Access token has expired; re-logging in')
|
||||||
|
self._perform_login(*self._get_login_info())
|
||||||
|
return {'Authorization': f'Bearer {self._access_token}'}
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
if not self._access_token:
|
if not self._access_token:
|
||||||
self.raise_login_required(
|
self.raise_login_required(
|
||||||
'All videos are only available to registered users', method='password')
|
'All videos are only available to registered users', method='password')
|
||||||
|
|
||||||
|
def _set_device_id(self, username):
|
||||||
|
if not self._device_id:
|
||||||
|
self._device_id = self.cache.load(
|
||||||
|
self._NETRC_MACHINE, 'device_ids', default={}).get(username)
|
||||||
|
if self._device_id:
|
||||||
|
return
|
||||||
|
self._device_id = str(uuid.uuid4())
|
||||||
|
self.cache.store(self._NETRC_MACHINE, 'device_ids', {username: self._device_id})
|
||||||
|
|
||||||
def _perform_login(self, username, password):
|
def _perform_login(self, username, password):
|
||||||
data = f'grant_type=password&username={urllib.parse.quote(username)}&password={urllib.parse.quote(password)}&scope=openid offline_access&client_id=0oa3e1nutA1HLzAKG356'
|
try:
|
||||||
access_token = self._download_json(
|
self._access_token = self._download_json(
|
||||||
'https://ids.mlb.com/oauth2/aus1m088yK07noBfh356/v1/token', None,
|
'https://ids.mlb.com/oauth2/aus1m088yK07noBfh356/v1/token', None,
|
||||||
headers={
|
'Logging in', 'Unable to log in', headers={
|
||||||
'User-Agent': 'okhttp/3.12.1',
|
'User-Agent': 'okhttp/3.12.1',
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
}, data=data.encode())['access_token']
|
}, data=urlencode_postdata({
|
||||||
|
'grant_type': 'password',
|
||||||
|
'username': username,
|
||||||
|
'password': password,
|
||||||
|
'scope': 'openid offline_access',
|
||||||
|
'client_id': '0oa3e1nutA1HLzAKG356',
|
||||||
|
}))['access_token']
|
||||||
|
except ExtractorError as error:
|
||||||
|
if isinstance(error.cause, HTTPError) and error.cause.status == 400:
|
||||||
|
raise ExtractorError('Invalid username or password', expected=True)
|
||||||
|
raise
|
||||||
|
|
||||||
entitlement = self._download_webpage(
|
self._token_expiry = traverse_obj(self._access_token, ({jwt_decode_hs256}, 'exp', {int})) or 0
|
||||||
f'https://media-entitlement.mlb.com/api/v3/jwt?os=Android&appname=AtBat&did={uuid.uuid4()}', None,
|
self._set_device_id(username)
|
||||||
headers={
|
|
||||||
'User-Agent': 'okhttp/3.12.1',
|
|
||||||
'Authorization': f'Bearer {access_token}',
|
|
||||||
})
|
|
||||||
|
|
||||||
data = f'grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token={entitlement}&subject_token_type=urn:ietf:params:oauth:token-type:jwt&platform=android-tv'
|
self._session_id = self._call_api({
|
||||||
self._access_token = self._download_json(
|
'operationName': 'initSession',
|
||||||
'https://us.edge.bamgrid.com/token', None,
|
'query': self._GRAPHQL_INIT_QUERY,
|
||||||
|
'variables': {
|
||||||
|
'device': {
|
||||||
|
'appVersion': self._APP_VERSION,
|
||||||
|
'deviceFamily': 'desktop',
|
||||||
|
'knownDeviceId': self._device_id,
|
||||||
|
'languagePreference': 'ENGLISH',
|
||||||
|
'manufacturer': '',
|
||||||
|
'model': '',
|
||||||
|
'os': '',
|
||||||
|
'osVersion': '',
|
||||||
|
},
|
||||||
|
'clientType': 'WEB',
|
||||||
|
},
|
||||||
|
}, None, 'session ID')['data']['initSession']['sessionId']
|
||||||
|
|
||||||
|
def _call_api(self, data, video_id, description='GraphQL JSON', fatal=True):
|
||||||
|
return self._download_json(
|
||||||
|
'https://media-gateway.mlb.com/graphql', video_id,
|
||||||
|
f'Downloading {description}', f'Unable to download {description}', fatal=fatal,
|
||||||
headers={
|
headers={
|
||||||
|
**self._api_headers,
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Authorization': 'Bearer bWxidHYmYW5kcm9pZCYxLjAuMA.6LZMbH2r--rbXcgEabaDdIslpo4RyZrlVfWZhsAgXIk',
|
'Content-Type': 'application/json',
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'x-client-name': 'WEB',
|
||||||
}, data=data.encode())['access_token']
|
'x-client-version': self._APP_VERSION,
|
||||||
|
}, data=json.dumps(data, separators=(',', ':')).encode())
|
||||||
|
|
||||||
|
def _extract_formats_and_subtitles(self, broadcast, video_id):
|
||||||
|
feed = traverse_obj(broadcast, ('homeAway', {str.title}))
|
||||||
|
medium = traverse_obj(broadcast, ('type', {str}))
|
||||||
|
language = traverse_obj(broadcast, ('language', {str.lower}))
|
||||||
|
format_id = join_nonempty(feed, medium, language)
|
||||||
|
|
||||||
|
response = self._call_api({
|
||||||
|
'operationName': 'initPlaybackSession',
|
||||||
|
'query': self._GRAPHQL_PLAYBACK_QUERY,
|
||||||
|
'variables': {
|
||||||
|
'adCapabilities': ['GOOGLE_STANDALONE_AD_PODS'],
|
||||||
|
'deviceId': self._device_id,
|
||||||
|
'mediaId': broadcast['mediaId'],
|
||||||
|
'quality': 'PLACEHOLDER',
|
||||||
|
'sessionId': self._session_id,
|
||||||
|
},
|
||||||
|
}, video_id, f'{format_id} broadcast JSON', fatal=False)
|
||||||
|
|
||||||
|
playback = traverse_obj(response, ('data', 'initPlaybackSession', 'playback', {dict}))
|
||||||
|
m3u8_url = traverse_obj(playback, ('url', {url_or_none}))
|
||||||
|
token = traverse_obj(playback, ('token', {str}))
|
||||||
|
|
||||||
|
if not (m3u8_url and token):
|
||||||
|
errors = '; '.join(traverse_obj(response, ('errors', ..., 'message', {str})))
|
||||||
|
if 'not entitled' in errors:
|
||||||
|
raise ExtractorError(errors, expected=True)
|
||||||
|
elif errors: # Only warn when 'blacked out' since radio formats are available
|
||||||
|
self.report_warning(f'API returned errors for {format_id}: {errors}')
|
||||||
|
else:
|
||||||
|
self.report_warning(f'No formats available for {format_id} broadcast; skipping')
|
||||||
|
return [], {}
|
||||||
|
|
||||||
|
cdn_headers = {'x-cdn-token': token}
|
||||||
|
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
||||||
|
m3u8_url.replace(f'/{token}/', '/'), video_id, 'mp4',
|
||||||
|
m3u8_id=format_id, fatal=False, headers=cdn_headers)
|
||||||
|
for fmt in fmts:
|
||||||
|
fmt['http_headers'] = cdn_headers
|
||||||
|
fmt.setdefault('format_note', join_nonempty(feed, medium, delim=' '))
|
||||||
|
fmt.setdefault('language', language)
|
||||||
|
if fmt.get('vcodec') == 'none' and fmt['language'] == 'en':
|
||||||
|
fmt['source_preference'] = 10
|
||||||
|
|
||||||
|
return fmts, subs
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
airings = self._download_json(
|
metadata = traverse_obj(self._download_json(
|
||||||
f'https://search-api-mlbtv.mlb.com/svc/search/v2/graphql/persisted/query/core/Airings?variables=%7B%22partnerProgramIds%22%3A%5B%22{video_id}%22%5D%2C%22applyEsniMediaRightsLabels%22%3Atrue%7D',
|
'https://statsapi.mlb.com/api/v1/schedule', video_id, query={
|
||||||
video_id)['data']['Airings']
|
'gamePk': video_id,
|
||||||
|
'hydrate': 'broadcasts(all),statusFlags',
|
||||||
|
}), ('dates', ..., 'games', lambda _, v: str(v['gamePk']) == video_id and v['broadcasts'], any))
|
||||||
|
|
||||||
|
broadcasts = traverse_obj(metadata, (
|
||||||
|
'broadcasts', lambda _, v: v['mediaId'] and v['mediaState']['mediaStateCode'] != 'MEDIA_OFF'))
|
||||||
|
|
||||||
formats, subtitles = [], {}
|
formats, subtitles = [], {}
|
||||||
for airing in traverse_obj(airings, lambda _, v: v['playbackUrls'][0]['href']):
|
for broadcast in broadcasts:
|
||||||
format_id = join_nonempty('feedType', 'feedLanguage', from_dict=airing)
|
fmts, subs = self._extract_formats_and_subtitles(broadcast, video_id)
|
||||||
m3u8_url = traverse_obj(self._download_json(
|
formats.extend(fmts)
|
||||||
airing['playbackUrls'][0]['href'].format(scenario='browser~csai'), video_id,
|
self._merge_subtitles(subs, target=subtitles)
|
||||||
note=f'Downloading {format_id} stream info JSON',
|
|
||||||
errnote=f'Failed to download {format_id} stream info, skipping',
|
|
||||||
fatal=False, headers={
|
|
||||||
'Authorization': self._access_token,
|
|
||||||
'Accept': 'application/vnd.media-service+json; version=2',
|
|
||||||
}), ('stream', 'complete', {url_or_none}))
|
|
||||||
if not m3u8_url:
|
|
||||||
continue
|
|
||||||
f, s = self._extract_m3u8_formats_and_subtitles(
|
|
||||||
m3u8_url, video_id, 'mp4', m3u8_id=format_id, fatal=False)
|
|
||||||
formats.extend(f)
|
|
||||||
self._merge_subtitles(s, target=subtitles)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': traverse_obj(airings, (..., 'titles', 0, 'episodeName'), get_all=False),
|
'title': join_nonempty(
|
||||||
'is_live': traverse_obj(airings, (..., 'mediaConfig', 'productType'), get_all=False) == 'LIVE',
|
traverse_obj(metadata, ('officialDate', {str})),
|
||||||
|
traverse_obj(metadata, ('teams', ('away', 'home'), 'team', 'name', {str}, all, {' @ '.join})),
|
||||||
|
delim=' - '),
|
||||||
|
'is_live': traverse_obj(broadcasts, (..., 'mediaState', 'mediaStateCode', {str}, any)) == 'MEDIA_ON',
|
||||||
|
'release_timestamp': traverse_obj(metadata, ('gameDate', {parse_iso8601})),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
'http_headers': {'Authorization': f'Bearer {self._access_token}'},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -871,7 +871,7 @@ class SoundcloudUserPermalinkIE(SoundcloudPagedPlaylistBaseIE):
|
||||||
'id': '30909869',
|
'id': '30909869',
|
||||||
'title': 'neilcic',
|
'title': 'neilcic',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 23,
|
'playlist_mincount': 22,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@ -880,7 +880,7 @@ def _real_extract(self, url):
|
||||||
self._resolv_url(url), user_id, 'Downloading user info', headers=self._HEADERS)
|
self._resolv_url(url), user_id, 'Downloading user info', headers=self._HEADERS)
|
||||||
|
|
||||||
return self._extract_playlist(
|
return self._extract_playlist(
|
||||||
f'{self._API_V2_BASE}stream/users/{user["id"]}', str(user['id']), user.get('username'))
|
f'{self._API_V2_BASE}users/{user["id"]}/tracks', str(user['id']), user.get('username'))
|
||||||
|
|
||||||
|
|
||||||
class SoundcloudTrackStationIE(SoundcloudPagedPlaylistBaseIE):
|
class SoundcloudTrackStationIE(SoundcloudPagedPlaylistBaseIE):
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
parse_qs,
|
parse_qs,
|
||||||
qualities,
|
qualities,
|
||||||
remove_start,
|
|
||||||
srt_subtitles_timecode,
|
srt_subtitles_timecode,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
|
@ -254,7 +253,16 @@ def _extract_web_data_and_status(self, url, video_id, fatal=True):
|
||||||
|
|
||||||
def _get_subtitles(self, aweme_detail, aweme_id, user_name):
|
def _get_subtitles(self, aweme_detail, aweme_id, user_name):
|
||||||
# TODO: Extract text positioning info
|
# TODO: Extract text positioning info
|
||||||
|
|
||||||
|
EXT_MAP = { # From lowest to highest preference
|
||||||
|
'creator_caption': 'json',
|
||||||
|
'srt': 'srt',
|
||||||
|
'webvtt': 'vtt',
|
||||||
|
}
|
||||||
|
preference = qualities(tuple(EXT_MAP.values()))
|
||||||
|
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
|
|
||||||
# aweme/detail endpoint subs
|
# aweme/detail endpoint subs
|
||||||
captions_info = traverse_obj(
|
captions_info = traverse_obj(
|
||||||
aweme_detail, ('interaction_stickers', ..., 'auto_video_caption_info', 'auto_captions', ...), expected_type=dict)
|
aweme_detail, ('interaction_stickers', ..., 'auto_video_caption_info', 'auto_captions', ...), expected_type=dict)
|
||||||
|
@ -278,8 +286,8 @@ def _get_subtitles(self, aweme_detail, aweme_id, user_name):
|
||||||
if not caption.get('url'):
|
if not caption.get('url'):
|
||||||
continue
|
continue
|
||||||
subtitles.setdefault(caption.get('lang') or 'en', []).append({
|
subtitles.setdefault(caption.get('lang') or 'en', []).append({
|
||||||
'ext': remove_start(caption.get('caption_format'), 'web'),
|
|
||||||
'url': caption['url'],
|
'url': caption['url'],
|
||||||
|
'ext': EXT_MAP.get(caption.get('Format')),
|
||||||
})
|
})
|
||||||
# webpage subs
|
# webpage subs
|
||||||
if not subtitles:
|
if not subtitles:
|
||||||
|
@ -288,9 +296,14 @@ def _get_subtitles(self, aweme_detail, aweme_id, user_name):
|
||||||
self._create_url(user_name, aweme_id), aweme_id, fatal=False)
|
self._create_url(user_name, aweme_id), aweme_id, fatal=False)
|
||||||
for caption in traverse_obj(aweme_detail, ('video', 'subtitleInfos', lambda _, v: v['Url'])):
|
for caption in traverse_obj(aweme_detail, ('video', 'subtitleInfos', lambda _, v: v['Url'])):
|
||||||
subtitles.setdefault(caption.get('LanguageCodeName') or 'en', []).append({
|
subtitles.setdefault(caption.get('LanguageCodeName') or 'en', []).append({
|
||||||
'ext': remove_start(caption.get('Format'), 'web'),
|
|
||||||
'url': caption['Url'],
|
'url': caption['Url'],
|
||||||
|
'ext': EXT_MAP.get(caption.get('Format')),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Deprioritize creator_caption json since it can't be embedded or used by media players
|
||||||
|
for lang, subs_list in subtitles.items():
|
||||||
|
subtitles[lang] = sorted(subs_list, key=lambda x: preference(x['ext']))
|
||||||
|
|
||||||
return subtitles
|
return subtitles
|
||||||
|
|
||||||
def _parse_url_key(self, url_key):
|
def _parse_url_key(self, url_key):
|
||||||
|
|
|
@ -212,16 +212,6 @@ def _parse_config(self, config, video_id):
|
||||||
owner = video_data.get('owner') or {}
|
owner = video_data.get('owner') or {}
|
||||||
video_uploader_url = owner.get('url')
|
video_uploader_url = owner.get('url')
|
||||||
|
|
||||||
duration = int_or_none(video_data.get('duration'))
|
|
||||||
chapter_data = try_get(config, lambda x: x['embed']['chapters']) or []
|
|
||||||
chapters = [{
|
|
||||||
'title': current_chapter.get('title'),
|
|
||||||
'start_time': current_chapter.get('timecode'),
|
|
||||||
'end_time': next_chapter.get('timecode'),
|
|
||||||
} for current_chapter, next_chapter in zip(chapter_data, chapter_data[1:] + [{'timecode': duration}])]
|
|
||||||
if chapters and chapters[0]['start_time']: # Chapters may not start from 0
|
|
||||||
chapters[:0] = [{'title': '<Untitled>', 'start_time': 0, 'end_time': chapters[0]['start_time']}]
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': str_or_none(video_data.get('id')) or video_id,
|
'id': str_or_none(video_data.get('id')) or video_id,
|
||||||
'title': video_title,
|
'title': video_title,
|
||||||
|
@ -229,8 +219,12 @@ def _parse_config(self, config, video_id):
|
||||||
'uploader_id': video_uploader_url.split('/')[-1] if video_uploader_url else None,
|
'uploader_id': video_uploader_url.split('/')[-1] if video_uploader_url else None,
|
||||||
'uploader_url': video_uploader_url,
|
'uploader_url': video_uploader_url,
|
||||||
'thumbnails': thumbnails,
|
'thumbnails': thumbnails,
|
||||||
'duration': duration,
|
'duration': int_or_none(video_data.get('duration')),
|
||||||
'chapters': chapters or None,
|
'chapters': sorted(traverse_obj(config, (
|
||||||
|
'embed', 'chapters', lambda _, v: int(v['timecode']) is not None, {
|
||||||
|
'title': ('title', {str}),
|
||||||
|
'start_time': ('timecode', {int_or_none}),
|
||||||
|
})), key=lambda c: c['start_time']) or None,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
'live_status': live_status,
|
'live_status': live_status,
|
||||||
|
@ -708,6 +702,39 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
# chapters must be sorted, see: https://github.com/yt-dlp/yt-dlp/issues/5308
|
||||||
|
'url': 'https://player.vimeo.com/video/756714419',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '756714419',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Dr Arielle Schwartz - Therapeutic yoga for optimum sleep',
|
||||||
|
'uploader': 'Alex Howard',
|
||||||
|
'uploader_id': 'user54729178',
|
||||||
|
'uploader_url': 'https://vimeo.com/user54729178',
|
||||||
|
'thumbnail': r're:https://i\.vimeocdn\.com/video/1520099929-[\da-f]+-d_1280',
|
||||||
|
'duration': 2636,
|
||||||
|
'chapters': [
|
||||||
|
{'start_time': 0, 'end_time': 10, 'title': '<Untitled Chapter 1>'},
|
||||||
|
{'start_time': 10, 'end_time': 106, 'title': 'Welcoming Dr Arielle Schwartz'},
|
||||||
|
{'start_time': 106, 'end_time': 305, 'title': 'What is therapeutic yoga?'},
|
||||||
|
{'start_time': 305, 'end_time': 594, 'title': 'Vagal toning practices'},
|
||||||
|
{'start_time': 594, 'end_time': 888, 'title': 'Trauma and difficulty letting go'},
|
||||||
|
{'start_time': 888, 'end_time': 1059, 'title': "Dr Schwartz' insomnia experience"},
|
||||||
|
{'start_time': 1059, 'end_time': 1471, 'title': 'A strategy for helping sleep issues'},
|
||||||
|
{'start_time': 1471, 'end_time': 1667, 'title': 'Yoga nidra'},
|
||||||
|
{'start_time': 1667, 'end_time': 2121, 'title': 'Wisdom in stillness'},
|
||||||
|
{'start_time': 2121, 'end_time': 2386, 'title': 'What helps us be more able to let go?'},
|
||||||
|
{'start_time': 2386, 'end_time': 2510, 'title': 'Practical tips to help ourselves'},
|
||||||
|
{'start_time': 2510, 'end_time': 2636, 'title': 'Where to find out more'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'http_headers': {'Referer': 'https://sleepsuperconference.com'},
|
||||||
|
'skip_download': 'm3u8',
|
||||||
|
},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
# user playlist alias -> https://vimeo.com/258705797
|
# user playlist alias -> https://vimeo.com/258705797
|
||||||
'url': 'https://vimeo.com/user26785108/newspiritualguide',
|
'url': 'https://vimeo.com/user26785108/newspiritualguide',
|
||||||
|
|
|
@ -270,7 +270,7 @@ def build_innertube_clients():
|
||||||
THIRD_PARTY = {
|
THIRD_PARTY = {
|
||||||
'embedUrl': 'https://www.youtube.com/', # Can be any valid URL
|
'embedUrl': 'https://www.youtube.com/', # Can be any valid URL
|
||||||
}
|
}
|
||||||
BASE_CLIENTS = ('ios', 'android', 'web', 'tv', 'mweb')
|
BASE_CLIENTS = ('ios', 'web', 'tv', 'mweb', 'android')
|
||||||
priority = qualities(BASE_CLIENTS[::-1])
|
priority = qualities(BASE_CLIENTS[::-1])
|
||||||
|
|
||||||
for client, ytcfg in tuple(INNERTUBE_CLIENTS.items()):
|
for client, ytcfg in tuple(INNERTUBE_CLIENTS.items()):
|
||||||
|
@ -3131,7 +3131,14 @@ def _decrypt_nsig(self, s, video_id, player_url):
|
||||||
|
|
||||||
def _extract_n_function_name(self, jscode):
|
def _extract_n_function_name(self, jscode):
|
||||||
funcname, idx = self._search_regex(
|
funcname, idx = self._search_regex(
|
||||||
r'''(?x)(?:\.get\("n"\)\)&&\(b=|b=String\.fromCharCode\(110\),c=a\.get\(b\)\)&&\(c=)
|
r'''(?x)
|
||||||
|
(?:
|
||||||
|
\.get\("n"\)\)&&\(b=|
|
||||||
|
(?:
|
||||||
|
b=String\.fromCharCode\(110\)|
|
||||||
|
([a-zA-Z0-9$.]+)&&\(b="nn"\[\+\1\]
|
||||||
|
),c=a\.get\(b\)\)&&\(c=
|
||||||
|
)
|
||||||
(?P<nfunc>[a-zA-Z0-9$]+)(?:\[(?P<idx>\d+)\])?\([a-zA-Z0-9]\)''',
|
(?P<nfunc>[a-zA-Z0-9$]+)(?:\[(?P<idx>\d+)\])?\([a-zA-Z0-9]\)''',
|
||||||
jscode, 'Initial JS player n function name', group=('nfunc', 'idx'))
|
jscode, 'Initial JS player n function name', group=('nfunc', 'idx'))
|
||||||
if not idx:
|
if not idx:
|
||||||
|
@ -3702,17 +3709,17 @@ def _invalid_player_response(self, pr, video_id):
|
||||||
return pr_id
|
return pr_id
|
||||||
|
|
||||||
def _extract_player_responses(self, clients, video_id, webpage, master_ytcfg, smuggled_data):
|
def _extract_player_responses(self, clients, video_id, webpage, master_ytcfg, smuggled_data):
|
||||||
initial_pr = None
|
initial_pr = ignore_initial_response = None
|
||||||
if webpage:
|
if webpage:
|
||||||
experiments = traverse_obj(master_ytcfg, (
|
if 'web' in clients:
|
||||||
'WEB_PLAYER_CONTEXT_CONFIGS', ..., 'serializedExperimentIds', {str}, {lambda x: x.split(',')}, ..., {str}))
|
experiments = traverse_obj(master_ytcfg, (
|
||||||
if all(x in experiments for x in self._POTOKEN_EXPERIMENTS):
|
'WEB_PLAYER_CONTEXT_CONFIGS', ..., 'serializedExperimentIds', {lambda x: x.split(',')}, ...))
|
||||||
self.report_warning(
|
if all(x in experiments for x in self._POTOKEN_EXPERIMENTS):
|
||||||
'Webpage contains broken formats (poToken experiment detected). Ignoring initial player response')
|
self.report_warning(
|
||||||
master_ytcfg = self._get_default_ytcfg()
|
'Webpage contains broken formats (poToken experiment detected). Ignoring initial player response')
|
||||||
else:
|
ignore_initial_response = True
|
||||||
initial_pr = self._search_json(
|
initial_pr = self._search_json(
|
||||||
self._YT_INITIAL_PLAYER_RESPONSE_RE, webpage, 'initial player response', video_id, fatal=False)
|
self._YT_INITIAL_PLAYER_RESPONSE_RE, webpage, 'initial player response', video_id, fatal=False)
|
||||||
|
|
||||||
prs = []
|
prs = []
|
||||||
if initial_pr and not self._invalid_player_response(initial_pr, video_id):
|
if initial_pr and not self._invalid_player_response(initial_pr, video_id):
|
||||||
|
@ -3740,8 +3747,10 @@ def append_client(*client_names):
|
||||||
skipped_clients = {}
|
skipped_clients = {}
|
||||||
while clients:
|
while clients:
|
||||||
client, base_client, variant = _split_innertube_client(clients.pop())
|
client, base_client, variant = _split_innertube_client(clients.pop())
|
||||||
player_ytcfg = master_ytcfg if client == 'web' else {}
|
player_ytcfg = {}
|
||||||
if 'configs' not in self._configuration_arg('player_skip') and client != 'web':
|
if client == 'web':
|
||||||
|
player_ytcfg = self._get_default_ytcfg() if ignore_initial_response else master_ytcfg
|
||||||
|
elif 'configs' not in self._configuration_arg('player_skip'):
|
||||||
player_ytcfg = self._download_ytcfg(client, video_id) or player_ytcfg
|
player_ytcfg = self._download_ytcfg(client, video_id) or player_ytcfg
|
||||||
|
|
||||||
player_url = player_url or self._extract_player_url(master_ytcfg, player_ytcfg, webpage=webpage)
|
player_url = player_url or self._extract_player_url(master_ytcfg, player_ytcfg, webpage=webpage)
|
||||||
|
@ -3754,7 +3763,7 @@ def append_client(*client_names):
|
||||||
player_url = self._download_player_url(video_id)
|
player_url = self._download_player_url(video_id)
|
||||||
tried_iframe_fallback = True
|
tried_iframe_fallback = True
|
||||||
|
|
||||||
pr = initial_pr if client == 'web' and initial_pr else None
|
pr = initial_pr if client == 'web' and not ignore_initial_response else None
|
||||||
for retry in self.RetryManager(fatal=False):
|
for retry in self.RetryManager(fatal=False):
|
||||||
try:
|
try:
|
||||||
pr = pr or self._extract_player_response(
|
pr = pr or self._extract_player_response(
|
||||||
|
@ -3765,7 +3774,7 @@ def append_client(*client_names):
|
||||||
break
|
break
|
||||||
experiments = traverse_obj(pr, (
|
experiments = traverse_obj(pr, (
|
||||||
'responseContext', 'serviceTrackingParams', lambda _, v: v['service'] == 'GFEEDBACK',
|
'responseContext', 'serviceTrackingParams', lambda _, v: v['service'] == 'GFEEDBACK',
|
||||||
'params', lambda _, v: v['key'] == 'e', 'value', {lambda x: x.split(',')}, ..., {str}))
|
'params', lambda _, v: v['key'] == 'e', 'value', {lambda x: x.split(',')}, ...))
|
||||||
if all(x in experiments for x in self._POTOKEN_EXPERIMENTS):
|
if all(x in experiments for x in self._POTOKEN_EXPERIMENTS):
|
||||||
pr = None
|
pr = None
|
||||||
retry.error = ExtractorError('API returned broken formats (poToken experiment detected)', expected=True)
|
retry.error = ExtractorError('API returned broken formats (poToken experiment detected)', expected=True)
|
||||||
|
|
|
@ -462,6 +462,7 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
'the STREAM (stdout or stderr) to apply the setting to. '
|
'the STREAM (stdout or stderr) to apply the setting to. '
|
||||||
'Can be one of "always", "auto" (default), "never", or '
|
'Can be one of "always", "auto" (default), "never", or '
|
||||||
'"no_color" (use non color terminal sequences). '
|
'"no_color" (use non color terminal sequences). '
|
||||||
|
'Use "auto-tty" or "no_color-tty" to decide based on terminal support only. '
|
||||||
'Can be used multiple times'))
|
'Can be used multiple times'))
|
||||||
general.add_option(
|
general.add_option(
|
||||||
'--compat-options',
|
'--compat-options',
|
||||||
|
|
|
@ -310,6 +310,7 @@ def _download_update_spec(self, source_tags):
|
||||||
if isinstance(error, HTTPError) and error.status == 404:
|
if isinstance(error, HTTPError) and error.status == 404:
|
||||||
continue
|
continue
|
||||||
self._report_network_error(f'fetch update spec: {error}')
|
self._report_network_error(f'fetch update spec: {error}')
|
||||||
|
return None
|
||||||
|
|
||||||
self._report_error(
|
self._report_error(
|
||||||
f'The requested tag {self.requested_tag} does not exist for {self.requested_repo}', True)
|
f'The requested tag {self.requested_tag} does not exist for {self.requested_repo}', True)
|
||||||
|
@ -557,9 +558,10 @@ def _report_permission_error(self, file):
|
||||||
def _report_network_error(self, action, delim=';', tag=None):
|
def _report_network_error(self, action, delim=';', tag=None):
|
||||||
if not tag:
|
if not tag:
|
||||||
tag = self.requested_tag
|
tag = self.requested_tag
|
||||||
|
path = tag if tag == 'latest' else f'tag/{tag}'
|
||||||
self._report_error(
|
self._report_error(
|
||||||
f'Unable to {action}{delim} visit https://github.com/{self.requested_repo}/releases/'
|
f'Unable to {action}{delim} visit '
|
||||||
+ tag if tag == 'latest' else f'tag/{tag}', True)
|
f'https://github.com/{self.requested_repo}/releases/{path}', True)
|
||||||
|
|
||||||
# XXX: Everything below this line in this class is deprecated / for compat only
|
# XXX: Everything below this line in this class is deprecated / for compat only
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -2984,6 +2984,7 @@ def parse_codecs(codecs_str):
|
||||||
str.strip, codecs_str.strip().strip(',').split(','))))
|
str.strip, codecs_str.strip().strip(',').split(','))))
|
||||||
vcodec, acodec, scodec, hdr = None, None, None, None
|
vcodec, acodec, scodec, hdr = None, None, None, None
|
||||||
for full_codec in split_codecs:
|
for full_codec in split_codecs:
|
||||||
|
full_codec = re.sub(r'^([^.]+)', lambda m: m.group(1).lower(), full_codec)
|
||||||
parts = re.sub(r'0+(?=\d)', '', full_codec).split('.')
|
parts = re.sub(r'0+(?=\d)', '', full_codec).split('.')
|
||||||
if parts[0] in ('avc1', 'avc2', 'avc3', 'avc4', 'vp9', 'vp8', 'hev1', 'hev2',
|
if parts[0] in ('avc1', 'avc2', 'avc3', 'avc4', 'vp9', 'vp8', 'hev1', 'hev2',
|
||||||
'h263', 'h264', 'mp4v', 'hvc1', 'av1', 'theora', 'dvh1', 'dvhe'):
|
'h263', 'h264', 'mp4v', 'hvc1', 'av1', 'theora', 'dvh1', 'dvhe'):
|
||||||
|
@ -5110,6 +5111,7 @@ class _UnsafeExtensionError(Exception):
|
||||||
'gif',
|
'gif',
|
||||||
'heic',
|
'heic',
|
||||||
'ico',
|
'ico',
|
||||||
|
'image',
|
||||||
'jng',
|
'jng',
|
||||||
'jpeg',
|
'jpeg',
|
||||||
'jxl',
|
'jxl',
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# Autogenerated by devscripts/update-version.py
|
# Autogenerated by devscripts/update-version.py
|
||||||
|
|
||||||
__version__ = '2024.07.09'
|
__version__ = '2024.07.25'
|
||||||
|
|
||||||
RELEASE_GIT_HEAD = '7ead7332af69422cee931aec3faa277288e9e212'
|
RELEASE_GIT_HEAD = 'f0993391e6052ec8f7aacc286609564f226943b9'
|
||||||
|
|
||||||
VARIANT = None
|
VARIANT = None
|
||||||
|
|
||||||
|
@ -12,4 +12,4 @@
|
||||||
|
|
||||||
ORIGIN = 'yt-dlp/yt-dlp'
|
ORIGIN = 'yt-dlp/yt-dlp'
|
||||||
|
|
||||||
_pkg_version = '2024.07.09'
|
_pkg_version = '2024.07.25'
|
||||||
|
|
Loading…
Reference in a new issue