Compare commits
25 Commits
2024.12.03
...
2024.12.23
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3905f64920 | ||
|
|
65cf46cddd | ||
|
|
9f42e68a74 | ||
|
|
6fc85f617a | ||
|
|
d298693b1b | ||
|
|
09a6c68712 | ||
|
|
1a8851b689 | ||
|
|
b91c3925c2 | ||
|
|
3d3ee458c1 | ||
|
|
2037a6414f | ||
|
|
5421669626 | ||
|
|
dc3c4fddcc | ||
|
|
5460cd9189 | ||
|
|
f6c73aad5f | ||
|
|
d5e2a379f2 | ||
|
|
bc262bcad4 | ||
|
|
f4d3e9e6dc | ||
|
|
6fef824025 | ||
|
|
4bd2655398 | ||
|
|
a95ee6d880 | ||
|
|
4c85ccd136 | ||
|
|
2feb28028e | ||
|
|
fca3eb5f8b | ||
|
|
2e49c789d3 | ||
|
|
354cb4026c |
@@ -710,3 +710,6 @@ subrat-lima
|
|||||||
gitninja1234
|
gitninja1234
|
||||||
jkruse
|
jkruse
|
||||||
xiaomac
|
xiaomac
|
||||||
|
wesson09
|
||||||
|
Crypto90
|
||||||
|
MutantPiggieGolem1
|
||||||
|
|||||||
43
Changelog.md
43
Changelog.md
@@ -4,6 +4,49 @@
|
|||||||
# 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.12.23
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- [Don't sanitize filename on Unix when `--no-windows-filenames`](https://github.com/yt-dlp/yt-dlp/commit/6fc85f617a5850307fd5b258477070e6ee177796) ([#9591](https://github.com/yt-dlp/yt-dlp/issues/9591)) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- **update**
|
||||||
|
- [Check 64-bitness when upgrading ARM builds](https://github.com/yt-dlp/yt-dlp/commit/b91c3925c2059970daa801cb131c0c2f4f302e72) ([#11819](https://github.com/yt-dlp/yt-dlp/issues/11819)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Fix endless update loop for `linux_exe` builds](https://github.com/yt-dlp/yt-dlp/commit/3d3ee458c1fe49dd5ebd7651a092119d23eb7000) ([#11827](https://github.com/yt-dlp/yt-dlp/issues/11827)) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **soundcloud**: [Various fixes](https://github.com/yt-dlp/yt-dlp/commit/d298693b1b266d198e8eeecb90ea17c4a031268f) ([#11820](https://github.com/yt-dlp/yt-dlp/issues/11820)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **youtube**
|
||||||
|
- [Add age-gate workaround for some embeddable videos](https://github.com/yt-dlp/yt-dlp/commit/09a6c687126f04e243fcb105a828787efddd1030) ([#11821](https://github.com/yt-dlp/yt-dlp/issues/11821)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Fix `uploader_id` extraction](https://github.com/yt-dlp/yt-dlp/commit/1a8851b689763e5173b96f70f8a71df0e4a44b66) ([#11818](https://github.com/yt-dlp/yt-dlp/issues/11818)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Player client maintenance](https://github.com/yt-dlp/yt-dlp/commit/65cf46cddd873fd229dbb0fc0689bca4c201c6b6) ([#11893](https://github.com/yt-dlp/yt-dlp/issues/11893)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Skip iOS formats that require PO Token](https://github.com/yt-dlp/yt-dlp/commit/9f42e68a74f3f00b0253fe70763abd57cac4237b) ([#11890](https://github.com/yt-dlp/yt-dlp/issues/11890)) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
|
||||||
|
### 2024.12.13
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **patreon**: campaign: [Support /c/ URLs](https://github.com/yt-dlp/yt-dlp/commit/bc262bcad4d3683ceadf61a7eb87e233e72adef3) ([#11756](https://github.com/yt-dlp/yt-dlp/issues/11756)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **soundcloud**: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/f4d3e9e6dc25077b79849a31a2f67f93fdc01e62) ([#11777](https://github.com/yt-dlp/yt-dlp/issues/11777)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **youtube**
|
||||||
|
- [Fix `release_date` extraction](https://github.com/yt-dlp/yt-dlp/commit/d5e2a379f2adcb28bc48c7d9e90716d7278f89d2) ([#11759](https://github.com/yt-dlp/yt-dlp/issues/11759)) by [MutantPiggieGolem1](https://github.com/MutantPiggieGolem1)
|
||||||
|
- [Fix signature function extraction for `2f1832d2`](https://github.com/yt-dlp/yt-dlp/commit/5460cd91891bf613a2065e2fc278d9903c37a127) ([#11801](https://github.com/yt-dlp/yt-dlp/issues/11801)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Prioritize original language over auto-dubbed audio](https://github.com/yt-dlp/yt-dlp/commit/dc3c4fddcc653989dae71fc563d82a308fc898cc) ([#11803](https://github.com/yt-dlp/yt-dlp/issues/11803)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- search_url: [Fix playlist searches](https://github.com/yt-dlp/yt-dlp/commit/f6c73aad5f1a67544bea137ebd9d1e22e0e56567) ([#11782](https://github.com/yt-dlp/yt-dlp/issues/11782)) by [Crypto90](https://github.com/Crypto90)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **cleanup**: [Make more playlist entries lazy](https://github.com/yt-dlp/yt-dlp/commit/54216696261bc07cacd9a837c501d9e0b7fed09e) ([#11763](https://github.com/yt-dlp/yt-dlp/issues/11763)) by [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
### 2024.12.06
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- **cookies**: [Add `--cookies-from-browser` support for MS Store Firefox](https://github.com/yt-dlp/yt-dlp/commit/354cb4026cf2191e1a130ec2a627b95cabfbc60a) ([#11731](https://github.com/yt-dlp/yt-dlp/issues/11731)) by [wesson09](https://github.com/wesson09)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **bilibili**: [Fix HD formats extraction](https://github.com/yt-dlp/yt-dlp/commit/fca3eb5f8be08d5fab2e18b45b7281a12e566725) ([#11734](https://github.com/yt-dlp/yt-dlp/issues/11734)) by [grqz](https://github.com/grqz)
|
||||||
|
- **soundcloud**: [Fix formats extraction](https://github.com/yt-dlp/yt-dlp/commit/2feb28028ee48f2185d2d95076e62accb09b9e2e) ([#11742](https://github.com/yt-dlp/yt-dlp/issues/11742)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **youtube**
|
||||||
|
- [Fix `n` sig extraction for player `3bb1f723`](https://github.com/yt-dlp/yt-dlp/commit/a95ee6d8803fca9157adecf63732ab58bf87fd88) ([#11750](https://github.com/yt-dlp/yt-dlp/issues/11750)) by [bashonly](https://github.com/bashonly) (With fixes in [4bd2655](https://github.com/yt-dlp/yt-dlp/commit/4bd2655398aed450456197a6767639114a24eac2))
|
||||||
|
- [Fix signature function extraction](https://github.com/yt-dlp/yt-dlp/commit/4c85ccd1366c88cf93982f8350f58eed17355981) ([#11751](https://github.com/yt-dlp/yt-dlp/issues/11751)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Player client maintenance](https://github.com/yt-dlp/yt-dlp/commit/2e49c789d3eebc39af8910705d65a98bca0e4c4f) ([#11724](https://github.com/yt-dlp/yt-dlp/issues/11724)) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
### 2024.12.03
|
### 2024.12.03
|
||||||
|
|
||||||
#### Core changes
|
#### Core changes
|
||||||
|
|||||||
@@ -613,8 +613,7 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
|||||||
--no-restrict-filenames Allow Unicode characters, "&" and spaces in
|
--no-restrict-filenames Allow Unicode characters, "&" and spaces in
|
||||||
filenames (default)
|
filenames (default)
|
||||||
--windows-filenames Force filenames to be Windows-compatible
|
--windows-filenames Force filenames to be Windows-compatible
|
||||||
--no-windows-filenames Make filenames Windows-compatible only if
|
--no-windows-filenames Sanitize filenames only minimally
|
||||||
using Windows (default)
|
|
||||||
--trim-filenames LENGTH Limit the filename length (excluding
|
--trim-filenames LENGTH Limit the filename length (excluding
|
||||||
extension) to the specified number of
|
extension) to the specified number of
|
||||||
characters
|
characters
|
||||||
@@ -1776,7 +1775,7 @@ The following extractors use this feature:
|
|||||||
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side)
|
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side)
|
||||||
* `max_comments`: Limit the amount of comments to gather. Comma-separated list of integers representing `max-comments,max-parents,max-replies,max-replies-per-thread`. Default is `all,all,all,all`
|
* `max_comments`: Limit the amount of comments to gather. Comma-separated list of integers representing `max-comments,max-parents,max-replies,max-replies-per-thread`. Default is `all,all,all,all`
|
||||||
* E.g. `all,all,1000,10` will get a maximum of 1000 replies total, with up to 10 replies per thread. `1000,all,100` will get a maximum of 1000 comments, with a maximum of 100 replies total
|
* E.g. `all,all,1000,10` will get a maximum of 1000 replies total, with up to 10 replies per thread. `1000,all,100` will get a maximum of 1000 comments, with a maximum of 100 replies total
|
||||||
* `formats`: Change the types of formats to return. `dashy` (convert HTTP to DASH), `duplicate` (identical content but different URLs or protocol; includes `dashy`), `incomplete` (cannot be downloaded completely - live dash and post-live m3u8)
|
* `formats`: Change the types of formats to return. `dashy` (convert HTTP to DASH), `duplicate` (identical content but different URLs or protocol; includes `dashy`), `incomplete` (cannot be downloaded completely - live dash and post-live m3u8), `missing_pot` (include formats that require a PO Token but are missing one)
|
||||||
* `innertube_host`: Innertube API host to use for all API requests; e.g. `studio.youtube.com`, `youtubei.googleapis.com`. Note that cookies exported from one subdomain will not work on others
|
* `innertube_host`: Innertube API host to use for all API requests; e.g. `studio.youtube.com`, `youtubei.googleapis.com`. Note that cookies exported from one subdomain will not work on others
|
||||||
* `innertube_key`: Innertube API key to use for all API requests. By default, no API key is used
|
* `innertube_key`: Innertube API key to use for all API requests. By default, no API key is used
|
||||||
* `raise_incomplete_data`: `Incomplete Data Received` raises an error instead of reporting a warning
|
* `raise_incomplete_data`: `Incomplete Data Received` raises an error instead of reporting a warning
|
||||||
@@ -1860,7 +1859,7 @@ The following extractors use this feature:
|
|||||||
* `cdn`: One or more CDN IDs to use with the API call for stream URLs, e.g. `gcp_cdn`, `gs_cdn_pc_app`, `gs_cdn_mobile_web`, `gs_cdn_pc_web`
|
* `cdn`: One or more CDN IDs to use with the API call for stream URLs, e.g. `gcp_cdn`, `gs_cdn_pc_app`, `gs_cdn_mobile_web`, `gs_cdn_pc_web`
|
||||||
|
|
||||||
#### soundcloud
|
#### soundcloud
|
||||||
* `formats`: Formats to request from the API. Requested values should be in the format of `{protocol}_{extension}` (omitting the bitrate), e.g. `hls_opus,http_aac`. The `*` character functions as a wildcard, e.g. `*_mp3`, and can be passed by itself to request all formats. Known protocols include `http`, `hls` and `hls-aes`; known extensions include `aac`, `opus` and `mp3`. Original `download` formats are always extracted. Default is `http_aac,hls_aac,http_opus,hls_opus,http_mp3,hls_mp3`
|
* `formats`: Formats to request from the API. Requested values should be in the format of `{protocol}_{codec}`, e.g. `hls_opus,http_aac`. The `*` character functions as a wildcard, e.g. `*_mp3`, and can be passed by itself to request all formats. Known protocols include `http`, `hls` and `hls-aes`; known codecs include `aac`, `opus` and `mp3`. Original `download` formats are always extracted. Default is `http_aac,hls_aac,http_opus,hls_opus,http_mp3,hls_mp3`
|
||||||
|
|
||||||
#### orfon (orf:on)
|
#### orfon (orf:on)
|
||||||
* `prefer_segments_playlist`: Prefer a playlist of program segments instead of a single complete video when available. If individual segments are desired, use `--concat-playlist never --extractor-args "orfon:prefer_segments_playlist"`
|
* `prefer_segments_playlist`: Prefer a playlist of program segments instead of a single complete video when available. If individual segments are desired, use `--concat-playlist never --extractor-args "orfon:prefer_segments_playlist"`
|
||||||
|
|||||||
@@ -761,6 +761,13 @@ class TestYoutubeDL(unittest.TestCase):
|
|||||||
test('%(width)06d.%%(ext)s', 'NA.%(ext)s')
|
test('%(width)06d.%%(ext)s', 'NA.%(ext)s')
|
||||||
test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
|
test('%%(width)06d.%(ext)s', '%(width)06d.mp4')
|
||||||
|
|
||||||
|
# Sanitization options
|
||||||
|
test('%(title3)s', (None, 'foo⧸bar⧹test'))
|
||||||
|
test('%(title5)s', (None, 'aei_A'), restrictfilenames=True)
|
||||||
|
test('%(title3)s', (None, 'foo_bar_test'), windowsfilenames=False, restrictfilenames=True)
|
||||||
|
if sys.platform != 'win32':
|
||||||
|
test('%(title3)s', (None, 'foo⧸bar\\test'), windowsfilenames=False)
|
||||||
|
|
||||||
# ID sanitization
|
# ID sanitization
|
||||||
test('%(id)s', '_abcd', info={'id': '_abcd'})
|
test('%(id)s', '_abcd', info={'id': '_abcd'})
|
||||||
test('%(some_id)s', '_abcd', info={'some_id': '_abcd'})
|
test('%(some_id)s', '_abcd', info={'some_id': '_abcd'})
|
||||||
|
|||||||
@@ -68,6 +68,16 @@ _SIG_TESTS = [
|
|||||||
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
||||||
'AOq0QJ8wRAIgXmPlOPSBkkUs1bYFYlJCfe29xx8j7v1pDL2QwbdV96sCIEzpWqMGkFR20CFOg51Tp-7vj_EMu-m37KtXJoOySqa0',
|
'AOq0QJ8wRAIgXmPlOPSBkkUs1bYFYlJCfe29xx8j7v1pDL2QwbdV96sCIEzpWqMGkFR20CFOg51Tp-7vj_EMu-m37KtXJoOySqa0',
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/3bb1f723/player_ias.vflset/en_US/base.js',
|
||||||
|
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
||||||
|
'MyOSJXtKI3m-uME_jv7-pT12gOFC02RFkGoqWpzE0Cs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/2f1832d2/player_ias.vflset/en_US/base.js',
|
||||||
|
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
||||||
|
'0QJ8wRAIgXmPlOPSBkkUs1bYFYlJCfe29xxAj7v1pDL0QwbdV96sCIEzpWqMGkFR20CFOg51Tp-7vj_EMu-m37KtXJ2OySqa0q',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
_NSIG_TESTS = [
|
_NSIG_TESTS = [
|
||||||
@@ -183,6 +193,14 @@ _NSIG_TESTS = [
|
|||||||
'https://www.youtube.com/s/player/b12cc44b/player_ias.vflset/en_US/base.js',
|
'https://www.youtube.com/s/player/b12cc44b/player_ias.vflset/en_US/base.js',
|
||||||
'keLa5R2U00sR9SQK', 'N1OGyujjEwMnLw',
|
'keLa5R2U00sR9SQK', 'N1OGyujjEwMnLw',
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/3bb1f723/player_ias.vflset/en_US/base.js',
|
||||||
|
'gK15nzVyaXE9RsMP3z', 'ZFFWFLPWx9DEgQ',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/2f1832d2/player_ias.vflset/en_US/base.js',
|
||||||
|
'YWt1qdbe8SAfkoPHW5d', 'RrRjWQOJmBiP',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -254,8 +272,11 @@ def signature(jscode, sig_input):
|
|||||||
|
|
||||||
|
|
||||||
def n_sig(jscode, sig_input):
|
def n_sig(jscode, sig_input):
|
||||||
funcname = YoutubeIE(FakeYDL())._extract_n_function_name(jscode)
|
ie = YoutubeIE(FakeYDL())
|
||||||
return JSInterpreter(jscode).call_function(funcname, sig_input)
|
funcname = ie._extract_n_function_name(jscode)
|
||||||
|
jsi = JSInterpreter(jscode)
|
||||||
|
func = jsi.extract_function_from_code(*ie._fixup_n_function_code(*jsi.extract_function_code(funcname)))
|
||||||
|
return func([sig_input])
|
||||||
|
|
||||||
|
|
||||||
make_sig_test = t_factory(
|
make_sig_test = t_factory(
|
||||||
|
|||||||
@@ -266,7 +266,9 @@ class YoutubeDL:
|
|||||||
outtmpl_na_placeholder: Placeholder for unavailable meta fields.
|
outtmpl_na_placeholder: Placeholder for unavailable meta fields.
|
||||||
restrictfilenames: Do not allow "&" and spaces in file names
|
restrictfilenames: Do not allow "&" and spaces in file names
|
||||||
trim_file_name: Limit length of filename (extension excluded)
|
trim_file_name: Limit length of filename (extension excluded)
|
||||||
windowsfilenames: Force the filenames to be windows compatible
|
windowsfilenames: True: Force filenames to be Windows compatible
|
||||||
|
False: Sanitize filenames only minimally
|
||||||
|
This option has no effect when running on Windows
|
||||||
ignoreerrors: Do not stop on download/postprocessing errors.
|
ignoreerrors: Do not stop on download/postprocessing errors.
|
||||||
Can be 'only_download' to ignore only download errors.
|
Can be 'only_download' to ignore only download errors.
|
||||||
Default is 'only_download' for CLI, but False for API
|
Default is 'only_download' for CLI, but False for API
|
||||||
@@ -1192,8 +1194,7 @@ class YoutubeDL:
|
|||||||
|
|
||||||
def prepare_outtmpl(self, outtmpl, info_dict, sanitize=False):
|
def prepare_outtmpl(self, outtmpl, info_dict, sanitize=False):
|
||||||
""" Make the outtmpl and info_dict suitable for substitution: ydl.escape_outtmpl(outtmpl) % info_dict
|
""" Make the outtmpl and info_dict suitable for substitution: ydl.escape_outtmpl(outtmpl) % info_dict
|
||||||
@param sanitize Whether to sanitize the output as a filename.
|
@param sanitize Whether to sanitize the output as a filename
|
||||||
For backward compatibility, a function can also be passed
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
info_dict.setdefault('epoch', int(time.time())) # keep epoch consistent once set
|
info_dict.setdefault('epoch', int(time.time())) # keep epoch consistent once set
|
||||||
@@ -1309,14 +1310,23 @@ class YoutubeDL:
|
|||||||
|
|
||||||
na = self.params.get('outtmpl_na_placeholder', 'NA')
|
na = self.params.get('outtmpl_na_placeholder', 'NA')
|
||||||
|
|
||||||
def filename_sanitizer(key, value, restricted=self.params.get('restrictfilenames')):
|
def filename_sanitizer(key, value, restricted):
|
||||||
return sanitize_filename(str(value), restricted=restricted, is_id=(
|
return sanitize_filename(str(value), restricted=restricted, is_id=(
|
||||||
bool(re.search(r'(^|[_.])id(\.|$)', key))
|
bool(re.search(r'(^|[_.])id(\.|$)', key))
|
||||||
if 'filename-sanitization' in self.params['compat_opts']
|
if 'filename-sanitization' in self.params['compat_opts']
|
||||||
else NO_DEFAULT))
|
else NO_DEFAULT))
|
||||||
|
|
||||||
sanitizer = sanitize if callable(sanitize) else filename_sanitizer
|
if callable(sanitize):
|
||||||
sanitize = bool(sanitize)
|
self.deprecation_warning('Passing a callable "sanitize" to YoutubeDL.prepare_outtmpl is deprecated')
|
||||||
|
elif not sanitize:
|
||||||
|
pass
|
||||||
|
elif (sys.platform != 'win32' and not self.params.get('restrictfilenames')
|
||||||
|
and self.params.get('windowsfilenames') is False):
|
||||||
|
def sanitize(key, value):
|
||||||
|
return value.replace('/', '\u29F8').replace('\0', '')
|
||||||
|
else:
|
||||||
|
def sanitize(key, value):
|
||||||
|
return filename_sanitizer(key, value, restricted=self.params.get('restrictfilenames'))
|
||||||
|
|
||||||
def _dumpjson_default(obj):
|
def _dumpjson_default(obj):
|
||||||
if isinstance(obj, (set, LazyList)):
|
if isinstance(obj, (set, LazyList)):
|
||||||
@@ -1399,13 +1409,13 @@ class YoutubeDL:
|
|||||||
|
|
||||||
if sanitize:
|
if sanitize:
|
||||||
# If value is an object, sanitize might convert it to a string
|
# If value is an object, sanitize might convert it to a string
|
||||||
# So we convert it to repr first
|
# So we manually convert it before sanitizing
|
||||||
if fmt[-1] == 'r':
|
if fmt[-1] == 'r':
|
||||||
value, fmt = repr(value), str_fmt
|
value, fmt = repr(value), str_fmt
|
||||||
elif fmt[-1] == 'a':
|
elif fmt[-1] == 'a':
|
||||||
value, fmt = ascii(value), str_fmt
|
value, fmt = ascii(value), str_fmt
|
||||||
if fmt[-1] in 'csra':
|
if fmt[-1] in 'csra':
|
||||||
value = sanitizer(last_field, value)
|
value = sanitize(last_field, value)
|
||||||
|
|
||||||
key = '{}\0{}'.format(key.replace('%', '%\0'), outer_mobj.group('format'))
|
key = '{}\0{}'.format(key.replace('%', '%\0'), outer_mobj.group('format'))
|
||||||
TMPL_DICT[key] = value
|
TMPL_DICT[key] = value
|
||||||
|
|||||||
@@ -195,7 +195,10 @@ def _extract_firefox_cookies(profile, container, logger):
|
|||||||
|
|
||||||
def _firefox_browser_dirs():
|
def _firefox_browser_dirs():
|
||||||
if sys.platform in ('cygwin', 'win32'):
|
if sys.platform in ('cygwin', 'win32'):
|
||||||
yield os.path.expandvars(R'%APPDATA%\Mozilla\Firefox\Profiles')
|
yield from map(os.path.expandvars, (
|
||||||
|
R'%APPDATA%\Mozilla\Firefox\Profiles',
|
||||||
|
R'%LOCALAPPDATA%\Packages\Mozilla.Firefox_n80bbvh6b1yt2\LocalCache\Roaming\Mozilla\Firefox\Profiles',
|
||||||
|
))
|
||||||
|
|
||||||
elif sys.platform == 'darwin':
|
elif sys.platform == 'darwin':
|
||||||
yield os.path.expanduser('~/Library/Application Support/Firefox/Profiles')
|
yield os.path.expanduser('~/Library/Application Support/Firefox/Profiles')
|
||||||
|
|||||||
@@ -681,12 +681,6 @@ class BiliBiliIE(BilibiliBaseIE):
|
|||||||
old_video_id = format_field(aid, None, f'%s_part{part_id or 1}')
|
old_video_id = format_field(aid, None, f'%s_part{part_id or 1}')
|
||||||
cid = traverse_obj(video_data, ('pages', part_id - 1, 'cid')) if part_id else video_data.get('cid')
|
cid = traverse_obj(video_data, ('pages', part_id - 1, 'cid')) if part_id else video_data.get('cid')
|
||||||
|
|
||||||
play_info = (
|
|
||||||
traverse_obj(
|
|
||||||
self._search_json(r'window\.__playinfo__\s*=', webpage, 'play info', video_id, default=None),
|
|
||||||
('data', {dict}))
|
|
||||||
or self._download_playinfo(video_id, cid, headers=headers, query={'try_look': 1}))
|
|
||||||
|
|
||||||
festival_info = {}
|
festival_info = {}
|
||||||
if is_festival:
|
if is_festival:
|
||||||
festival_info = traverse_obj(initial_state, {
|
festival_info = traverse_obj(initial_state, {
|
||||||
@@ -724,6 +718,13 @@ class BiliBiliIE(BilibiliBaseIE):
|
|||||||
duration=traverse_obj(initial_state, ('videoData', 'duration', {int_or_none})),
|
duration=traverse_obj(initial_state, ('videoData', 'duration', {int_or_none})),
|
||||||
__post_extractor=self.extract_comments(aid))
|
__post_extractor=self.extract_comments(aid))
|
||||||
|
|
||||||
|
play_info = None
|
||||||
|
if self.is_logged_in:
|
||||||
|
play_info = traverse_obj(
|
||||||
|
self._search_json(r'window\.__playinfo__\s*=', webpage, 'play info', video_id, default=None),
|
||||||
|
('data', {dict}))
|
||||||
|
if not play_info:
|
||||||
|
play_info = self._download_playinfo(video_id, cid, headers=headers, query={'try_look': 1})
|
||||||
formats = self.extract_formats(play_info)
|
formats = self.extract_formats(play_info)
|
||||||
|
|
||||||
if video_data.get('is_upower_exclusive'):
|
if video_data.get('is_upower_exclusive'):
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from ..utils import (
|
|||||||
update_url_query,
|
update_url_query,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class BrightcoveLegacyIE(InfoExtractor):
|
class BrightcoveLegacyIE(InfoExtractor):
|
||||||
@@ -935,8 +936,8 @@ class BrightcoveNewIE(BrightcoveNewBaseIE):
|
|||||||
|
|
||||||
if content_type == 'playlist':
|
if content_type == 'playlist':
|
||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
[self._parse_brightcove_metadata(vid, vid.get('id'), headers)
|
(self._parse_brightcove_metadata(vid, vid['id'], headers)
|
||||||
for vid in json_data.get('videos', []) if vid.get('id')],
|
for vid in traverse_obj(json_data, ('videos', lambda _, v: v['id']))),
|
||||||
json_data.get('id'), json_data.get('name'),
|
json_data.get('id'), json_data.get('name'),
|
||||||
json_data.get('description'))
|
json_data.get('description'))
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ class DVTVIE(InfoExtractor):
|
|||||||
items = re.findall(r'(?s)playlist\.push\(({.+?})\);', webpage)
|
items = re.findall(r'(?s)playlist\.push\(({.+?})\);', webpage)
|
||||||
if items:
|
if items:
|
||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
[self._parse_video_metadata(i, video_id, timestamp) for i in items],
|
(self._parse_video_metadata(i, video_id, timestamp) for i in items),
|
||||||
video_id, self._html_search_meta('twitter:title', webpage))
|
video_id, self._html_search_meta('twitter:title', webpage))
|
||||||
|
|
||||||
item = self._search_regex(
|
item = self._search_regex(
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ class NYTimesCookingIE(NYTimesBaseIE):
|
|||||||
if media_ids:
|
if media_ids:
|
||||||
media_ids.append(lead_video_id)
|
media_ids.append(lead_video_id)
|
||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
[self._extract_video(media_id) for media_id in media_ids], page_id, title, description)
|
map(self._extract_video, media_ids), page_id, title, description)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**self._extract_video(lead_video_id),
|
**self._extract_video(lead_video_id),
|
||||||
|
|||||||
@@ -457,7 +457,7 @@ class PatreonCampaignIE(PatreonBaseIE):
|
|||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
https?://(?:www\.)?patreon\.com/(?:
|
https?://(?:www\.)?patreon\.com/(?:
|
||||||
(?:m|api/campaigns)/(?P<campaign_id>\d+)|
|
(?:m|api/campaigns)/(?P<campaign_id>\d+)|
|
||||||
(?P<vanity>(?!creation[?/]|posts/|rss[?/])[\w-]+)
|
(?:c/)?(?P<vanity>(?!creation[?/]|posts/|rss[?/])[\w-]+)
|
||||||
)(?:/posts)?/?(?:$|[?#])'''
|
)(?:/posts)?/?(?:$|[?#])'''
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.patreon.com/dissonancepod/',
|
'url': 'https://www.patreon.com/dissonancepod/',
|
||||||
@@ -509,6 +509,26 @@ class PatreonCampaignIE(PatreonBaseIE):
|
|||||||
'thumbnail': r're:^https?://.*$',
|
'thumbnail': r're:^https?://.*$',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 201,
|
'playlist_mincount': 201,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.patreon.com/c/OgSog',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '8504388',
|
||||||
|
'title': 'OGSoG',
|
||||||
|
'description': r're:(?s)Hello and welcome to our Patreon page. We are Mari, Lasercorn, .+',
|
||||||
|
'channel': 'OGSoG',
|
||||||
|
'channel_id': '8504388',
|
||||||
|
'channel_url': 'https://www.patreon.com/OgSog',
|
||||||
|
'uploader_url': 'https://www.patreon.com/OgSog',
|
||||||
|
'uploader_id': '72323575',
|
||||||
|
'uploader': 'David Moss',
|
||||||
|
'thumbnail': r're:https?://.+/.+',
|
||||||
|
'channel_follower_count': int,
|
||||||
|
'age_limit': 0,
|
||||||
|
},
|
||||||
|
'playlist_mincount': 331,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.patreon.com/c/OgSog/posts',
|
||||||
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.patreon.com/dissonancepod/posts',
|
'url': 'https://www.patreon.com/dissonancepod/posts',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from .common import InfoExtractor, SearchInfoExtractor
|
|||||||
from ..networking import HEADRequest
|
from ..networking import HEADRequest
|
||||||
from ..networking.exceptions import HTTPError
|
from ..networking.exceptions import HTTPError
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
KNOWN_EXTENSIONS,
|
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
@@ -211,6 +210,7 @@ class SoundcloudBaseIE(InfoExtractor):
|
|||||||
|
|
||||||
format_urls = set()
|
format_urls = set()
|
||||||
formats = []
|
formats = []
|
||||||
|
has_drm = False
|
||||||
query = {'client_id': self._CLIENT_ID}
|
query = {'client_id': self._CLIENT_ID}
|
||||||
if secret_token:
|
if secret_token:
|
||||||
query['secret_token'] = secret_token
|
query['secret_token'] = secret_token
|
||||||
@@ -246,55 +246,24 @@ class SoundcloudBaseIE(InfoExtractor):
|
|||||||
'url': format_url,
|
'url': format_url,
|
||||||
'quality': 10,
|
'quality': 10,
|
||||||
'format_note': 'Original',
|
'format_note': 'Original',
|
||||||
|
'vcodec': 'none',
|
||||||
})
|
})
|
||||||
|
|
||||||
def invalid_url(url):
|
def invalid_url(url):
|
||||||
return not url or url in format_urls
|
return not url or url in format_urls
|
||||||
|
|
||||||
def add_format(f, protocol, is_preview=False):
|
|
||||||
mobj = re.search(r'\.(?P<abr>\d+)\.(?P<ext>[0-9a-z]{3,4})(?=[/?])', stream_url)
|
|
||||||
if mobj:
|
|
||||||
for k, v in mobj.groupdict().items():
|
|
||||||
if not f.get(k):
|
|
||||||
f[k] = v
|
|
||||||
format_id_list = []
|
|
||||||
if protocol:
|
|
||||||
format_id_list.append(protocol)
|
|
||||||
ext = f.get('ext')
|
|
||||||
if ext == 'aac':
|
|
||||||
f.update({
|
|
||||||
'abr': 256,
|
|
||||||
'quality': 5,
|
|
||||||
'format_note': 'Premium',
|
|
||||||
})
|
|
||||||
for k in ('ext', 'abr'):
|
|
||||||
v = str_or_none(f.get(k))
|
|
||||||
if v:
|
|
||||||
format_id_list.append(v)
|
|
||||||
preview = is_preview or re.search(r'/(?:preview|playlist)/0/30/', f['url'])
|
|
||||||
if preview:
|
|
||||||
format_id_list.append('preview')
|
|
||||||
abr = f.get('abr')
|
|
||||||
if abr:
|
|
||||||
f['abr'] = int(abr)
|
|
||||||
if protocol in ('hls', 'hls-aes'):
|
|
||||||
protocol = 'm3u8' if ext == 'aac' else 'm3u8_native'
|
|
||||||
else:
|
|
||||||
protocol = 'http'
|
|
||||||
f.update({
|
|
||||||
'format_id': '_'.join(format_id_list),
|
|
||||||
'protocol': protocol,
|
|
||||||
'preference': -10 if preview else None,
|
|
||||||
})
|
|
||||||
formats.append(f)
|
|
||||||
|
|
||||||
# New API
|
# New API
|
||||||
for t in traverse_obj(info, ('media', 'transcodings', lambda _, v: url_or_none(v['url']))):
|
for t in traverse_obj(info, ('media', 'transcodings', lambda _, v: url_or_none(v['url']) and v['preset'])):
|
||||||
if extract_flat:
|
if extract_flat:
|
||||||
break
|
break
|
||||||
format_url = t['url']
|
format_url = t['url']
|
||||||
|
preset = t['preset']
|
||||||
|
preset_base = preset.partition('_')[0]
|
||||||
|
|
||||||
protocol = traverse_obj(t, ('format', 'protocol', {str}))
|
protocol = traverse_obj(t, ('format', 'protocol', {str})) or 'http'
|
||||||
|
if protocol.startswith(('ctr-', 'cbc-')):
|
||||||
|
has_drm = True
|
||||||
|
continue
|
||||||
if protocol == 'progressive':
|
if protocol == 'progressive':
|
||||||
protocol = 'http'
|
protocol = 'http'
|
||||||
if protocol != 'hls' and '/hls' in format_url:
|
if protocol != 'hls' and '/hls' in format_url:
|
||||||
@@ -302,35 +271,60 @@ class SoundcloudBaseIE(InfoExtractor):
|
|||||||
if protocol == 'encrypted-hls' or '/encrypted-hls' in format_url:
|
if protocol == 'encrypted-hls' or '/encrypted-hls' in format_url:
|
||||||
protocol = 'hls-aes'
|
protocol = 'hls-aes'
|
||||||
|
|
||||||
ext = None
|
short_identifier = f'{protocol}_{preset_base}'
|
||||||
if preset := traverse_obj(t, ('preset', {str_or_none})):
|
if preset_base == 'abr':
|
||||||
ext = preset.split('_')[0]
|
self.write_debug(f'Skipping broken "{short_identifier}" format')
|
||||||
if ext not in KNOWN_EXTENSIONS:
|
continue
|
||||||
ext = mimetype2ext(traverse_obj(t, ('format', 'mime_type', {str})))
|
if not self._is_requested(short_identifier):
|
||||||
|
self.write_debug(f'"{short_identifier}" is not a requested format, skipping')
|
||||||
identifier = join_nonempty(protocol, ext, delim='_')
|
|
||||||
if not self._is_requested(identifier):
|
|
||||||
self.write_debug(f'"{identifier}" is not a requested format, skipping')
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# XXX: if not extract_flat, 429 error must be caught where _extract_info_dict is called
|
# XXX: if not extract_flat, 429 error must be caught where _extract_info_dict is called
|
||||||
stream_url = traverse_obj(self._call_api(
|
stream_url = traverse_obj(self._call_api(
|
||||||
format_url, track_id, f'Downloading {identifier} format info JSON',
|
format_url, track_id, f'Downloading {short_identifier} format info JSON',
|
||||||
query=query, headers=self._HEADERS), ('url', {url_or_none}))
|
query=query, headers=self._HEADERS), ('url', {url_or_none}))
|
||||||
|
|
||||||
if invalid_url(stream_url):
|
if invalid_url(stream_url):
|
||||||
continue
|
continue
|
||||||
format_urls.add(stream_url)
|
format_urls.add(stream_url)
|
||||||
add_format({
|
|
||||||
|
mime_type = traverse_obj(t, ('format', 'mime_type', {str}))
|
||||||
|
codec = self._search_regex(r'codecs="([^"]+)"', mime_type, 'codec', default=None)
|
||||||
|
ext = {
|
||||||
|
'mp4a': 'm4a',
|
||||||
|
'opus': 'opus',
|
||||||
|
}.get(codec[:4] if codec else None) or mimetype2ext(mime_type, default=None)
|
||||||
|
if not ext or ext == 'm3u8':
|
||||||
|
ext = preset_base
|
||||||
|
|
||||||
|
is_premium = t.get('quality') == 'hq'
|
||||||
|
abr = int_or_none(
|
||||||
|
self._search_regex(r'(\d+)k$', preset, 'abr', default=None)
|
||||||
|
or self._search_regex(r'\.(\d+)\.(?:opus|mp3)[/?]', stream_url, 'abr', default=None)
|
||||||
|
or (256 if (is_premium and 'aac' in preset) else None))
|
||||||
|
|
||||||
|
is_preview = (t.get('snipped')
|
||||||
|
or '/preview/' in format_url
|
||||||
|
or re.search(r'/(?:preview|playlist)/0/30/', stream_url))
|
||||||
|
|
||||||
|
formats.append({
|
||||||
|
'format_id': join_nonempty(protocol, preset, is_preview and 'preview', delim='_'),
|
||||||
'url': stream_url,
|
'url': stream_url,
|
||||||
'ext': ext,
|
'ext': ext,
|
||||||
}, protocol, t.get('snipped') or '/preview/' in format_url)
|
'acodec': codec,
|
||||||
|
'vcodec': 'none',
|
||||||
|
'abr': abr,
|
||||||
|
'protocol': 'm3u8_native' if protocol in ('hls', 'hls-aes') else 'http',
|
||||||
|
'container': 'm4a_dash' if ext == 'm4a' else None,
|
||||||
|
'quality': 5 if is_premium else 0 if (abr and abr >= 160) else -1,
|
||||||
|
'format_note': 'Premium' if is_premium else None,
|
||||||
|
'preference': -10 if is_preview else None,
|
||||||
|
})
|
||||||
|
|
||||||
for f in formats:
|
if not formats:
|
||||||
f['vcodec'] = 'none'
|
if has_drm:
|
||||||
|
self.report_drm(track_id)
|
||||||
if not formats and info.get('policy') == 'BLOCK':
|
if info.get('policy') == 'BLOCK':
|
||||||
self.raise_geo_restricted(metadata_available=True)
|
self.raise_geo_restricted(metadata_available=True)
|
||||||
|
|
||||||
user = info.get('user') or {}
|
user = info.get('user') or {}
|
||||||
|
|
||||||
|
|||||||
@@ -421,5 +421,5 @@ class VidyardIE(VidyardBaseIE):
|
|||||||
return self._process_video_json(video_json['chapters'][0], video_id)
|
return self._process_video_json(video_json['chapters'][0], video_id)
|
||||||
|
|
||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
[self._process_video_json(chapter, video_id) for chapter in video_json['chapters']],
|
(self._process_video_json(chapter, video_id) for chapter in video_json['chapters']),
|
||||||
str(video_json['playerUuid']), video_json.get('name'))
|
str(video_json['playerUuid']), video_json.get('name'))
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'WEB',
|
'clientName': 'WEB',
|
||||||
'clientVersion': '2.20240726.00.00',
|
'clientVersion': '2.20241126.01.00',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 1,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 1,
|
||||||
@@ -90,7 +90,7 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'WEB',
|
'clientName': 'WEB',
|
||||||
'clientVersion': '2.20240726.00.00',
|
'clientVersion': '2.20241126.01.00',
|
||||||
'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15,gzip(gfe)',
|
'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15,gzip(gfe)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -102,7 +102,7 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'WEB_EMBEDDED_PLAYER',
|
'clientName': 'WEB_EMBEDDED_PLAYER',
|
||||||
'clientVersion': '1.20240723.01.00',
|
'clientVersion': '1.20241201.00.00',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 56,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 56,
|
||||||
@@ -113,7 +113,7 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'WEB_REMIX',
|
'clientName': 'WEB_REMIX',
|
||||||
'clientVersion': '1.20240724.00.00',
|
'clientVersion': '1.20241127.01.00',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 67,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 67,
|
||||||
@@ -124,7 +124,7 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'WEB_CREATOR',
|
'clientName': 'WEB_CREATOR',
|
||||||
'clientVersion': '1.20240723.03.00',
|
'clientVersion': '1.20241203.01.00',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 62,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 62,
|
||||||
@@ -162,7 +162,6 @@ INNERTUBE_CLIENTS = {
|
|||||||
'REQUIRE_JS_PLAYER': False,
|
'REQUIRE_JS_PLAYER': False,
|
||||||
'REQUIRE_PO_TOKEN': True,
|
'REQUIRE_PO_TOKEN': True,
|
||||||
'REQUIRE_AUTH': True,
|
'REQUIRE_AUTH': True,
|
||||||
'SUPPORTS_COOKIES': True,
|
|
||||||
},
|
},
|
||||||
# This client now requires sign-in for every video
|
# This client now requires sign-in for every video
|
||||||
'android_creator': {
|
'android_creator': {
|
||||||
@@ -197,7 +196,6 @@ INNERTUBE_CLIENTS = {
|
|||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 28,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 28,
|
||||||
'REQUIRE_JS_PLAYER': False,
|
'REQUIRE_JS_PLAYER': False,
|
||||||
'SUPPORTS_COOKIES': True,
|
|
||||||
},
|
},
|
||||||
# iOS clients have HLS live streams. Setting device model to get 60fps formats.
|
# iOS clients have HLS live streams. Setting device model to get 60fps formats.
|
||||||
# See: https://github.com/TeamNewPipe/NewPipeExtractor/issues/680#issuecomment-1002724558
|
# See: https://github.com/TeamNewPipe/NewPipeExtractor/issues/680#issuecomment-1002724558
|
||||||
@@ -214,6 +212,7 @@ INNERTUBE_CLIENTS = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 5,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 5,
|
||||||
|
'REQUIRE_PO_TOKEN': True,
|
||||||
'REQUIRE_JS_PLAYER': False,
|
'REQUIRE_JS_PLAYER': False,
|
||||||
},
|
},
|
||||||
# This client now requires sign-in for every video
|
# This client now requires sign-in for every video
|
||||||
@@ -232,7 +231,6 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 26,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 26,
|
||||||
'REQUIRE_JS_PLAYER': False,
|
'REQUIRE_JS_PLAYER': False,
|
||||||
'REQUIRE_AUTH': True,
|
'REQUIRE_AUTH': True,
|
||||||
'SUPPORTS_COOKIES': True,
|
|
||||||
},
|
},
|
||||||
# This client now requires sign-in for every video
|
# This client now requires sign-in for every video
|
||||||
'ios_creator': {
|
'ios_creator': {
|
||||||
@@ -257,7 +255,8 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'MWEB',
|
'clientName': 'MWEB',
|
||||||
'clientVersion': '2.20240726.01.00',
|
'clientVersion': '2.20241202.07.00',
|
||||||
|
'userAgent': 'Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 2,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 2,
|
||||||
@@ -267,7 +266,7 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'TVHTML5',
|
'clientName': 'TVHTML5',
|
||||||
'clientVersion': '7.20240724.13.00',
|
'clientVersion': '7.20241201.18.00',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 7,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 7,
|
||||||
@@ -517,11 +516,12 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
|||||||
return self._search_regex(rf'^({self._YT_CHANNEL_UCID_RE})$', ucid, 'UC-id', default=None)
|
return self._search_regex(rf'^({self._YT_CHANNEL_UCID_RE})$', ucid, 'UC-id', default=None)
|
||||||
|
|
||||||
def handle_or_none(self, handle):
|
def handle_or_none(self, handle):
|
||||||
return self._search_regex(rf'^({self._YT_HANDLE_RE})$', handle, '@-handle', default=None)
|
return self._search_regex(rf'^({self._YT_HANDLE_RE})$', urllib.parse.unquote(handle or ''),
|
||||||
|
'@-handle', default=None)
|
||||||
|
|
||||||
def handle_from_url(self, url):
|
def handle_from_url(self, url):
|
||||||
return self._search_regex(rf'^(?:https?://(?:www\.)?youtube\.com)?/({self._YT_HANDLE_RE})',
|
return self._search_regex(rf'^(?:https?://(?:www\.)?youtube\.com)?/({self._YT_HANDLE_RE})',
|
||||||
url, 'channel handle', default=None)
|
urllib.parse.unquote(url or ''), 'channel handle', default=None)
|
||||||
|
|
||||||
def ucid_from_url(self, url):
|
def ucid_from_url(self, url):
|
||||||
return self._search_regex(rf'^(?:https?://(?:www\.)?youtube\.com)?/({self._YT_CHANNEL_UCID_RE})',
|
return self._search_regex(rf'^(?:https?://(?:www\.)?youtube\.com)?/({self._YT_CHANNEL_UCID_RE})',
|
||||||
@@ -1494,7 +1494,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
},
|
},
|
||||||
# Age-gate videos. See https://github.com/yt-dlp/yt-dlp/pull/575#issuecomment-888837000
|
# Age-gate videos. See https://github.com/yt-dlp/yt-dlp/pull/575#issuecomment-888837000
|
||||||
{
|
{
|
||||||
'note': 'Embed allowed age-gate video',
|
'note': 'Embed allowed age-gate video; works with web_embedded',
|
||||||
'url': 'https://youtube.com/watch?v=HtVdAasjOgU',
|
'url': 'https://youtube.com/watch?v=HtVdAasjOgU',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'HtVdAasjOgU',
|
'id': 'HtVdAasjOgU',
|
||||||
@@ -1524,7 +1524,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
'heatmap': 'count:100',
|
'heatmap': 'count:100',
|
||||||
'timestamp': 1401991663,
|
'timestamp': 1401991663,
|
||||||
},
|
},
|
||||||
'skip': 'Age-restricted; requires authentication',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'note': 'Age-gate video with embed allowed in public site',
|
'note': 'Age-gate video with embed allowed in public site',
|
||||||
@@ -2800,6 +2799,35 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
'extractor_args': {'youtube': {'player_client': ['ios'], 'player_skip': ['webpage']}},
|
'extractor_args': {'youtube': {'player_client': ['ios'], 'player_skip': ['webpage']}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
# uploader_id has non-ASCII characters that are percent-encoded in YT's JSON
|
||||||
|
'url': 'https://www.youtube.com/shorts/18NGQq7p3LY',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '18NGQq7p3LY',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '아이브 이서 장원영 리즈 삐끼삐끼 챌린지',
|
||||||
|
'description': '',
|
||||||
|
'uploader': 'ㅇㅇ',
|
||||||
|
'uploader_id': '@으아-v1k',
|
||||||
|
'uploader_url': 'https://www.youtube.com/@으아-v1k',
|
||||||
|
'channel': 'ㅇㅇ',
|
||||||
|
'channel_id': 'UCC25oTm2J7ZVoi5TngOHg9g',
|
||||||
|
'channel_url': 'https://www.youtube.com/channel/UCC25oTm2J7ZVoi5TngOHg9g',
|
||||||
|
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||||
|
'playable_in_embed': True,
|
||||||
|
'age_limit': 0,
|
||||||
|
'duration': 3,
|
||||||
|
'timestamp': 1724306170,
|
||||||
|
'upload_date': '20240822',
|
||||||
|
'availability': 'public',
|
||||||
|
'live_status': 'not_live',
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'channel_follower_count': int,
|
||||||
|
'categories': ['People & Blogs'],
|
||||||
|
'tags': [],
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
_WEBPAGE_TESTS = [
|
_WEBPAGE_TESTS = [
|
||||||
@@ -3118,19 +3146,26 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
self.to_screen('Extracted signature function:\n' + code)
|
self.to_screen('Extracted signature function:\n' + code)
|
||||||
|
|
||||||
def _parse_sig_js(self, jscode):
|
def _parse_sig_js(self, jscode):
|
||||||
|
# Examples where `sig` is funcname:
|
||||||
|
# sig=function(a){a=a.split(""); ... ;return a.join("")};
|
||||||
|
# ;c&&(c=sig(decodeURIComponent(c)),a.set(b,encodeURIComponent(c)));return a};
|
||||||
|
# {var l=f,m=h.sp,n=sig(decodeURIComponent(h.s));l.set(m,encodeURIComponent(n))}
|
||||||
|
# sig=function(J){J=J.split(""); ... ;return J.join("")};
|
||||||
|
# ;N&&(N=sig(decodeURIComponent(N)),J.set(R,encodeURIComponent(N)));return J};
|
||||||
|
# {var H=u,k=f.sp,v=sig(decodeURIComponent(f.s));H.set(k,encodeURIComponent(v))}
|
||||||
funcname = self._search_regex(
|
funcname = self._search_regex(
|
||||||
(r'\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
|
(r'\b(?P<var>[a-zA-Z0-9_$]+)&&\((?P=var)=(?P<sig>[a-zA-Z0-9_$]{2,})\(decodeURIComponent\((?P=var)\)\)',
|
||||||
|
r'(?P<sig>[a-zA-Z0-9_$]+)\s*=\s*function\(\s*(?P<arg>[a-zA-Z0-9_$]+)\s*\)\s*{\s*(?P=arg)\s*=\s*(?P=arg)\.split\(\s*""\s*\)\s*;\s*[^}]+;\s*return\s+(?P=arg)\.join\(\s*""\s*\)',
|
||||||
|
r'(?:\b|[^a-zA-Z0-9_$])(?P<sig>[a-zA-Z0-9_$]{2,})\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)(?:;[a-zA-Z0-9_$]{2}\.[a-zA-Z0-9_$]{2}\(a,\d+\))?',
|
||||||
|
# Old patterns
|
||||||
|
r'\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
|
||||||
r'\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
|
r'\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
|
||||||
r'\bm=(?P<sig>[a-zA-Z0-9$]{2,})\(decodeURIComponent\(h\.s\)\)',
|
r'\bm=(?P<sig>[a-zA-Z0-9$]{2,})\(decodeURIComponent\(h\.s\)\)',
|
||||||
r'\bc&&\(c=(?P<sig>[a-zA-Z0-9$]{2,})\(decodeURIComponent\(c\)\)',
|
|
||||||
r'(?:\b|[^a-zA-Z0-9$])(?P<sig>[a-zA-Z0-9$]{2,})\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)(?:;[a-zA-Z0-9$]{2}\.[a-zA-Z0-9$]{2}\(a,\d+\))?',
|
|
||||||
r'(?P<sig>[a-zA-Z0-9$]+)\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)',
|
|
||||||
# Obsolete patterns
|
# Obsolete patterns
|
||||||
r'("|\')signature\1\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
|
r'("|\')signature\1\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
|
||||||
r'\.sig\|\|(?P<sig>[a-zA-Z0-9$]+)\(',
|
r'\.sig\|\|(?P<sig>[a-zA-Z0-9$]+)\(',
|
||||||
r'yt\.akamaized\.net/\)\s*\|\|\s*.*?\s*[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?:encodeURIComponent\s*\()?\s*(?P<sig>[a-zA-Z0-9$]+)\(',
|
r'yt\.akamaized\.net/\)\s*\|\|\s*.*?\s*[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?:encodeURIComponent\s*\()?\s*(?P<sig>[a-zA-Z0-9$]+)\(',
|
||||||
r'\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
|
r'\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
|
||||||
r'\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
|
|
||||||
r'\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\('),
|
r'\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\('),
|
||||||
jscode, 'Initial JS player signature function name', group='sig')
|
jscode, 'Initial JS player signature function name', group='sig')
|
||||||
|
|
||||||
@@ -3204,6 +3239,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
# * a.D&&(b="nn"[+a.D],c=a.get(b))&&(c=narray[idx](c),a.set(b,c),narray.length||nfunc("")
|
# * a.D&&(b="nn"[+a.D],c=a.get(b))&&(c=narray[idx](c),a.set(b,c),narray.length||nfunc("")
|
||||||
# * a.D&&(PL(a),b=a.j.n||null)&&(b=narray[0](b),a.set("n",b),narray.length||nfunc("")
|
# * a.D&&(PL(a),b=a.j.n||null)&&(b=narray[0](b),a.set("n",b),narray.length||nfunc("")
|
||||||
# * a.D&&(b="nn"[+a.D],vL(a),c=a.j[b]||null)&&(c=narray[idx](c),a.set(b,c),narray.length||nfunc("")
|
# * a.D&&(b="nn"[+a.D],vL(a),c=a.j[b]||null)&&(c=narray[idx](c),a.set(b,c),narray.length||nfunc("")
|
||||||
|
# * J.J="";J.url="";J.Z&&(R="nn"[+J.Z],mW(J),N=J.K[R]||null)&&(N=narray[idx](N),J.set(R,N))}};
|
||||||
funcname, idx = self._search_regex(
|
funcname, idx = self._search_regex(
|
||||||
r'''(?x)
|
r'''(?x)
|
||||||
(?:
|
(?:
|
||||||
@@ -3220,7 +3256,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
)\)&&\(c=|
|
)\)&&\(c=|
|
||||||
\b(?P<var>[a-zA-Z0-9_$]+)=
|
\b(?P<var>[a-zA-Z0-9_$]+)=
|
||||||
)(?P<nfunc>[a-zA-Z0-9_$]+)(?:\[(?P<idx>\d+)\])?\([a-zA-Z]\)
|
)(?P<nfunc>[a-zA-Z0-9_$]+)(?:\[(?P<idx>\d+)\])?\([a-zA-Z]\)
|
||||||
(?(var),[a-zA-Z0-9_$]+\.set\("n"\,(?P=var)\),(?P=nfunc)\.length)''',
|
(?(var),[a-zA-Z0-9_$]+\.set\((?:"n+"|[a-zA-Z0-9_$]+)\,(?P=var)\))''',
|
||||||
jscode, 'n function name', group=('nfunc', 'idx'), default=(None, None))
|
jscode, 'n function name', group=('nfunc', 'idx'), default=(None, None))
|
||||||
if not funcname:
|
if not funcname:
|
||||||
self.report_warning(join_nonempty(
|
self.report_warning(join_nonempty(
|
||||||
@@ -3229,7 +3265,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
return self._search_regex(
|
return self._search_regex(
|
||||||
r'''(?xs)
|
r'''(?xs)
|
||||||
;\s*(?P<name>[a-zA-Z0-9_$]+)\s*=\s*function\([a-zA-Z0-9_$]+\)
|
;\s*(?P<name>[a-zA-Z0-9_$]+)\s*=\s*function\([a-zA-Z0-9_$]+\)
|
||||||
\s*\{(?:(?!};).)+?["']enhanced_except_''',
|
\s*\{(?:(?!};).)+?return\s*(?P<q>["'])[\w-]+_w8_(?P=q)\s*\+\s*[a-zA-Z0-9_$]+''',
|
||||||
jscode, 'Initial JS player n function name', group='name')
|
jscode, 'Initial JS player n function name', group='name')
|
||||||
elif not idx:
|
elif not idx:
|
||||||
return funcname
|
return funcname
|
||||||
@@ -3238,6 +3274,11 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
rf'var {re.escape(funcname)}\s*=\s*(\[.+?\])\s*[,;]', jscode,
|
rf'var {re.escape(funcname)}\s*=\s*(\[.+?\])\s*[,;]', jscode,
|
||||||
f'Initial JS player n function list ({funcname}.{idx})')))[int(idx)]
|
f'Initial JS player n function list ({funcname}.{idx})')))[int(idx)]
|
||||||
|
|
||||||
|
def _fixup_n_function_code(self, argnames, code):
|
||||||
|
return argnames, re.sub(
|
||||||
|
rf';\s*if\s*\(\s*typeof\s+[a-zA-Z0-9_$]+\s*===?\s*(["\'])undefined\1\s*\)\s*return\s+{argnames[0]};',
|
||||||
|
';', code)
|
||||||
|
|
||||||
def _extract_n_function_code(self, video_id, player_url):
|
def _extract_n_function_code(self, video_id, player_url):
|
||||||
player_id = self._extract_player_info(player_url)
|
player_id = self._extract_player_info(player_url)
|
||||||
func_code = self.cache.load('youtube-nsig', player_id, min_ver='2024.07.09')
|
func_code = self.cache.load('youtube-nsig', player_id, min_ver='2024.07.09')
|
||||||
@@ -3249,7 +3290,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
|
|
||||||
func_name = self._extract_n_function_name(jscode, player_url=player_url)
|
func_name = self._extract_n_function_name(jscode, player_url=player_url)
|
||||||
|
|
||||||
func_code = jsi.extract_function_code(func_name)
|
# XXX: Workaround for the `typeof` gotcha
|
||||||
|
func_code = self._fixup_n_function_code(*jsi.extract_function_code(func_name))
|
||||||
|
|
||||||
self.cache.store('youtube-nsig', player_id, func_code)
|
self.cache.store('youtube-nsig', player_id, func_code)
|
||||||
return jsi, player_id, func_code
|
return jsi, player_id, func_code
|
||||||
@@ -3265,7 +3307,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise JSInterpreter.Exception(traceback.format_exc(), cause=e)
|
raise JSInterpreter.Exception(traceback.format_exc(), cause=e)
|
||||||
|
|
||||||
if ret.startswith('enhanced_except_'):
|
if ret.startswith('enhanced_except_') or ret.endswith(s):
|
||||||
raise JSInterpreter.Exception('Signature function returned an exception')
|
raise JSInterpreter.Exception('Signature function returned an exception')
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -3929,13 +3971,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
)
|
)
|
||||||
|
|
||||||
require_po_token = self._get_default_ytcfg(client).get('REQUIRE_PO_TOKEN')
|
require_po_token = self._get_default_ytcfg(client).get('REQUIRE_PO_TOKEN')
|
||||||
if not po_token and require_po_token:
|
if not po_token and require_po_token and 'missing_pot' in self._configuration_arg('formats'):
|
||||||
self.report_warning(
|
self.report_warning(
|
||||||
f'No PO Token provided for {client} client, '
|
f'No PO Token provided for {client} client, '
|
||||||
f'which is required for working {client} formats. '
|
f'which may be required for working {client} formats. This client will be deprioritized', only_once=True)
|
||||||
f'You can manually pass a PO Token for this client with '
|
|
||||||
f'--extractor-args "youtube:po_token={client}+XXX"',
|
|
||||||
only_once=True)
|
|
||||||
deprioritize_pr = True
|
deprioritize_pr = True
|
||||||
|
|
||||||
pr = initial_pr if client == 'web' else None
|
pr = initial_pr if client == 'web' else None
|
||||||
@@ -3968,15 +4007,24 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
else:
|
else:
|
||||||
prs.append(pr)
|
prs.append(pr)
|
||||||
|
|
||||||
|
# web_embedded can work around age-gate and age-verification for some embeddable videos
|
||||||
|
if self._is_agegated(pr) and variant != 'web_embedded':
|
||||||
|
append_client(f'web_embedded.{base_client}')
|
||||||
|
# Unauthenticated users will only get web_embedded client formats if age-gated
|
||||||
|
if self._is_agegated(pr) and not self.is_authenticated:
|
||||||
|
self.to_screen(
|
||||||
|
f'{video_id}: This video is age-restricted; some formats may be missing '
|
||||||
|
f'without authentication. {self._login_hint()}', only_once=True)
|
||||||
|
|
||||||
''' This code is pointless while web_creator is in _DEFAULT_AUTHED_CLIENTS
|
''' This code is pointless while web_creator is in _DEFAULT_AUTHED_CLIENTS
|
||||||
# EU countries require age-verification for accounts to access age-restricted videos
|
# EU countries require age-verification for accounts to access age-restricted videos
|
||||||
# If account is not age-verified, _is_agegated() will be truthy for non-embedded clients
|
# If account is not age-verified, _is_agegated() will be truthy for non-embedded clients
|
||||||
if self.is_authenticated and self._is_agegated(pr):
|
embedding_is_disabled = variant == 'web_embedded' and self._is_unplayable(pr)
|
||||||
|
if self.is_authenticated and (self._is_agegated(pr) or embedding_is_disabled):
|
||||||
self.to_screen(
|
self.to_screen(
|
||||||
f'{video_id}: This video is age-restricted and YouTube is requiring '
|
f'{video_id}: This video is age-restricted and YouTube is requiring '
|
||||||
'account age-verification; some formats may be missing', only_once=True)
|
'account age-verification; some formats may be missing', only_once=True)
|
||||||
# web_creator can work around the age-verification requirement
|
# web_creator can work around the age-verification requirement
|
||||||
# android_vr may also be able to work around age-verification
|
|
||||||
# tv_embedded may(?) still work around age-verification if the video is embeddable
|
# tv_embedded may(?) still work around age-verification if the video is embeddable
|
||||||
append_client('web_creator')
|
append_client('web_creator')
|
||||||
'''
|
'''
|
||||||
@@ -3999,6 +4047,21 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
or (live_status == 'post_live' and (duration or 0) > 2 * 3600)):
|
or (live_status == 'post_live' and (duration or 0) > 2 * 3600)):
|
||||||
return live_status
|
return live_status
|
||||||
|
|
||||||
|
def _report_pot_format_skipped(self, video_id, client_name, proto):
|
||||||
|
msg = (
|
||||||
|
f'{video_id}: {client_name} client {proto} formats require a PO Token which was not provided. '
|
||||||
|
'They will be skipped as they may yield HTTP Error 403. '
|
||||||
|
f'You can manually pass a PO Token for this client with --extractor-args "youtube:po_token={client_name}+XXX. '
|
||||||
|
'For more information, refer to https://github.com/yt-dlp/yt-dlp/wiki/Extractors#po-token-guide . '
|
||||||
|
'To enable these broken formats anyway, pass --extractor-args "youtube:formats=missing_pot"')
|
||||||
|
|
||||||
|
# Only raise a warning for non-default clients, to not confuse users.
|
||||||
|
# iOS HLS formats still work without PO Token, so we don't need to warn about them.
|
||||||
|
if client_name in (*self._DEFAULT_CLIENTS, *self._DEFAULT_AUTHED_CLIENTS):
|
||||||
|
self.write_debug(msg, only_once=True)
|
||||||
|
else:
|
||||||
|
self.report_warning(msg, only_once=True)
|
||||||
|
|
||||||
def _extract_formats_and_subtitles(self, streaming_data, video_id, player_url, live_status, duration):
|
def _extract_formats_and_subtitles(self, streaming_data, video_id, player_url, live_status, duration):
|
||||||
CHUNK_SIZE = 10 << 20
|
CHUNK_SIZE = 10 << 20
|
||||||
PREFERRED_LANG_VALUE = 10
|
PREFERRED_LANG_VALUE = 10
|
||||||
@@ -4052,10 +4115,12 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
if height:
|
if height:
|
||||||
res_qualities[height] = quality
|
res_qualities[height] = quality
|
||||||
|
|
||||||
|
display_name = audio_track.get('displayName') or ''
|
||||||
|
is_original = 'original' in display_name.lower()
|
||||||
|
is_descriptive = 'descriptive' in display_name.lower()
|
||||||
is_default = audio_track.get('audioIsDefault')
|
is_default = audio_track.get('audioIsDefault')
|
||||||
is_descriptive = 'descriptive' in (audio_track.get('displayName') or '').lower()
|
|
||||||
language_code = audio_track.get('id', '').split('.')[0]
|
language_code = audio_track.get('id', '').split('.')[0]
|
||||||
if language_code and is_default:
|
if language_code and (is_original or (is_default and not original_language)):
|
||||||
original_language = language_code
|
original_language = language_code
|
||||||
|
|
||||||
# FORMAT_STREAM_TYPE_OTF(otf=1) requires downloading the init fragment
|
# FORMAT_STREAM_TYPE_OTF(otf=1) requires downloading the init fragment
|
||||||
@@ -4123,11 +4188,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
fmt_url = update_url_query(fmt_url, {'pot': po_token})
|
fmt_url = update_url_query(fmt_url, {'pot': po_token})
|
||||||
|
|
||||||
# Clients that require PO Token return videoplayback URLs that may return 403
|
# Clients that require PO Token return videoplayback URLs that may return 403
|
||||||
is_broken = (not po_token and self._get_default_ytcfg(client_name).get('REQUIRE_PO_TOKEN'))
|
require_po_token = (not po_token and self._get_default_ytcfg(client_name).get('REQUIRE_PO_TOKEN'))
|
||||||
if is_broken:
|
if require_po_token and 'missing_pot' not in self._configuration_arg('formats'):
|
||||||
self.report_warning(
|
self._report_pot_format_skipped(video_id, client_name, 'https')
|
||||||
f'{video_id}: {client_name} client formats require a PO Token which was not provided. '
|
continue
|
||||||
'They will be deprioritized as they may yield HTTP Error 403', only_once=True)
|
|
||||||
|
|
||||||
name = fmt.get('qualityLabel') or quality.replace('audio_quality_', '') or ''
|
name = fmt.get('qualityLabel') or quality.replace('audio_quality_', '') or ''
|
||||||
fps = int_or_none(fmt.get('fps')) or 0
|
fps = int_or_none(fmt.get('fps')) or 0
|
||||||
@@ -4136,11 +4200,11 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
'filesize': int_or_none(fmt.get('contentLength')),
|
'filesize': int_or_none(fmt.get('contentLength')),
|
||||||
'format_id': f'{itag}{"-drc" if fmt.get("isDrc") else ""}',
|
'format_id': f'{itag}{"-drc" if fmt.get("isDrc") else ""}',
|
||||||
'format_note': join_nonempty(
|
'format_note': join_nonempty(
|
||||||
join_nonempty(audio_track.get('displayName'), is_default and ' (default)', delim=''),
|
join_nonempty(display_name, is_default and ' (default)', delim=''),
|
||||||
name, fmt.get('isDrc') and 'DRC',
|
name, fmt.get('isDrc') and 'DRC',
|
||||||
try_get(fmt, lambda x: x['projectionType'].replace('RECTANGULAR', '').lower()),
|
try_get(fmt, lambda x: x['projectionType'].replace('RECTANGULAR', '').lower()),
|
||||||
try_get(fmt, lambda x: x['spatialAudioType'].replace('SPATIAL_AUDIO_TYPE_', '').lower()),
|
try_get(fmt, lambda x: x['spatialAudioType'].replace('SPATIAL_AUDIO_TYPE_', '').lower()),
|
||||||
is_damaged and 'DAMAGED', is_broken and 'BROKEN',
|
is_damaged and 'DAMAGED', require_po_token and 'MISSING POT',
|
||||||
(self.get_param('verbose') or all_formats) and short_client_name(client_name),
|
(self.get_param('verbose') or all_formats) and short_client_name(client_name),
|
||||||
delim=', '),
|
delim=', '),
|
||||||
# Format 22 is likely to be damaged. See https://github.com/yt-dlp/yt-dlp/issues/3372
|
# Format 22 is likely to be damaged. See https://github.com/yt-dlp/yt-dlp/issues/3372
|
||||||
@@ -4155,9 +4219,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
'url': fmt_url,
|
'url': fmt_url,
|
||||||
'width': int_or_none(fmt.get('width')),
|
'width': int_or_none(fmt.get('width')),
|
||||||
'language': join_nonempty(language_code, 'desc' if is_descriptive else '') or None,
|
'language': join_nonempty(language_code, 'desc' if is_descriptive else '') or None,
|
||||||
'language_preference': PREFERRED_LANG_VALUE if is_default else -10 if is_descriptive else -1,
|
'language_preference': PREFERRED_LANG_VALUE if is_original else 5 if is_default else -10 if is_descriptive else -1,
|
||||||
# Strictly de-prioritize broken, damaged and 3gp formats
|
# Strictly de-prioritize broken, damaged and 3gp formats
|
||||||
'preference': -20 if is_broken else -10 if is_damaged else -2 if itag == '17' else None,
|
'preference': -20 if require_po_token else -10 if is_damaged else -2 if itag == '17' else None,
|
||||||
}
|
}
|
||||||
mime_mobj = re.match(
|
mime_mobj = re.match(
|
||||||
r'((?:[^/]+)/(?:[^;]+))(?:;\s*codecs="([^"]+)")?', fmt.get('mimeType') or '')
|
r'((?:[^/]+)/(?:[^;]+))(?:;\s*codecs="([^"]+)")?', fmt.get('mimeType') or '')
|
||||||
@@ -4215,10 +4279,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
# Clients that require PO Token return videoplayback URLs that may return 403
|
# Clients that require PO Token return videoplayback URLs that may return 403
|
||||||
# hls does not currently require PO Token
|
# hls does not currently require PO Token
|
||||||
if (not po_token and self._get_default_ytcfg(client_name).get('REQUIRE_PO_TOKEN')) and proto != 'hls':
|
if (not po_token and self._get_default_ytcfg(client_name).get('REQUIRE_PO_TOKEN')) and proto != 'hls':
|
||||||
self.report_warning(
|
if 'missing_pot' not in self._configuration_arg('formats'):
|
||||||
f'{video_id}: {client_name} client {proto} formats require a PO Token which was not provided. '
|
self._report_pot_format_skipped(video_id, client_name, proto)
|
||||||
'They will be deprioritized as they may yield HTTP Error 403', only_once=True)
|
return False
|
||||||
f['format_note'] = join_nonempty(f.get('format_note'), 'BROKEN', delim=' ')
|
f['format_note'] = join_nonempty(f.get('format_note'), 'MISSING POT', delim=' ')
|
||||||
f['source_preference'] -= 20
|
f['source_preference'] -= 20
|
||||||
|
|
||||||
if itag and all_formats:
|
if itag and all_formats:
|
||||||
@@ -4674,7 +4738,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
(?=(?P<artist>[^\n]+))(?P=artist)\n+
|
(?=(?P<artist>[^\n]+))(?P=artist)\n+
|
||||||
(?=(?P<album>[^\n]+))(?P=album)\n
|
(?=(?P<album>[^\n]+))(?P=album)\n
|
||||||
(?:.+?℗\s*(?P<release_year>\d{4})(?!\d))?
|
(?:.+?℗\s*(?P<release_year>\d{4})(?!\d))?
|
||||||
(?:.+?Released on\s*:\s*(?P<release_date>\d{4}-\d{2}-\d{2}))?
|
(?:.+?Released\ on\s*:\s*(?P<release_date>\d{4}-\d{2}-\d{2}))?
|
||||||
(.+?\nArtist\s*:\s*
|
(.+?\nArtist\s*:\s*
|
||||||
(?=(?P<clean_artist>[^\n]+))(?P=clean_artist)\n
|
(?=(?P<clean_artist>[^\n]+))(?P=clean_artist)\n
|
||||||
)?.+\nAuto-generated\ by\ YouTube\.\s*$
|
)?.+\nAuto-generated\ by\ YouTube\.\s*$
|
||||||
@@ -5267,6 +5331,7 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor):
|
|||||||
'channelRenderer': lambda x: self._grid_entries({'items': [{'channelRenderer': x}]}),
|
'channelRenderer': lambda x: self._grid_entries({'items': [{'channelRenderer': x}]}),
|
||||||
'hashtagTileRenderer': lambda x: [self._hashtag_tile_entry(x)],
|
'hashtagTileRenderer': lambda x: [self._hashtag_tile_entry(x)],
|
||||||
'richGridRenderer': lambda x: self._extract_entries(x, continuation_list),
|
'richGridRenderer': lambda x: self._extract_entries(x, continuation_list),
|
||||||
|
'lockupViewModel': lambda x: [self._extract_lockup_view_model(x)],
|
||||||
}
|
}
|
||||||
for key, renderer in isr_content.items():
|
for key, renderer in isr_content.items():
|
||||||
if key not in known_renderers:
|
if key not in known_renderers:
|
||||||
|
|||||||
@@ -1370,12 +1370,12 @@ def create_parser():
|
|||||||
help='Allow Unicode characters, "&" and spaces in filenames (default)')
|
help='Allow Unicode characters, "&" and spaces in filenames (default)')
|
||||||
filesystem.add_option(
|
filesystem.add_option(
|
||||||
'--windows-filenames',
|
'--windows-filenames',
|
||||||
action='store_true', dest='windowsfilenames', default=False,
|
action='store_true', dest='windowsfilenames', default=None,
|
||||||
help='Force filenames to be Windows-compatible')
|
help='Force filenames to be Windows-compatible')
|
||||||
filesystem.add_option(
|
filesystem.add_option(
|
||||||
'--no-windows-filenames',
|
'--no-windows-filenames',
|
||||||
action='store_false', dest='windowsfilenames',
|
action='store_false', dest='windowsfilenames',
|
||||||
help='Make filenames Windows-compatible only if using Windows (default)')
|
help='Sanitize filenames only minimally')
|
||||||
filesystem.add_option(
|
filesystem.add_option(
|
||||||
'--trim-filenames', '--trim-file-names', metavar='LENGTH',
|
'--trim-filenames', '--trim-file-names', metavar='LENGTH',
|
||||||
dest='trim_file_name', default=0, type=int,
|
dest='trim_file_name', default=0, type=int,
|
||||||
|
|||||||
@@ -65,9 +65,14 @@ def _get_variant_and_executable_path():
|
|||||||
machine = '_legacy' if version_tuple(platform.mac_ver()[0]) < (10, 15) else ''
|
machine = '_legacy' if version_tuple(platform.mac_ver()[0]) < (10, 15) else ''
|
||||||
else:
|
else:
|
||||||
machine = f'_{platform.machine().lower()}'
|
machine = f'_{platform.machine().lower()}'
|
||||||
|
is_64bits = sys.maxsize > 2**32
|
||||||
# Ref: https://en.wikipedia.org/wiki/Uname#Examples
|
# Ref: https://en.wikipedia.org/wiki/Uname#Examples
|
||||||
if machine[1:] in ('x86', 'x86_64', 'amd64', 'i386', 'i686'):
|
if machine[1:] in ('x86', 'x86_64', 'amd64', 'i386', 'i686'):
|
||||||
machine = '_x86' if platform.architecture()[0][:2] == '32' else ''
|
machine = '_x86' if not is_64bits else ''
|
||||||
|
# platform.machine() on 32-bit raspbian OS may return 'aarch64', so check "64-bitness"
|
||||||
|
# See: https://github.com/yt-dlp/yt-dlp/issues/11813
|
||||||
|
elif machine[1:] == 'aarch64' and not is_64bits:
|
||||||
|
machine = '_armv7l'
|
||||||
# sys.executable returns a /tmp/ path for staticx builds (linux_static)
|
# sys.executable returns a /tmp/ path for staticx builds (linux_static)
|
||||||
# Ref: https://staticx.readthedocs.io/en/latest/usage.html#run-time-information
|
# Ref: https://staticx.readthedocs.io/en/latest/usage.html#run-time-information
|
||||||
if static_exe_path := os.getenv('STATICX_PROG_PATH'):
|
if static_exe_path := os.getenv('STATICX_PROG_PATH'):
|
||||||
@@ -525,11 +530,16 @@ class Updater:
|
|||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
def cmd(self):
|
def cmd(self):
|
||||||
"""The command-line to run the executable, if known"""
|
"""The command-line to run the executable, if known"""
|
||||||
|
argv = None
|
||||||
# There is no sys.orig_argv in py < 3.10. Also, it can be [] when frozen
|
# There is no sys.orig_argv in py < 3.10. Also, it can be [] when frozen
|
||||||
if getattr(sys, 'orig_argv', None):
|
if getattr(sys, 'orig_argv', None):
|
||||||
return sys.orig_argv
|
argv = sys.orig_argv
|
||||||
elif getattr(sys, 'frozen', False):
|
elif getattr(sys, 'frozen', False):
|
||||||
return sys.argv
|
argv = sys.argv
|
||||||
|
# linux_static exe's argv[0] will be /tmp/staticx-NNNN/yt-dlp_linux if we don't fixup here
|
||||||
|
if argv and os.getenv('STATICX_PROG_PATH'):
|
||||||
|
argv = [self.filename, *argv[1:]]
|
||||||
|
return argv
|
||||||
|
|
||||||
def restart(self):
|
def restart(self):
|
||||||
"""Restart the executable"""
|
"""Restart the executable"""
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Autogenerated by devscripts/update-version.py
|
# Autogenerated by devscripts/update-version.py
|
||||||
|
|
||||||
__version__ = '2024.12.03'
|
__version__ = '2024.12.23'
|
||||||
|
|
||||||
RELEASE_GIT_HEAD = '2b67ac300ac8b44368fb121637d1743cea8c5b6b'
|
RELEASE_GIT_HEAD = '65cf46cddd873fd229dbb0fc0689bca4c201c6b6'
|
||||||
|
|
||||||
VARIANT = None
|
VARIANT = None
|
||||||
|
|
||||||
@@ -12,4 +12,4 @@ CHANNEL = 'stable'
|
|||||||
|
|
||||||
ORIGIN = 'yt-dlp/yt-dlp'
|
ORIGIN = 'yt-dlp/yt-dlp'
|
||||||
|
|
||||||
_pkg_version = '2024.12.03'
|
_pkg_version = '2024.12.23'
|
||||||
|
|||||||
Reference in New Issue
Block a user