mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2025-12-17 03:12:24 +01:00
Compare commits
17 Commits
2021.12.25
...
2021.12.27
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e5c83b26b | ||
|
|
6223f67a8c | ||
|
|
6a34813a0d | ||
|
|
f59f5ef8b6 | ||
|
|
f44afb54ef | ||
|
|
77cee0f188 | ||
|
|
6a17677577 | ||
|
|
ee7b9bdf5d | ||
|
|
185bf31070 | ||
|
|
0b77924a38 | ||
|
|
8126298c1b | ||
|
|
6da22e7d4f | ||
|
|
c62ecf0d90 | ||
|
|
3774f4f427 | ||
|
|
9980d3d213 | ||
|
|
8eb4b1bb8e | ||
|
|
332da56f52 |
6
.github/ISSUE_TEMPLATE/1_broken_site.yml
vendored
6
.github/ISSUE_TEMPLATE/1_broken_site.yml
vendored
@@ -11,7 +11,7 @@ body:
|
||||
options:
|
||||
- label: I'm reporting a broken site
|
||||
required: true
|
||||
- label: I've verified that I'm running yt-dlp version **2021.12.25**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||
- label: I've verified that I'm running yt-dlp version **2021.12.27**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||
required: true
|
||||
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||
required: true
|
||||
@@ -51,12 +51,12 @@ body:
|
||||
[debug] Portable config file: yt-dlp.conf
|
||||
[debug] Portable config: ['-i']
|
||||
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
||||
[debug] yt-dlp version 2021.12.25 (exe)
|
||||
[debug] yt-dlp version 2021.12.27 (exe)
|
||||
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
||||
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
||||
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
||||
[debug] Proxy map: {}
|
||||
yt-dlp is up to date (2021.12.25)
|
||||
yt-dlp is up to date (2021.12.27)
|
||||
<more lines>
|
||||
render: shell
|
||||
validations:
|
||||
|
||||
@@ -11,7 +11,7 @@ body:
|
||||
options:
|
||||
- label: I'm reporting a new site support request
|
||||
required: true
|
||||
- label: I've verified that I'm running yt-dlp version **2021.12.25**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||
- label: I've verified that I'm running yt-dlp version **2021.12.27**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||
required: true
|
||||
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||
required: true
|
||||
@@ -62,12 +62,12 @@ body:
|
||||
[debug] Portable config file: yt-dlp.conf
|
||||
[debug] Portable config: ['-i']
|
||||
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
||||
[debug] yt-dlp version 2021.12.25 (exe)
|
||||
[debug] yt-dlp version 2021.12.27 (exe)
|
||||
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
||||
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
||||
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
||||
[debug] Proxy map: {}
|
||||
yt-dlp is up to date (2021.12.25)
|
||||
yt-dlp is up to date (2021.12.27)
|
||||
<more lines>
|
||||
render: shell
|
||||
validations:
|
||||
|
||||
@@ -11,7 +11,7 @@ body:
|
||||
options:
|
||||
- label: I'm reporting a site feature request
|
||||
required: true
|
||||
- label: I've verified that I'm running yt-dlp version **2021.12.25**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||
- label: I've verified that I'm running yt-dlp version **2021.12.27**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||
required: true
|
||||
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||
required: true
|
||||
@@ -60,12 +60,12 @@ body:
|
||||
[debug] Portable config file: yt-dlp.conf
|
||||
[debug] Portable config: ['-i']
|
||||
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
||||
[debug] yt-dlp version 2021.12.25 (exe)
|
||||
[debug] yt-dlp version 2021.12.27 (exe)
|
||||
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
||||
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
||||
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
||||
[debug] Proxy map: {}
|
||||
yt-dlp is up to date (2021.12.25)
|
||||
yt-dlp is up to date (2021.12.27)
|
||||
<more lines>
|
||||
render: shell
|
||||
validations:
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/4_bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/4_bug_report.yml
vendored
@@ -11,7 +11,7 @@ body:
|
||||
options:
|
||||
- label: I'm reporting a bug unrelated to a specific site
|
||||
required: true
|
||||
- label: I've verified that I'm running yt-dlp version **2021.12.25**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||
- label: I've verified that I'm running yt-dlp version **2021.12.27**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||
required: true
|
||||
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||
required: true
|
||||
@@ -45,12 +45,12 @@ body:
|
||||
[debug] Portable config file: yt-dlp.conf
|
||||
[debug] Portable config: ['-i']
|
||||
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
||||
[debug] yt-dlp version 2021.12.25 (exe)
|
||||
[debug] yt-dlp version 2021.12.27 (exe)
|
||||
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
||||
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
||||
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
||||
[debug] Proxy map: {}
|
||||
yt-dlp is up to date (2021.12.25)
|
||||
yt-dlp is up to date (2021.12.27)
|
||||
<more lines>
|
||||
render: shell
|
||||
validations:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
@@ -11,7 +11,7 @@ body:
|
||||
options:
|
||||
- label: I'm reporting a feature request
|
||||
required: true
|
||||
- label: I've verified that I'm running yt-dlp version **2021.12.25**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||
- label: I've verified that I'm running yt-dlp version **2021.12.27**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||
required: true
|
||||
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
|
||||
required: true
|
||||
|
||||
@@ -177,3 +177,4 @@ Sematre
|
||||
jaller94
|
||||
r5d
|
||||
julien-hadleyjack
|
||||
git-anony-mouse
|
||||
|
||||
20
Changelog.md
20
Changelog.md
@@ -11,6 +11,24 @@
|
||||
-->
|
||||
|
||||
|
||||
### 2021.12.27
|
||||
|
||||
* Avoid recursion error when re-extracting info
|
||||
* [ffmpeg] Fix position of `--ppa`
|
||||
* [aria2c] Don't show progress when `--no-progress`
|
||||
* [cookies] Support other keyrings by [mbway](https://github.com/mbway)
|
||||
* [EmbedThumbnail] Prefer AtomicParsley over ffmpeg if available
|
||||
* [generic] Fix HTTP KVS Player by [git-anony-mouse](https://github.com/git-anony-mouse)
|
||||
* [ThumbnailsConvertor] Fix for when there are no thumbnails
|
||||
* [docs] Add examples for using `TYPES:` in `-P`/`-o`
|
||||
* [PixivSketch] Add extractors by [nao20010128nao](https://github.com/nao20010128nao)
|
||||
* [tiktok] Add music, sticker and tag IEs by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||
* [BiliIntl] Fix extractor by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||
* [CBC] Fix URL regex
|
||||
* [tiktok] Fix `extractor_key` used in archive
|
||||
* [youtube] **End `live-from-start` properly when stream ends with 403**
|
||||
* [Zee5] Fix VALID_URL for tv-shows by [Ashish0804](https://github.com/Ashish0804)
|
||||
|
||||
### 2021.12.25
|
||||
|
||||
* [dash,youtube] **Download live from start to end** by [nao20010128nao](https://github.com/nao20010128nao), [pukkandan](https://github.com/pukkandan)
|
||||
@@ -104,7 +122,7 @@
|
||||
* [youtube:comments] Add more options for limiting number of comments extracted by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [youtube:tab] Extract more metadata from feeds/channels/playlists by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [youtube:tab] Extract video thumbnails from playlist by [coletdjnz](https://github.com/coletdjnz), [pukkandan](https://github.com/pukkandan)
|
||||
* [youtube:tab] Ignore query when redirecting channel to playlist and cleanup of related code Closes #2046
|
||||
* [youtube:tab] Ignore query when redirecting channel to playlist and cleanup of related code
|
||||
* [youtube] Fix `ytsearchdate`
|
||||
* [zdf] Support videos with different ptmd location by [iw0nderhow](https://github.com/iw0nderhow)
|
||||
* [zee5] Support /episodes in URL
|
||||
|
||||
38
README.md
38
README.md
@@ -90,7 +90,7 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
|
||||
* Youtube music Albums, channels etc can be downloaded ([except self-uploaded music](https://github.com/yt-dlp/yt-dlp/issues/723))
|
||||
* Download livestreams from the start using `--live-from-start`
|
||||
|
||||
* **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[:PROFILE]`
|
||||
* **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[+KEYRING][:PROFILE]`
|
||||
|
||||
* **Split video by chapters**: Videos can be split into multiple files based on chapters using `--split-chapters`
|
||||
|
||||
@@ -255,7 +255,7 @@ While all the other dependencies are optional, `ffmpeg` and `ffprobe` are highly
|
||||
* [**mutagen**](https://github.com/quodlibet/mutagen) - For embedding thumbnail in certain formats. Licensed under [GPLv2+](https://github.com/quodlibet/mutagen/blob/master/COPYING)
|
||||
* [**pycryptodomex**](https://github.com/Legrandin/pycryptodome) - For decrypting AES-128 HLS streams and various other data. Licensed under [BSD2](https://github.com/Legrandin/pycryptodome/blob/master/LICENSE.rst)
|
||||
* [**websockets**](https://github.com/aaugustin/websockets) - For downloading over websocket. Licensed under [BSD3](https://github.com/aaugustin/websockets/blob/main/LICENSE)
|
||||
* [**keyring**](https://github.com/jaraco/keyring) - For decrypting cookies of chromium-based browsers on Linux. Licensed under [MIT](https://github.com/jaraco/keyring/blob/main/LICENSE)
|
||||
* [**secretstorage**](https://github.com/mitya57/secretstorage) - For accessing the Gnome keyring while decrypting cookies of Chromium-based browsers on Linux. Licensed under [BSD](https://github.com/mitya57/secretstorage/blob/master/LICENSE)
|
||||
* [**AtomicParsley**](https://github.com/wez/atomicparsley) - For embedding thumbnail in mp4/m4a if mutagen is not present. Licensed under [GPLv2+](https://github.com/wez/atomicparsley/blob/master/COPYING)
|
||||
* [**rtmpdump**](http://rtmpdump.mplayerhq.hu) - For downloading `rtmp` streams. ffmpeg will be used as a fallback. Licensed under [GPLv2+](http://rtmpdump.mplayerhq.hu)
|
||||
* [**mplayer**](http://mplayerhq.hu/design7/info.html) or [**mpv**](https://mpv.io) - For downloading `rstp` streams. ffmpeg will be used as a fallback. Licensed under [GPLv2+](https://github.com/mpv-player/mpv/blob/master/Copyright)
|
||||
@@ -535,7 +535,7 @@ You can also fork the project on github and run your fork's [build workflow](.gi
|
||||
-P, --paths [TYPES:]PATH The paths where the files should be
|
||||
downloaded. Specify the type of file and
|
||||
the path separated by a colon ":". All the
|
||||
same types as --output are supported.
|
||||
same TYPES as --output are supported.
|
||||
Additionally, you can also provide "home"
|
||||
(default) and "temp" paths. All
|
||||
intermediary files are first downloaded to
|
||||
@@ -607,16 +607,19 @@ You can also fork the project on github and run your fork's [build workflow](.gi
|
||||
from and dump cookie jar in
|
||||
--no-cookies Do not read/dump cookies from/to file
|
||||
(default)
|
||||
--cookies-from-browser BROWSER[:PROFILE]
|
||||
Load cookies from a user profile of the
|
||||
given web browser. Currently supported
|
||||
browsers are: brave, chrome, chromium,
|
||||
edge, firefox, opera, safari, vivaldi. You
|
||||
can specify the user profile name or
|
||||
directory using "BROWSER:PROFILE_NAME" or
|
||||
"BROWSER:PROFILE_PATH". If no profile is
|
||||
given, the most recently accessed one is
|
||||
used
|
||||
--cookies-from-browser BROWSER[+KEYRING][:PROFILE]
|
||||
The name of the browser and (optionally)
|
||||
the name/path of the profile to load
|
||||
cookies from, separated by a ":". Currently
|
||||
supported browsers are: brave, chrome,
|
||||
chromium, edge, firefox, opera, safari,
|
||||
vivaldi. By default, the most recently
|
||||
accessed profile is used. The keyring used
|
||||
for decrypting Chromium cookies on Linux
|
||||
can be (optionally) specified after the
|
||||
browser name separated by a "+". Currently
|
||||
supported keyrings are: basictext,
|
||||
gnomekeyring, kwallet
|
||||
--no-cookies-from-browser Do not load cookies from browser (default)
|
||||
--cache-dir DIR Location in the filesystem where youtube-dl
|
||||
can store some downloaded information (such
|
||||
@@ -1260,7 +1263,7 @@ $ yt-dlp -o "%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s" "https://www.y
|
||||
$ yt-dlp -o "%(upload_date>%Y)s/%(title)s.%(ext)s" "https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re"
|
||||
|
||||
# Prefix playlist index with " - " separator, but only if it is available
|
||||
$ yt-dlp -o '%(playlist_index|)s%(playlist_index& - |)s%(title)s.%(ext)s' BaW_jenozKc https://www.youtube.com/user/TheLinuxFoundation/playlists
|
||||
$ yt-dlp -o '%(playlist_index|)s%(playlist_index& - |)s%(title)s.%(ext)s' BaW_jenozKc "https://www.youtube.com/user/TheLinuxFoundation/playlists"
|
||||
|
||||
# Download all playlists of YouTube channel/user keeping each playlist in separate directory:
|
||||
$ yt-dlp -o "%(uploader)s/%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s" "https://www.youtube.com/user/TheLinuxFoundation/playlists"
|
||||
@@ -1271,6 +1274,13 @@ $ yt-dlp -u user -p password -P "~/MyVideos" -o "%(playlist)s/%(chapter_number)s
|
||||
# Download entire series season keeping each series and each season in separate directory under C:/MyVideos
|
||||
$ yt-dlp -P "C:/MyVideos" -o "%(series)s/%(season_number)s - %(season)s/%(episode_number)s - %(episode)s.%(ext)s" "https://videomore.ru/kino_v_detalayah/5_sezon/367617"
|
||||
|
||||
# Download video as "C:\MyVideos\uploader\title.ext", subtitles as "C:\MyVideos\subs\uploader\title.ext"
|
||||
# and put all temporary files in "C:\MyVideos\tmp"
|
||||
$ yt-dlp -P "C:/MyVideos" -P "temp:tmp" -P "subtitle:subs" -o "%(uploader)s/%(title)s.%(ext)s" BaW_jenoz --write-subs
|
||||
|
||||
# Download video as "C:\MyVideos\uploader\title.ext" and subtitles as "C:\MyVideos\uploader\subs\title.ext"
|
||||
$ yt-dlp -P "C:/MyVideos" -o "%(uploader)s/%(title)s.%(ext)s" -o "subtitle:%(uploader)s/subs/%(title)s.%(ext)s" BaW_jenozKc --write-subs
|
||||
|
||||
# Stream the video being downloaded to stdout
|
||||
$ yt-dlp -o - BaW_jenozKc
|
||||
```
|
||||
|
||||
@@ -836,6 +836,8 @@
|
||||
- **Pinkbike**
|
||||
- **Pinterest**
|
||||
- **PinterestCollection**
|
||||
- **pixiv:sketch**
|
||||
- **pixiv:sketch:user**
|
||||
- **Pladform**
|
||||
- **PlanetMarathi**
|
||||
- **Platzi**
|
||||
@@ -1141,6 +1143,9 @@
|
||||
- **ThreeSpeak**
|
||||
- **ThreeSpeakUser**
|
||||
- **TikTok**
|
||||
- **tiktok:effect**
|
||||
- **tiktok:sound**
|
||||
- **tiktok:tag**
|
||||
- **tiktok:user**
|
||||
- **tinypic**: tinypic.com videos
|
||||
- **TMZ**
|
||||
|
||||
@@ -8,6 +8,8 @@ from yt_dlp.cookies import (
|
||||
WindowsChromeCookieDecryptor,
|
||||
parse_safari_cookies,
|
||||
pbkdf2_sha1,
|
||||
_get_linux_desktop_environment,
|
||||
_LinuxDesktopEnvironment,
|
||||
)
|
||||
|
||||
|
||||
@@ -42,6 +44,37 @@ class MonkeyPatch:
|
||||
|
||||
|
||||
class TestCookies(unittest.TestCase):
|
||||
def test_get_desktop_environment(self):
|
||||
""" based on https://chromium.googlesource.com/chromium/src/+/refs/heads/main/base/nix/xdg_util_unittest.cc """
|
||||
test_cases = [
|
||||
({}, _LinuxDesktopEnvironment.OTHER),
|
||||
|
||||
({'DESKTOP_SESSION': 'gnome'}, _LinuxDesktopEnvironment.GNOME),
|
||||
({'DESKTOP_SESSION': 'mate'}, _LinuxDesktopEnvironment.GNOME),
|
||||
({'DESKTOP_SESSION': 'kde4'}, _LinuxDesktopEnvironment.KDE),
|
||||
({'DESKTOP_SESSION': 'kde'}, _LinuxDesktopEnvironment.KDE),
|
||||
({'DESKTOP_SESSION': 'xfce'}, _LinuxDesktopEnvironment.XFCE),
|
||||
|
||||
({'GNOME_DESKTOP_SESSION_ID': 1}, _LinuxDesktopEnvironment.GNOME),
|
||||
({'KDE_FULL_SESSION': 1}, _LinuxDesktopEnvironment.KDE),
|
||||
|
||||
({'XDG_CURRENT_DESKTOP': 'X-Cinnamon'}, _LinuxDesktopEnvironment.CINNAMON),
|
||||
({'XDG_CURRENT_DESKTOP': 'GNOME'}, _LinuxDesktopEnvironment.GNOME),
|
||||
({'XDG_CURRENT_DESKTOP': 'GNOME:GNOME-Classic'}, _LinuxDesktopEnvironment.GNOME),
|
||||
({'XDG_CURRENT_DESKTOP': 'GNOME : GNOME-Classic'}, _LinuxDesktopEnvironment.GNOME),
|
||||
|
||||
({'XDG_CURRENT_DESKTOP': 'Unity', 'DESKTOP_SESSION': 'gnome-fallback'}, _LinuxDesktopEnvironment.GNOME),
|
||||
({'XDG_CURRENT_DESKTOP': 'KDE', 'KDE_SESSION_VERSION': '5'}, _LinuxDesktopEnvironment.KDE),
|
||||
({'XDG_CURRENT_DESKTOP': 'KDE'}, _LinuxDesktopEnvironment.KDE),
|
||||
({'XDG_CURRENT_DESKTOP': 'Pantheon'}, _LinuxDesktopEnvironment.PANTHEON),
|
||||
({'XDG_CURRENT_DESKTOP': 'Unity'}, _LinuxDesktopEnvironment.UNITY),
|
||||
({'XDG_CURRENT_DESKTOP': 'Unity:Unity7'}, _LinuxDesktopEnvironment.UNITY),
|
||||
({'XDG_CURRENT_DESKTOP': 'Unity:Unity8'}, _LinuxDesktopEnvironment.UNITY),
|
||||
]
|
||||
|
||||
for env, expected_desktop_environment in test_cases:
|
||||
self.assertEqual(_get_linux_desktop_environment(env), expected_desktop_environment)
|
||||
|
||||
def test_chrome_cookie_decryptor_linux_derive_key(self):
|
||||
key = LinuxChromeCookieDecryptor.derive_key(b'abc')
|
||||
self.assertEqual(key, b'7\xa1\xec\xd4m\xfcA\xc7\xb19Z\xd0\x19\xdcM\x17')
|
||||
@@ -58,8 +91,7 @@ class TestCookies(unittest.TestCase):
|
||||
self.assertEqual(decryptor.decrypt(encrypted_value), value)
|
||||
|
||||
def test_chrome_cookie_decryptor_linux_v11(self):
|
||||
with MonkeyPatch(cookies, {'_get_linux_keyring_password': lambda *args, **kwargs: b'',
|
||||
'KEYRING_AVAILABLE': True}):
|
||||
with MonkeyPatch(cookies, {'_get_linux_keyring_password': lambda *args, **kwargs: b''}):
|
||||
encrypted_value = b'v11#\x81\x10>`w\x8f)\xc0\xb2\xc1\r\xf4\x1al\xdd\x93\xfd\xf8\xf8N\xf2\xa9\x83\xf1\xe9o\x0elVQd'
|
||||
value = 'tz=Europe.London'
|
||||
decryptor = LinuxChromeCookieDecryptor('Chrome', Logger())
|
||||
|
||||
@@ -317,10 +317,10 @@ class YoutubeDL(object):
|
||||
break_per_url: Whether break_on_reject and break_on_existing
|
||||
should act on each input URL as opposed to for the entire queue
|
||||
cookiefile: File name where cookies should be read from and dumped to
|
||||
cookiesfrombrowser: A tuple containing the name of the browser and the profile
|
||||
name/path from where cookies are loaded.
|
||||
Eg: ('chrome', ) or ('vivaldi', 'default')
|
||||
nocheckcertificate:Do not verify SSL certificates
|
||||
cookiesfrombrowser: A tuple containing the name of the browser, the profile
|
||||
name/pathfrom where cookies are loaded, and the name of the
|
||||
keyring. Eg: ('chrome', ) or ('vivaldi', 'default', 'BASICTEXT')
|
||||
nocheckcertificate: Do not verify SSL certificates
|
||||
prefer_insecure: Use HTTP instead of HTTPS to retrieve information.
|
||||
At the moment, this is only supported by YouTube.
|
||||
proxy: URL of the proxy server to use
|
||||
@@ -1348,31 +1348,33 @@ class YoutubeDL(object):
|
||||
def __handle_extraction_exceptions(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
except GeoRestrictedError as e:
|
||||
msg = e.msg
|
||||
if e.countries:
|
||||
msg += '\nThis video is available in %s.' % ', '.join(
|
||||
map(ISO3166Utils.short2full, e.countries))
|
||||
msg += '\nYou might want to use a VPN or a proxy server (with --proxy) to workaround.'
|
||||
self.report_error(msg)
|
||||
except ExtractorError as e: # An error we somewhat expected
|
||||
self.report_error(compat_str(e), e.format_traceback())
|
||||
except ReExtractInfo as e:
|
||||
if e.expected:
|
||||
self.to_screen(f'{e}; Re-extracting data')
|
||||
else:
|
||||
self.to_stderr('\r')
|
||||
self.report_warning(f'{e}; Re-extracting data')
|
||||
return wrapper(self, *args, **kwargs)
|
||||
except (DownloadCancelled, LazyList.IndexError, PagedList.IndexError):
|
||||
raise
|
||||
except Exception as e:
|
||||
if self.params.get('ignoreerrors'):
|
||||
self.report_error(error_to_compat_str(e), tb=encode_compat_str(traceback.format_exc()))
|
||||
else:
|
||||
while True:
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
except (DownloadCancelled, LazyList.IndexError, PagedList.IndexError):
|
||||
raise
|
||||
except ReExtractInfo as e:
|
||||
if e.expected:
|
||||
self.to_screen(f'{e}; Re-extracting data')
|
||||
else:
|
||||
self.to_stderr('\r')
|
||||
self.report_warning(f'{e}; Re-extracting data')
|
||||
continue
|
||||
except GeoRestrictedError as e:
|
||||
msg = e.msg
|
||||
if e.countries:
|
||||
msg += '\nThis video is available in %s.' % ', '.join(
|
||||
map(ISO3166Utils.short2full, e.countries))
|
||||
msg += '\nYou might want to use a VPN or a proxy server (with --proxy) to workaround.'
|
||||
self.report_error(msg)
|
||||
except ExtractorError as e: # An error we somewhat expected
|
||||
self.report_error(str(e), e.format_traceback())
|
||||
except Exception as e:
|
||||
if self.params.get('ignoreerrors'):
|
||||
self.report_error(str(e), tb=encode_compat_str(traceback.format_exc()))
|
||||
else:
|
||||
raise
|
||||
break
|
||||
return wrapper
|
||||
|
||||
def _wait_for_video(self, ie_result):
|
||||
@@ -3540,11 +3542,11 @@ class YoutubeDL(object):
|
||||
|
||||
from .downloader.websocket import has_websockets
|
||||
from .postprocessor.embedthumbnail import has_mutagen
|
||||
from .cookies import SQLITE_AVAILABLE, KEYRING_AVAILABLE
|
||||
from .cookies import SQLITE_AVAILABLE, SECRETSTORAGE_AVAILABLE
|
||||
|
||||
lib_str = join_nonempty(
|
||||
compat_pycrypto_AES and compat_pycrypto_AES.__name__.split('.')[0],
|
||||
KEYRING_AVAILABLE and 'keyring',
|
||||
SECRETSTORAGE_AVAILABLE and 'secretstorage',
|
||||
has_mutagen and 'mutagen',
|
||||
SQLITE_AVAILABLE and 'sqlite',
|
||||
has_websockets and 'websockets',
|
||||
|
||||
@@ -22,7 +22,7 @@ from .compat import (
|
||||
compat_shlex_quote,
|
||||
workaround_optparse_bug9161,
|
||||
)
|
||||
from .cookies import SUPPORTED_BROWSERS
|
||||
from .cookies import SUPPORTED_BROWSERS, SUPPORTED_KEYRINGS
|
||||
from .utils import (
|
||||
DateRange,
|
||||
decodeOption,
|
||||
@@ -266,10 +266,20 @@ def _real_main(argv=None):
|
||||
if opts.convertthumbnails not in FFmpegThumbnailsConvertorPP.SUPPORTED_EXTS:
|
||||
parser.error('invalid thumbnail format specified')
|
||||
if opts.cookiesfrombrowser is not None:
|
||||
opts.cookiesfrombrowser = [
|
||||
part.strip() or None for part in opts.cookiesfrombrowser.split(':', 1)]
|
||||
if opts.cookiesfrombrowser[0].lower() not in SUPPORTED_BROWSERS:
|
||||
parser.error('unsupported browser specified for cookies')
|
||||
mobj = re.match(r'(?P<name>[^+:]+)(\s*\+\s*(?P<keyring>[^:]+))?(\s*:(?P<profile>.+))?', opts.cookiesfrombrowser)
|
||||
if mobj is None:
|
||||
parser.error(f'invalid cookies from browser arguments: {opts.cookiesfrombrowser}')
|
||||
browser_name, keyring, profile = mobj.group('name', 'keyring', 'profile')
|
||||
browser_name = browser_name.lower()
|
||||
if browser_name not in SUPPORTED_BROWSERS:
|
||||
parser.error(f'unsupported browser specified for cookies: "{browser_name}". '
|
||||
f'Supported browsers are: {", ".join(sorted(SUPPORTED_BROWSERS))}')
|
||||
if keyring is not None:
|
||||
keyring = keyring.upper()
|
||||
if keyring not in SUPPORTED_KEYRINGS:
|
||||
parser.error(f'unsupported keyring specified for cookies: "{keyring}". '
|
||||
f'Supported keyrings are: {", ".join(sorted(SUPPORTED_KEYRINGS))}')
|
||||
opts.cookiesfrombrowser = (browser_name, profile, keyring)
|
||||
geo_bypass_code = opts.geo_bypass_ip_block or opts.geo_bypass_country
|
||||
if geo_bypass_code is not None:
|
||||
try:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import contextlib
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
@@ -7,6 +8,7 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from enum import Enum, auto
|
||||
from hashlib import pbkdf2_hmac
|
||||
|
||||
from .aes import aes_cbc_decrypt_bytes, aes_gcm_decrypt_and_verify_bytes
|
||||
@@ -15,7 +17,6 @@ from .compat import (
|
||||
compat_cookiejar_Cookie,
|
||||
)
|
||||
from .utils import (
|
||||
bug_reports_message,
|
||||
expand_path,
|
||||
Popen,
|
||||
YoutubeDLCookieJar,
|
||||
@@ -31,19 +32,16 @@ except ImportError:
|
||||
|
||||
|
||||
try:
|
||||
import keyring
|
||||
KEYRING_AVAILABLE = True
|
||||
KEYRING_UNAVAILABLE_REASON = f'due to unknown reasons{bug_reports_message()}'
|
||||
import secretstorage
|
||||
SECRETSTORAGE_AVAILABLE = True
|
||||
except ImportError:
|
||||
KEYRING_AVAILABLE = False
|
||||
KEYRING_UNAVAILABLE_REASON = (
|
||||
'as the `keyring` module is not installed. '
|
||||
'Please install by running `python3 -m pip install keyring`. '
|
||||
'Depending on your platform, additional packages may be required '
|
||||
'to access the keyring; see https://pypi.org/project/keyring')
|
||||
SECRETSTORAGE_AVAILABLE = False
|
||||
SECRETSTORAGE_UNAVAILABLE_REASON = (
|
||||
'as the `secretstorage` module is not installed. '
|
||||
'Please install by running `python3 -m pip install secretstorage`.')
|
||||
except Exception as _err:
|
||||
KEYRING_AVAILABLE = False
|
||||
KEYRING_UNAVAILABLE_REASON = 'as the `keyring` module could not be initialized: %s' % _err
|
||||
SECRETSTORAGE_AVAILABLE = False
|
||||
SECRETSTORAGE_UNAVAILABLE_REASON = f'as the `secretstorage` module could not be initialized. {_err}'
|
||||
|
||||
|
||||
CHROMIUM_BASED_BROWSERS = {'brave', 'chrome', 'chromium', 'edge', 'opera', 'vivaldi'}
|
||||
@@ -74,8 +72,8 @@ class YDLLogger:
|
||||
def load_cookies(cookie_file, browser_specification, ydl):
|
||||
cookie_jars = []
|
||||
if browser_specification is not None:
|
||||
browser_name, profile = _parse_browser_specification(*browser_specification)
|
||||
cookie_jars.append(extract_cookies_from_browser(browser_name, profile, YDLLogger(ydl)))
|
||||
browser_name, profile, keyring = _parse_browser_specification(*browser_specification)
|
||||
cookie_jars.append(extract_cookies_from_browser(browser_name, profile, YDLLogger(ydl), keyring=keyring))
|
||||
|
||||
if cookie_file is not None:
|
||||
cookie_file = expand_path(cookie_file)
|
||||
@@ -87,13 +85,13 @@ def load_cookies(cookie_file, browser_specification, ydl):
|
||||
return _merge_cookie_jars(cookie_jars)
|
||||
|
||||
|
||||
def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger()):
|
||||
def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger(), *, keyring=None):
|
||||
if browser_name == 'firefox':
|
||||
return _extract_firefox_cookies(profile, logger)
|
||||
elif browser_name == 'safari':
|
||||
return _extract_safari_cookies(profile, logger)
|
||||
elif browser_name in CHROMIUM_BASED_BROWSERS:
|
||||
return _extract_chrome_cookies(browser_name, profile, logger)
|
||||
return _extract_chrome_cookies(browser_name, profile, keyring, logger)
|
||||
else:
|
||||
raise ValueError('unknown browser: {}'.format(browser_name))
|
||||
|
||||
@@ -207,7 +205,7 @@ def _get_chromium_based_browser_settings(browser_name):
|
||||
}
|
||||
|
||||
|
||||
def _extract_chrome_cookies(browser_name, profile, logger):
|
||||
def _extract_chrome_cookies(browser_name, profile, keyring, logger):
|
||||
logger.info('Extracting cookies from {}'.format(browser_name))
|
||||
|
||||
if not SQLITE_AVAILABLE:
|
||||
@@ -234,7 +232,7 @@ def _extract_chrome_cookies(browser_name, profile, logger):
|
||||
raise FileNotFoundError('could not find {} cookies database in "{}"'.format(browser_name, search_root))
|
||||
logger.debug('Extracting cookies from: "{}"'.format(cookie_database_path))
|
||||
|
||||
decryptor = get_cookie_decryptor(config['browser_dir'], config['keyring_name'], logger)
|
||||
decryptor = get_cookie_decryptor(config['browser_dir'], config['keyring_name'], logger, keyring=keyring)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='yt_dlp') as tmpdir:
|
||||
cursor = None
|
||||
@@ -247,6 +245,7 @@ def _extract_chrome_cookies(browser_name, profile, logger):
|
||||
'expires_utc, {} FROM cookies'.format(secure_column))
|
||||
jar = YoutubeDLCookieJar()
|
||||
failed_cookies = 0
|
||||
unencrypted_cookies = 0
|
||||
for host_key, name, value, encrypted_value, path, expires_utc, is_secure in cursor.fetchall():
|
||||
host_key = host_key.decode('utf-8')
|
||||
name = name.decode('utf-8')
|
||||
@@ -258,6 +257,8 @@ def _extract_chrome_cookies(browser_name, profile, logger):
|
||||
if value is None:
|
||||
failed_cookies += 1
|
||||
continue
|
||||
else:
|
||||
unencrypted_cookies += 1
|
||||
|
||||
cookie = compat_cookiejar_Cookie(
|
||||
version=0, name=name, value=value, port=None, port_specified=False,
|
||||
@@ -270,6 +271,9 @@ def _extract_chrome_cookies(browser_name, profile, logger):
|
||||
else:
|
||||
failed_message = ''
|
||||
logger.info('Extracted {} cookies from {}{}'.format(len(jar), browser_name, failed_message))
|
||||
counts = decryptor.cookie_counts.copy()
|
||||
counts['unencrypted'] = unencrypted_cookies
|
||||
logger.debug('cookie version breakdown: {}'.format(counts))
|
||||
return jar
|
||||
finally:
|
||||
if cursor is not None:
|
||||
@@ -305,10 +309,14 @@ class ChromeCookieDecryptor:
|
||||
def decrypt(self, encrypted_value):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def cookie_counts(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_cookie_decryptor(browser_root, browser_keyring_name, logger):
|
||||
|
||||
def get_cookie_decryptor(browser_root, browser_keyring_name, logger, *, keyring=None):
|
||||
if sys.platform in ('linux', 'linux2'):
|
||||
return LinuxChromeCookieDecryptor(browser_keyring_name, logger)
|
||||
return LinuxChromeCookieDecryptor(browser_keyring_name, logger, keyring=keyring)
|
||||
elif sys.platform == 'darwin':
|
||||
return MacChromeCookieDecryptor(browser_keyring_name, logger)
|
||||
elif sys.platform == 'win32':
|
||||
@@ -319,13 +327,12 @@ def get_cookie_decryptor(browser_root, browser_keyring_name, logger):
|
||||
|
||||
|
||||
class LinuxChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||
def __init__(self, browser_keyring_name, logger):
|
||||
def __init__(self, browser_keyring_name, logger, *, keyring=None):
|
||||
self._logger = logger
|
||||
self._v10_key = self.derive_key(b'peanuts')
|
||||
if KEYRING_AVAILABLE:
|
||||
self._v11_key = self.derive_key(_get_linux_keyring_password(browser_keyring_name))
|
||||
else:
|
||||
self._v11_key = None
|
||||
password = _get_linux_keyring_password(browser_keyring_name, keyring, logger)
|
||||
self._v11_key = None if password is None else self.derive_key(password)
|
||||
self._cookie_counts = {'v10': 0, 'v11': 0, 'other': 0}
|
||||
|
||||
@staticmethod
|
||||
def derive_key(password):
|
||||
@@ -333,20 +340,27 @@ class LinuxChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||
# https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/os_crypt_linux.cc
|
||||
return pbkdf2_sha1(password, salt=b'saltysalt', iterations=1, key_length=16)
|
||||
|
||||
@property
|
||||
def cookie_counts(self):
|
||||
return self._cookie_counts
|
||||
|
||||
def decrypt(self, encrypted_value):
|
||||
version = encrypted_value[:3]
|
||||
ciphertext = encrypted_value[3:]
|
||||
|
||||
if version == b'v10':
|
||||
self._cookie_counts['v10'] += 1
|
||||
return _decrypt_aes_cbc(ciphertext, self._v10_key, self._logger)
|
||||
|
||||
elif version == b'v11':
|
||||
self._cookie_counts['v11'] += 1
|
||||
if self._v11_key is None:
|
||||
self._logger.warning(f'cannot decrypt cookie {KEYRING_UNAVAILABLE_REASON}', only_once=True)
|
||||
self._logger.warning('cannot decrypt v11 cookies: no key found', only_once=True)
|
||||
return None
|
||||
return _decrypt_aes_cbc(ciphertext, self._v11_key, self._logger)
|
||||
|
||||
else:
|
||||
self._cookie_counts['other'] += 1
|
||||
return None
|
||||
|
||||
|
||||
@@ -355,6 +369,7 @@ class MacChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||
self._logger = logger
|
||||
password = _get_mac_keyring_password(browser_keyring_name, logger)
|
||||
self._v10_key = None if password is None else self.derive_key(password)
|
||||
self._cookie_counts = {'v10': 0, 'other': 0}
|
||||
|
||||
@staticmethod
|
||||
def derive_key(password):
|
||||
@@ -362,11 +377,16 @@ class MacChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||
# https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/os_crypt_mac.mm
|
||||
return pbkdf2_sha1(password, salt=b'saltysalt', iterations=1003, key_length=16)
|
||||
|
||||
@property
|
||||
def cookie_counts(self):
|
||||
return self._cookie_counts
|
||||
|
||||
def decrypt(self, encrypted_value):
|
||||
version = encrypted_value[:3]
|
||||
ciphertext = encrypted_value[3:]
|
||||
|
||||
if version == b'v10':
|
||||
self._cookie_counts['v10'] += 1
|
||||
if self._v10_key is None:
|
||||
self._logger.warning('cannot decrypt v10 cookies: no key found', only_once=True)
|
||||
return None
|
||||
@@ -374,6 +394,7 @@ class MacChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||
return _decrypt_aes_cbc(ciphertext, self._v10_key, self._logger)
|
||||
|
||||
else:
|
||||
self._cookie_counts['other'] += 1
|
||||
# other prefixes are considered 'old data' which were stored as plaintext
|
||||
# https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/os_crypt_mac.mm
|
||||
return encrypted_value
|
||||
@@ -383,12 +404,18 @@ class WindowsChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||
def __init__(self, browser_root, logger):
|
||||
self._logger = logger
|
||||
self._v10_key = _get_windows_v10_key(browser_root, logger)
|
||||
self._cookie_counts = {'v10': 0, 'other': 0}
|
||||
|
||||
@property
|
||||
def cookie_counts(self):
|
||||
return self._cookie_counts
|
||||
|
||||
def decrypt(self, encrypted_value):
|
||||
version = encrypted_value[:3]
|
||||
ciphertext = encrypted_value[3:]
|
||||
|
||||
if version == b'v10':
|
||||
self._cookie_counts['v10'] += 1
|
||||
if self._v10_key is None:
|
||||
self._logger.warning('cannot decrypt v10 cookies: no key found', only_once=True)
|
||||
return None
|
||||
@@ -408,6 +435,7 @@ class WindowsChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||
return _decrypt_aes_gcm(ciphertext, self._v10_key, nonce, authentication_tag, self._logger)
|
||||
|
||||
else:
|
||||
self._cookie_counts['other'] += 1
|
||||
# any other prefix means the data is DPAPI encrypted
|
||||
# https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/os_crypt_win.cc
|
||||
return _decrypt_windows_dpapi(encrypted_value, self._logger).decode('utf-8')
|
||||
@@ -577,42 +605,221 @@ def parse_safari_cookies(data, jar=None, logger=YDLLogger()):
|
||||
return jar
|
||||
|
||||
|
||||
def _get_linux_keyring_password(browser_keyring_name):
|
||||
password = keyring.get_password('{} Keys'.format(browser_keyring_name),
|
||||
'{} Safe Storage'.format(browser_keyring_name))
|
||||
if password is None:
|
||||
# this sometimes occurs in KDE because chrome does not check hasEntry and instead
|
||||
# just tries to read the value (which kwallet returns "") whereas keyring checks hasEntry
|
||||
# to verify this:
|
||||
# dbus-monitor "interface='org.kde.KWallet'" "type=method_return"
|
||||
# while starting chrome.
|
||||
# this may be a bug as the intended behaviour is to generate a random password and store
|
||||
# it, but that doesn't matter here.
|
||||
password = ''
|
||||
return password.encode('utf-8')
|
||||
class _LinuxDesktopEnvironment(Enum):
|
||||
"""
|
||||
https://chromium.googlesource.com/chromium/src/+/refs/heads/main/base/nix/xdg_util.h
|
||||
DesktopEnvironment
|
||||
"""
|
||||
OTHER = auto()
|
||||
CINNAMON = auto()
|
||||
GNOME = auto()
|
||||
KDE = auto()
|
||||
PANTHEON = auto()
|
||||
UNITY = auto()
|
||||
XFCE = auto()
|
||||
|
||||
|
||||
class _LinuxKeyring(Enum):
|
||||
"""
|
||||
https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/key_storage_util_linux.h
|
||||
SelectedLinuxBackend
|
||||
"""
|
||||
KWALLET = auto()
|
||||
GNOMEKEYRING = auto()
|
||||
BASICTEXT = auto()
|
||||
|
||||
|
||||
SUPPORTED_KEYRINGS = _LinuxKeyring.__members__.keys()
|
||||
|
||||
|
||||
def _get_linux_desktop_environment(env):
|
||||
"""
|
||||
https://chromium.googlesource.com/chromium/src/+/refs/heads/main/base/nix/xdg_util.cc
|
||||
GetDesktopEnvironment
|
||||
"""
|
||||
xdg_current_desktop = env.get('XDG_CURRENT_DESKTOP', None)
|
||||
desktop_session = env.get('DESKTOP_SESSION', None)
|
||||
if xdg_current_desktop is not None:
|
||||
xdg_current_desktop = xdg_current_desktop.split(':')[0].strip()
|
||||
|
||||
if xdg_current_desktop == 'Unity':
|
||||
if desktop_session is not None and 'gnome-fallback' in desktop_session:
|
||||
return _LinuxDesktopEnvironment.GNOME
|
||||
else:
|
||||
return _LinuxDesktopEnvironment.UNITY
|
||||
elif xdg_current_desktop == 'GNOME':
|
||||
return _LinuxDesktopEnvironment.GNOME
|
||||
elif xdg_current_desktop == 'X-Cinnamon':
|
||||
return _LinuxDesktopEnvironment.CINNAMON
|
||||
elif xdg_current_desktop == 'KDE':
|
||||
return _LinuxDesktopEnvironment.KDE
|
||||
elif xdg_current_desktop == 'Pantheon':
|
||||
return _LinuxDesktopEnvironment.PANTHEON
|
||||
elif xdg_current_desktop == 'XFCE':
|
||||
return _LinuxDesktopEnvironment.XFCE
|
||||
elif desktop_session is not None:
|
||||
if desktop_session in ('mate', 'gnome'):
|
||||
return _LinuxDesktopEnvironment.GNOME
|
||||
elif 'kde' in desktop_session:
|
||||
return _LinuxDesktopEnvironment.KDE
|
||||
elif 'xfce' in desktop_session:
|
||||
return _LinuxDesktopEnvironment.XFCE
|
||||
else:
|
||||
if 'GNOME_DESKTOP_SESSION_ID' in env:
|
||||
return _LinuxDesktopEnvironment.GNOME
|
||||
elif 'KDE_FULL_SESSION' in env:
|
||||
return _LinuxDesktopEnvironment.KDE
|
||||
else:
|
||||
return _LinuxDesktopEnvironment.OTHER
|
||||
|
||||
|
||||
def _choose_linux_keyring(logger):
|
||||
"""
|
||||
https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/key_storage_util_linux.cc
|
||||
SelectBackend
|
||||
"""
|
||||
desktop_environment = _get_linux_desktop_environment(os.environ)
|
||||
logger.debug('detected desktop environment: {}'.format(desktop_environment.name))
|
||||
if desktop_environment == _LinuxDesktopEnvironment.KDE:
|
||||
linux_keyring = _LinuxKeyring.KWALLET
|
||||
elif desktop_environment == _LinuxDesktopEnvironment.OTHER:
|
||||
linux_keyring = _LinuxKeyring.BASICTEXT
|
||||
else:
|
||||
linux_keyring = _LinuxKeyring.GNOMEKEYRING
|
||||
return linux_keyring
|
||||
|
||||
|
||||
def _get_kwallet_network_wallet(logger):
|
||||
""" The name of the wallet used to store network passwords.
|
||||
|
||||
https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/kwallet_dbus.cc
|
||||
KWalletDBus::NetworkWallet
|
||||
which does a dbus call to the following function:
|
||||
https://api.kde.org/frameworks/kwallet/html/classKWallet_1_1Wallet.html
|
||||
Wallet::NetworkWallet
|
||||
"""
|
||||
default_wallet = 'kdewallet'
|
||||
try:
|
||||
proc = Popen([
|
||||
'dbus-send', '--session', '--print-reply=literal',
|
||||
'--dest=org.kde.kwalletd5',
|
||||
'/modules/kwalletd5',
|
||||
'org.kde.KWallet.networkWallet'
|
||||
], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
||||
|
||||
stdout, stderr = proc.communicate_or_kill()
|
||||
if proc.returncode != 0:
|
||||
logger.warning('failed to read NetworkWallet')
|
||||
return default_wallet
|
||||
else:
|
||||
network_wallet = stdout.decode('utf-8').strip()
|
||||
logger.debug('NetworkWallet = "{}"'.format(network_wallet))
|
||||
return network_wallet
|
||||
except BaseException as e:
|
||||
logger.warning('exception while obtaining NetworkWallet: {}'.format(e))
|
||||
return default_wallet
|
||||
|
||||
|
||||
def _get_kwallet_password(browser_keyring_name, logger):
|
||||
logger.debug('using kwallet-query to obtain password from kwallet')
|
||||
|
||||
if shutil.which('kwallet-query') is None:
|
||||
logger.error('kwallet-query command not found. KWallet and kwallet-query '
|
||||
'must be installed to read from KWallet. kwallet-query should be'
|
||||
'included in the kwallet package for your distribution')
|
||||
return b''
|
||||
|
||||
network_wallet = _get_kwallet_network_wallet(logger)
|
||||
|
||||
try:
|
||||
proc = Popen([
|
||||
'kwallet-query',
|
||||
'--read-password', '{} Safe Storage'.format(browser_keyring_name),
|
||||
'--folder', '{} Keys'.format(browser_keyring_name),
|
||||
network_wallet
|
||||
], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
||||
|
||||
stdout, stderr = proc.communicate_or_kill()
|
||||
if proc.returncode != 0:
|
||||
logger.error('kwallet-query failed with return code {}. Please consult '
|
||||
'the kwallet-query man page for details'.format(proc.returncode))
|
||||
return b''
|
||||
else:
|
||||
if stdout.lower().startswith(b'failed to read'):
|
||||
logger.debug('failed to read password from kwallet. Using empty string instead')
|
||||
# this sometimes occurs in KDE because chrome does not check hasEntry and instead
|
||||
# just tries to read the value (which kwallet returns "") whereas kwallet-query
|
||||
# checks hasEntry. To verify this:
|
||||
# dbus-monitor "interface='org.kde.KWallet'" "type=method_return"
|
||||
# while starting chrome.
|
||||
# this may be a bug as the intended behaviour is to generate a random password and store
|
||||
# it, but that doesn't matter here.
|
||||
return b''
|
||||
else:
|
||||
logger.debug('password found')
|
||||
if stdout[-1:] == b'\n':
|
||||
stdout = stdout[:-1]
|
||||
return stdout
|
||||
except BaseException as e:
|
||||
logger.warning(f'exception running kwallet-query: {type(e).__name__}({e})')
|
||||
return b''
|
||||
|
||||
|
||||
def _get_gnome_keyring_password(browser_keyring_name, logger):
|
||||
if not SECRETSTORAGE_AVAILABLE:
|
||||
logger.error('secretstorage not available {}'.format(SECRETSTORAGE_UNAVAILABLE_REASON))
|
||||
return b''
|
||||
# the Gnome keyring does not seem to organise keys in the same way as KWallet,
|
||||
# using `dbus-monitor` during startup, it can be observed that chromium lists all keys
|
||||
# and presumably searches for its key in the list. It appears that we must do the same.
|
||||
# https://github.com/jaraco/keyring/issues/556
|
||||
with contextlib.closing(secretstorage.dbus_init()) as con:
|
||||
col = secretstorage.get_default_collection(con)
|
||||
for item in col.get_all_items():
|
||||
if item.get_label() == '{} Safe Storage'.format(browser_keyring_name):
|
||||
return item.get_secret()
|
||||
else:
|
||||
logger.error('failed to read from keyring')
|
||||
return b''
|
||||
|
||||
|
||||
def _get_linux_keyring_password(browser_keyring_name, keyring, logger):
|
||||
# note: chrome/chromium can be run with the following flags to determine which keyring backend
|
||||
# it has chosen to use
|
||||
# chromium --enable-logging=stderr --v=1 2>&1 | grep key_storage_
|
||||
# Chromium supports a flag: --password-store=<basic|gnome|kwallet> so the automatic detection
|
||||
# will not be sufficient in all cases.
|
||||
|
||||
keyring = _LinuxKeyring[keyring] or _choose_linux_keyring(logger)
|
||||
logger.debug(f'Chosen keyring: {keyring.name}')
|
||||
|
||||
if keyring == _LinuxKeyring.KWALLET:
|
||||
return _get_kwallet_password(browser_keyring_name, logger)
|
||||
elif keyring == _LinuxKeyring.GNOMEKEYRING:
|
||||
return _get_gnome_keyring_password(browser_keyring_name, logger)
|
||||
elif keyring == _LinuxKeyring.BASICTEXT:
|
||||
# when basic text is chosen, all cookies are stored as v10 (so no keyring password is required)
|
||||
return None
|
||||
assert False, f'Unknown keyring {keyring}'
|
||||
|
||||
|
||||
def _get_mac_keyring_password(browser_keyring_name, logger):
|
||||
if KEYRING_AVAILABLE:
|
||||
logger.debug('using keyring to obtain password')
|
||||
password = keyring.get_password('{} Safe Storage'.format(browser_keyring_name), browser_keyring_name)
|
||||
return password.encode('utf-8')
|
||||
else:
|
||||
logger.debug('using find-generic-password to obtain password')
|
||||
logger.debug('using find-generic-password to obtain password from OSX keychain')
|
||||
try:
|
||||
proc = Popen(
|
||||
['security', 'find-generic-password',
|
||||
'-w', # write password to stdout
|
||||
'-a', browser_keyring_name, # match 'account'
|
||||
'-s', '{} Safe Storage'.format(browser_keyring_name)], # match 'service'
|
||||
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
||||
try:
|
||||
stdout, stderr = proc.communicate_or_kill()
|
||||
if stdout[-1:] == b'\n':
|
||||
stdout = stdout[:-1]
|
||||
return stdout
|
||||
except BaseException as e:
|
||||
logger.warning(f'exception running find-generic-password: {type(e).__name__}({e})')
|
||||
return None
|
||||
|
||||
stdout, stderr = proc.communicate_or_kill()
|
||||
if stdout[-1:] == b'\n':
|
||||
stdout = stdout[:-1]
|
||||
return stdout
|
||||
except BaseException as e:
|
||||
logger.warning(f'exception running find-generic-password: {type(e).__name__}({e})')
|
||||
return None
|
||||
|
||||
|
||||
def _get_windows_v10_key(browser_root, logger):
|
||||
@@ -736,10 +943,11 @@ def _is_path(value):
|
||||
return os.path.sep in value
|
||||
|
||||
|
||||
def _parse_browser_specification(browser_name, profile=None):
|
||||
browser_name = browser_name.lower()
|
||||
def _parse_browser_specification(browser_name, profile=None, keyring=None):
|
||||
if browser_name not in SUPPORTED_BROWSERS:
|
||||
raise ValueError(f'unsupported browser: "{browser_name}"')
|
||||
if keyring not in (None, *SUPPORTED_KEYRINGS):
|
||||
raise ValueError(f'unsupported keyring: "{keyring}"')
|
||||
if profile is not None and _is_path(profile):
|
||||
profile = os.path.expanduser(profile)
|
||||
return browser_name, profile
|
||||
return browser_name, profile, keyring
|
||||
|
||||
@@ -265,6 +265,7 @@ class Aria2cFD(ExternalFD):
|
||||
cmd += self._option('--all-proxy', 'proxy')
|
||||
cmd += self._bool_option('--check-certificate', 'nocheckcertificate', 'false', 'true', '=')
|
||||
cmd += self._bool_option('--remote-time', 'updatetime', 'true', 'false', '=')
|
||||
cmd += self._bool_option('--show-console-readout', 'noprogress', 'false', 'true', '=')
|
||||
cmd += self._configuration_args()
|
||||
|
||||
# aria2c strips out spaces from the beginning/end of filenames and paths.
|
||||
|
||||
@@ -433,6 +433,7 @@ class FragmentFD(FileDownloader):
|
||||
|
||||
def download_fragment(fragment, ctx):
|
||||
frag_index = ctx['fragment_index'] = fragment['frag_index']
|
||||
ctx['last_error'] = None
|
||||
if not interrupt_trigger[0]:
|
||||
return False, frag_index
|
||||
headers = info_dict.get('http_headers', {}).copy()
|
||||
@@ -455,6 +456,7 @@ class FragmentFD(FileDownloader):
|
||||
# See https://github.com/ytdl-org/youtube-dl/issues/10165,
|
||||
# https://github.com/ytdl-org/youtube-dl/issues/10448).
|
||||
count += 1
|
||||
ctx['last_error'] = err
|
||||
if count <= fragment_retries:
|
||||
self.report_retry_fragment(err, frag_index, count, fragment_retries)
|
||||
except DownloadError:
|
||||
|
||||
@@ -19,14 +19,15 @@ from ..utils import (
|
||||
parse_iso8601,
|
||||
traverse_obj,
|
||||
try_get,
|
||||
parse_count,
|
||||
smuggle_url,
|
||||
srt_subtitles_timecode,
|
||||
str_or_none,
|
||||
str_to_int,
|
||||
strip_jsonp,
|
||||
unified_timestamp,
|
||||
unsmuggle_url,
|
||||
urlencode_postdata,
|
||||
url_or_none,
|
||||
OnDemandPagedList
|
||||
)
|
||||
|
||||
@@ -722,10 +723,10 @@ class BiliBiliPlayerIE(InfoExtractor):
|
||||
|
||||
|
||||
class BiliIntlBaseIE(InfoExtractor):
|
||||
_API_URL = 'https://api.bili{}/intl/gateway{}'
|
||||
_API_URL = 'https://api.bilibili.tv/intl/gateway'
|
||||
|
||||
def _call_api(self, type, endpoint, id):
|
||||
return self._download_json(self._API_URL.format(type, endpoint), id)['data']
|
||||
def _call_api(self, endpoint, *args, **kwargs):
|
||||
return self._download_json(self._API_URL + endpoint, *args, **kwargs)['data']
|
||||
|
||||
def json2srt(self, json):
|
||||
data = '\n\n'.join(
|
||||
@@ -733,29 +734,40 @@ class BiliIntlBaseIE(InfoExtractor):
|
||||
for i, line in enumerate(json['body']))
|
||||
return data
|
||||
|
||||
def _get_subtitles(self, type, ep_id):
|
||||
sub_json = self._call_api(type, f'/m/subtitle?ep_id={ep_id}&platform=web', ep_id)
|
||||
def _get_subtitles(self, ep_id):
|
||||
sub_json = self._call_api(f'/web/v2/subtitle?episode_id={ep_id}&platform=web', ep_id)
|
||||
subtitles = {}
|
||||
for sub in sub_json.get('subtitles', []):
|
||||
for sub in sub_json.get('subtitles') or []:
|
||||
sub_url = sub.get('url')
|
||||
if not sub_url:
|
||||
continue
|
||||
sub_data = self._download_json(sub_url, ep_id, fatal=False)
|
||||
sub_data = self._download_json(
|
||||
sub_url, ep_id, errnote='Unable to download subtitles', fatal=False,
|
||||
note='Downloading subtitles%s' % f' for {sub["lang"]}' if sub.get('lang') else '')
|
||||
if not sub_data:
|
||||
continue
|
||||
subtitles.setdefault(sub.get('key', 'en'), []).append({
|
||||
subtitles.setdefault(sub.get('lang_key', 'en'), []).append({
|
||||
'ext': 'srt',
|
||||
'data': self.json2srt(sub_data)
|
||||
})
|
||||
return subtitles
|
||||
|
||||
def _get_formats(self, type, ep_id):
|
||||
video_json = self._call_api(type, f'/web/playurl?ep_id={ep_id}&platform=web', ep_id)
|
||||
if not video_json:
|
||||
self.raise_login_required(method='cookies')
|
||||
def _get_formats(self, ep_id):
|
||||
video_json = self._call_api(f'/web/playurl?ep_id={ep_id}&platform=web', ep_id,
|
||||
note='Downloading video formats', errnote='Unable to download video formats')
|
||||
if video_json.get('code'):
|
||||
if video_json['code'] in (10004004, 10004005, 10023006):
|
||||
self.raise_login_required(method='cookies')
|
||||
elif video_json['code'] == 10004001:
|
||||
self.raise_geo_restricted()
|
||||
elif video_json.get('message') and str(video_json['code']) != video_json['message']:
|
||||
raise ExtractorError(
|
||||
f'Unable to download video formats: {self.IE_NAME} said: {video_json["message"]}', expected=True)
|
||||
else:
|
||||
raise ExtractorError('Unable to download video formats')
|
||||
video_json = video_json['playurl']
|
||||
formats = []
|
||||
for vid in video_json.get('video', []):
|
||||
for vid in video_json.get('video') or []:
|
||||
video_res = vid.get('video_resource') or {}
|
||||
video_info = vid.get('stream_info') or {}
|
||||
if not video_res.get('url'):
|
||||
@@ -771,7 +783,7 @@ class BiliIntlBaseIE(InfoExtractor):
|
||||
'vcodec': video_res.get('codecs'),
|
||||
'filesize': video_res.get('size'),
|
||||
})
|
||||
for aud in video_json.get('audio_resource', []):
|
||||
for aud in video_json.get('audio_resource') or []:
|
||||
if not aud.get('url'):
|
||||
continue
|
||||
formats.append({
|
||||
@@ -786,85 +798,93 @@ class BiliIntlBaseIE(InfoExtractor):
|
||||
self._sort_formats(formats)
|
||||
return formats
|
||||
|
||||
def _extract_ep_info(self, type, episode_data, ep_id):
|
||||
def _extract_ep_info(self, episode_data, ep_id):
|
||||
return {
|
||||
'id': ep_id,
|
||||
'title': episode_data.get('long_title') or episode_data['title'],
|
||||
'title': episode_data.get('title_display') or episode_data['title'],
|
||||
'thumbnail': episode_data.get('cover'),
|
||||
'episode_number': str_to_int(episode_data.get('title')),
|
||||
'formats': self._get_formats(type, ep_id),
|
||||
'subtitles': self._get_subtitles(type, ep_id),
|
||||
'episode_number': int_or_none(self._search_regex(
|
||||
r'^E(\d+)(?:$| - )', episode_data.get('title_display'), 'episode number', default=None)),
|
||||
'formats': self._get_formats(ep_id),
|
||||
'subtitles': self._get_subtitles(ep_id),
|
||||
'extractor_key': BiliIntlIE.ie_key(),
|
||||
}
|
||||
|
||||
|
||||
class BiliIntlIE(BiliIntlBaseIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?bili(?P<type>bili\.tv|intl.com)/(?:[a-z]{2}/)?play/(?P<season_id>\d+)/(?P<id>\d+)'
|
||||
_VALID_URL = r'https?://(?:www\.)?bili(?:bili\.tv|intl\.com)/(?:[a-z]{2}/)?play/(?P<season_id>\d+)/(?P<id>\d+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.bilibili.tv/en/play/34613/341736',
|
||||
'info_dict': {
|
||||
'id': '341736',
|
||||
'ext': 'mp4',
|
||||
'title': 'The First Night',
|
||||
'thumbnail': 'https://i0.hdslb.com/bfs/intl/management/91e30e5521235d9b163339a26a0b030ebda54310.png',
|
||||
'title': 'E2 - The First Night',
|
||||
'thumbnail': r're:^https://pic\.bstarstatic\.com/ogv/.+\.png$',
|
||||
'episode_number': 2,
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
},
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.bilibili.tv/en/play/1033760/11005006',
|
||||
'info_dict': {
|
||||
'id': '11005006',
|
||||
'ext': 'mp4',
|
||||
'title': 'E3 - Who?',
|
||||
'thumbnail': r're:^https://pic\.bstarstatic\.com/ogv/.+\.png$',
|
||||
'episode_number': 3,
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.biliintl.com/en/play/34613/341736',
|
||||
'info_dict': {
|
||||
'id': '341736',
|
||||
'ext': 'mp4',
|
||||
'title': 'The First Night',
|
||||
'thumbnail': 'https://i0.hdslb.com/bfs/intl/management/91e30e5521235d9b163339a26a0b030ebda54310.png',
|
||||
'episode_number': 2,
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
},
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
type, season_id, id = self._match_valid_url(url).groups()
|
||||
data_json = self._call_api(type, f'/web/view/ogv_collection?season_id={season_id}', id)
|
||||
episode_data = next(
|
||||
episode for episode in data_json.get('episodes', [])
|
||||
if str(episode.get('ep_id')) == id)
|
||||
return self._extract_ep_info(type, episode_data, id)
|
||||
season_id, video_id = self._match_valid_url(url).groups()
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
# Bstation layout
|
||||
initial_data = self._parse_json(self._search_regex(
|
||||
r'window\.__INITIAL_DATA__\s*=\s*({.+?});', webpage,
|
||||
'preload state', default='{}'), video_id, fatal=False) or {}
|
||||
episode_data = traverse_obj(initial_data, ('OgvVideo', 'epDetail'), expected_type=dict)
|
||||
|
||||
if not episode_data:
|
||||
# Non-Bstation layout, read through episode list
|
||||
season_json = self._call_api(f'/web/v2/ogv/play/episodes?season_id={season_id}&platform=web', video_id)
|
||||
episode_data = next(
|
||||
episode for episode in traverse_obj(season_json, ('sections', ..., 'episodes', ...), expected_type=dict)
|
||||
if str(episode.get('episode_id')) == video_id)
|
||||
return self._extract_ep_info(episode_data, video_id)
|
||||
|
||||
|
||||
class BiliIntlSeriesIE(BiliIntlBaseIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?bili(?P<type>bili\.tv|intl.com)/(?:[a-z]{2}/)?play/(?P<id>\d+)$'
|
||||
_VALID_URL = r'https?://(?:www\.)?bili(?:bili\.tv|intl\.com)/(?:[a-z]{2}/)?play/(?P<id>\d+)$'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.bilibili.tv/en/play/34613',
|
||||
'playlist_mincount': 15,
|
||||
'info_dict': {
|
||||
'id': '34613',
|
||||
'title': 'Fly Me to the Moon',
|
||||
'description': 'md5:a861ee1c4dc0acfad85f557cc42ac627',
|
||||
'categories': ['Romance', 'Comedy', 'Slice of life'],
|
||||
'thumbnail': r're:^https://pic\.bstarstatic\.com/ogv/.+\.png$',
|
||||
'view_count': int,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
'format': 'bv',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.biliintl.com/en/play/34613',
|
||||
'playlist_mincount': 15,
|
||||
'info_dict': {
|
||||
'id': '34613',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
'format': 'bv',
|
||||
},
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _entries(self, id, type):
|
||||
data_json = self._call_api(type, f'/web/view/ogv_collection?season_id={id}', id)
|
||||
for episode in data_json.get('episodes', []):
|
||||
episode_id = str(episode.get('ep_id'))
|
||||
yield self._extract_ep_info(type, episode, episode_id)
|
||||
def _entries(self, series_id):
|
||||
series_json = self._call_api(f'/web/v2/ogv/play/episodes?season_id={series_id}&platform=web', series_id)
|
||||
for episode in traverse_obj(series_json, ('sections', ..., 'episodes', ...), expected_type=dict, default=[]):
|
||||
episode_id = str(episode.get('episode_id'))
|
||||
yield self._extract_ep_info(episode, episode_id)
|
||||
|
||||
def _real_extract(self, url):
|
||||
type, id = self._match_valid_url(url).groups()
|
||||
return self.playlist_result(self._entries(id, type), playlist_id=id)
|
||||
series_id = self._match_id(url)
|
||||
series_info = self._call_api(f'/web/v2/ogv/play/season_info?season_id={series_id}&platform=web', series_id).get('season') or {}
|
||||
return self.playlist_result(
|
||||
self._entries(series_id), series_id, series_info.get('title'), series_info.get('description'),
|
||||
categories=traverse_obj(series_info, ('styles', ..., 'title'), expected_type=str_or_none),
|
||||
thumbnail=url_or_none(series_info.get('horizontal_cover')), view_count=parse_count(series_info.get('view')))
|
||||
|
||||
@@ -340,7 +340,8 @@ class CBCGemIE(InfoExtractor):
|
||||
yield {
|
||||
**base_format,
|
||||
'format_id': join_nonempty('sec', height),
|
||||
'url': re.sub(r'(QualityLevels\()\d+(\))', fr'\1{bitrate}\2', base_url),
|
||||
# Note: \g<1> is necessary instead of \1 since bitrate is a number
|
||||
'url': re.sub(r'(QualityLevels\()\d+(\))', fr'\g<1>{bitrate}\2', base_url),
|
||||
'width': int_or_none(video_quality.attrib.get('MaxWidth')),
|
||||
'tbr': bitrate / 1000.0,
|
||||
'height': height,
|
||||
|
||||
@@ -1126,6 +1126,10 @@ from .pinterest import (
|
||||
PinterestIE,
|
||||
PinterestCollectionIE,
|
||||
)
|
||||
from .pixivsketch import (
|
||||
PixivSketchIE,
|
||||
PixivSketchUserIE,
|
||||
)
|
||||
from .pladform import PladformIE
|
||||
from .planetmarathi import PlanetMarathiIE
|
||||
from .platzi import (
|
||||
@@ -1534,6 +1538,9 @@ from .threeqsdn import ThreeQSDNIE
|
||||
from .tiktok import (
|
||||
TikTokIE,
|
||||
TikTokUserIE,
|
||||
TikTokSoundIE,
|
||||
TikTokEffectIE,
|
||||
TikTokTagIE,
|
||||
DouyinIE,
|
||||
)
|
||||
from .tinypic import TinyPicIE
|
||||
|
||||
@@ -2344,6 +2344,18 @@ class GenericIE(InfoExtractor):
|
||||
'thumbnail': 'https://bogmedia.org/contents/videos_screenshots/21000/21217/preview_480p.mp4.jpg',
|
||||
}
|
||||
},
|
||||
{
|
||||
# KVS Player (for sites that serve kt_player.js via non-https urls)
|
||||
'url': 'http://www.camhub.world/embed/389508',
|
||||
'md5': 'fbe89af4cfb59c8fd9f34a202bb03e32',
|
||||
'info_dict': {
|
||||
'id': '389508',
|
||||
'display_id': 'syren-de-mer-onlyfans-05-07-2020have-a-happy-safe-holiday5f014e68a220979bdb8cd-source',
|
||||
'ext': 'mp4',
|
||||
'title': 'Syren De Mer onlyfans_05-07-2020Have_a_happy_safe_holiday5f014e68a220979bdb8cd_source / Embed плеер',
|
||||
'thumbnail': 'http://www.camhub.world/contents/videos_screenshots/389000/389508/preview.mp4.jpg',
|
||||
}
|
||||
},
|
||||
{
|
||||
# Reddit-hosted video that will redirect and be processed by RedditIE
|
||||
# Redirects to https://www.reddit.com/r/videos/comments/6rrwyj/that_small_heart_attack/
|
||||
@@ -3689,7 +3701,7 @@ class GenericIE(InfoExtractor):
|
||||
self.report_detected('JW Player embed')
|
||||
if not found:
|
||||
# Look for generic KVS player
|
||||
found = re.search(r'<script [^>]*?src="https://.+?/kt_player\.js\?v=(?P<ver>(?P<maj_ver>\d+)(\.\d+)+)".*?>', webpage)
|
||||
found = re.search(r'<script [^>]*?src="https?://.+?/kt_player\.js\?v=(?P<ver>(?P<maj_ver>\d+)(\.\d+)+)".*?>', webpage)
|
||||
if found:
|
||||
self.report_detected('KWS Player')
|
||||
if found.group('maj_ver') not in ['4', '5']:
|
||||
|
||||
122
yt_dlp/extractor/pixivsketch.py
Normal file
122
yt_dlp/extractor/pixivsketch.py
Normal file
@@ -0,0 +1,122 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
traverse_obj,
|
||||
unified_timestamp,
|
||||
)
|
||||
|
||||
|
||||
class PixivSketchBaseIE(InfoExtractor):
|
||||
def _call_api(self, video_id, path, referer, note='Downloading JSON metadata'):
|
||||
response = self._download_json(f'https://sketch.pixiv.net/api/{path}', video_id, note=note, headers={
|
||||
'Referer': referer,
|
||||
'X-Requested-With': referer,
|
||||
})
|
||||
errors = traverse_obj(response, ('errors', ..., 'message'))
|
||||
if errors:
|
||||
raise ExtractorError(' '.join(f'{e}.' for e in errors))
|
||||
return response.get('data') or {}
|
||||
|
||||
|
||||
class PixivSketchIE(PixivSketchBaseIE):
|
||||
IE_NAME = 'pixiv:sketch'
|
||||
_VALID_URL = r'https?://sketch\.pixiv\.net/@(?P<uploader_id>[a-zA-Z0-9_-]+)/lives/(?P<id>\d+)/?'
|
||||
_TESTS = [{
|
||||
'url': 'https://sketch.pixiv.net/@nuhutya/lives/3654620468641830507',
|
||||
'info_dict': {
|
||||
'id': '7370666691623196569',
|
||||
'title': 'まにあえクリスマス!',
|
||||
'uploader': 'ぬふちゃ',
|
||||
'uploader_id': 'nuhutya',
|
||||
'channel_id': '9844815',
|
||||
'age_limit': 0,
|
||||
'timestamp': 1640351536,
|
||||
},
|
||||
'skip': True,
|
||||
}, {
|
||||
# these two (age_limit > 0) requires you to login on website, but it's actually not required for download
|
||||
'url': 'https://sketch.pixiv.net/@namahyou/lives/4393103321546851377',
|
||||
'info_dict': {
|
||||
'id': '4907995960957946943',
|
||||
'title': 'クリスマスなんて知らん🖕',
|
||||
'uploader': 'すゃもり',
|
||||
'uploader_id': 'suya2mori2',
|
||||
'channel_id': '31169300',
|
||||
'age_limit': 15,
|
||||
'timestamp': 1640347640,
|
||||
},
|
||||
'skip': True,
|
||||
}, {
|
||||
'url': 'https://sketch.pixiv.net/@8aki/lives/3553803162487249670',
|
||||
'info_dict': {
|
||||
'id': '1593420639479156945',
|
||||
'title': 'おまけ本作業(リョナ有)',
|
||||
'uploader': 'おぶい / Obui',
|
||||
'uploader_id': 'oving',
|
||||
'channel_id': '17606',
|
||||
'age_limit': 18,
|
||||
'timestamp': 1640330263,
|
||||
},
|
||||
'skip': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id, uploader_id = self._match_valid_url(url).group('id', 'uploader_id')
|
||||
data = self._call_api(video_id, f'lives/{video_id}.json', url)
|
||||
|
||||
if not traverse_obj(data, 'is_broadcasting'):
|
||||
raise ExtractorError(f'This live is offline. Use https://sketch.pixiv.net/@{uploader_id} for ongoing live.', expected=True)
|
||||
|
||||
m3u8_url = traverse_obj(data, ('owner', 'hls_movie', 'url'))
|
||||
formats = self._extract_m3u8_formats(
|
||||
m3u8_url, video_id, ext='mp4',
|
||||
entry_protocol='m3u8_native', m3u8_id='hls')
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': data.get('name'),
|
||||
'formats': formats,
|
||||
'uploader': traverse_obj(data, ('user', 'name'), ('owner', 'user', 'name')),
|
||||
'uploader_id': traverse_obj(data, ('user', 'unique_name'), ('owner', 'user', 'unique_name')),
|
||||
'channel_id': str(traverse_obj(data, ('user', 'pixiv_user_id'), ('owner', 'user', 'pixiv_user_id'))),
|
||||
'age_limit': 18 if data.get('is_r18') else 15 if data.get('is_r15') else 0,
|
||||
'timestamp': unified_timestamp(data.get('created_at')),
|
||||
'is_live': True
|
||||
}
|
||||
|
||||
|
||||
class PixivSketchUserIE(PixivSketchBaseIE):
|
||||
IE_NAME = 'pixiv:sketch:user'
|
||||
_VALID_URL = r'https?://sketch\.pixiv\.net/@(?P<id>[a-zA-Z0-9_-]+)/?'
|
||||
_TESTS = [{
|
||||
'url': 'https://sketch.pixiv.net/@nuhutya',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://sketch.pixiv.net/@namahyou',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://sketch.pixiv.net/@8aki',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
@classmethod
|
||||
def suitable(cls, url):
|
||||
return super(PixivSketchUserIE, cls).suitable(url) and not PixivSketchIE.suitable(url)
|
||||
|
||||
def _real_extract(self, url):
|
||||
user_id = self._match_id(url)
|
||||
data = self._call_api(user_id, f'lives/users/@{user_id}.json', url)
|
||||
|
||||
if not traverse_obj(data, 'is_broadcasting'):
|
||||
try:
|
||||
self._call_api(user_id, 'users/current.json', url, 'Investigating reason for request failure')
|
||||
except ExtractorError as ex:
|
||||
if ex.cause and ex.cause.code == 401:
|
||||
self.raise_login_required(f'Please log in, or use direct link like https://sketch.pixiv.net/@{user_id}/1234567890', method='cookies')
|
||||
raise ExtractorError('This user is offline', expected=True)
|
||||
|
||||
return self.url_result(f'https://sketch.pixiv.net/@{user_id}/lives/{data["id"]}')
|
||||
@@ -447,7 +447,7 @@ class TikTokUserIE(TikTokBaseIE):
|
||||
for video in post_list.get('aweme_list', []):
|
||||
yield {
|
||||
**self._parse_aweme_video_app(video),
|
||||
'ie_key': TikTokIE.ie_key(),
|
||||
'extractor_key': TikTokIE.ie_key(),
|
||||
'extractor': 'TikTok',
|
||||
'webpage_url': f'https://tiktok.com/@{user_id}/video/{video["aweme_id"]}',
|
||||
}
|
||||
@@ -464,6 +464,114 @@ class TikTokUserIE(TikTokBaseIE):
|
||||
return self.playlist_result(self._entries_api(webpage, user_id, user_name), user_id, user_name)
|
||||
|
||||
|
||||
class TikTokBaseListIE(TikTokBaseIE):
|
||||
def _entries(self, list_id, display_id):
|
||||
query = {
|
||||
self._QUERY_NAME: list_id,
|
||||
'cursor': 0,
|
||||
'count': 20,
|
||||
'type': 5,
|
||||
'device_id': ''.join(random.choice(string.digits) for i in range(19))
|
||||
}
|
||||
|
||||
max_retries = self.get_param('extractor_retries', 3)
|
||||
for page in itertools.count(1):
|
||||
for retries in itertools.count():
|
||||
try:
|
||||
post_list = self._call_api(self._API_ENDPOINT, query, display_id,
|
||||
note='Downloading video list page %d%s' % (page, f' (attempt {retries})' if retries != 0 else ''),
|
||||
errnote='Unable to download video list')
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, json.JSONDecodeError) and e.cause.pos == 0 and retries != max_retries:
|
||||
self.report_warning('%s. Retrying...' % str(e.cause or e.msg))
|
||||
continue
|
||||
raise
|
||||
break
|
||||
for video in post_list.get('aweme_list', []):
|
||||
yield {
|
||||
**self._parse_aweme_video_app(video),
|
||||
'extractor_key': TikTokIE.ie_key(),
|
||||
'extractor': 'TikTok',
|
||||
'webpage_url': f'https://tiktok.com/@_/video/{video["aweme_id"]}',
|
||||
}
|
||||
if not post_list.get('has_more'):
|
||||
break
|
||||
query['cursor'] = post_list['cursor']
|
||||
|
||||
def _real_extract(self, url):
|
||||
list_id = self._match_id(url)
|
||||
return self.playlist_result(self._entries(list_id, list_id), list_id)
|
||||
|
||||
|
||||
class TikTokSoundIE(TikTokBaseListIE):
|
||||
IE_NAME = 'tiktok:sound'
|
||||
_VALID_URL = r'https?://(?:www\.)?tiktok\.com/music/[\w\.-]+-(?P<id>[\d]+)[/?#&]?'
|
||||
_QUERY_NAME = 'music_id'
|
||||
_API_ENDPOINT = 'music/aweme'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.tiktok.com/music/Build-a-Btch-6956990112127585029?lang=en',
|
||||
'playlist_mincount': 100,
|
||||
'info_dict': {
|
||||
'id': '6956990112127585029'
|
||||
},
|
||||
'expected_warnings': ['Retrying']
|
||||
}, {
|
||||
# Actual entries are less than listed video count
|
||||
'url': 'https://www.tiktok.com/music/jiefei-soap-remix-7036843036118469381',
|
||||
'playlist_mincount': 2182,
|
||||
'info_dict': {
|
||||
'id': '7036843036118469381'
|
||||
},
|
||||
'expected_warnings': ['Retrying']
|
||||
}]
|
||||
|
||||
|
||||
class TikTokEffectIE(TikTokBaseListIE):
|
||||
IE_NAME = 'tiktok:effect'
|
||||
_VALID_URL = r'https?://(?:www\.)?tiktok\.com/sticker/[\w\.-]+-(?P<id>[\d]+)[/?#&]?'
|
||||
_QUERY_NAME = 'sticker_id'
|
||||
_API_ENDPOINT = 'sticker/aweme'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.tiktok.com/sticker/MATERIAL-GWOOORL-1258156',
|
||||
'playlist_mincount': 100,
|
||||
'info_dict': {
|
||||
'id': '1258156',
|
||||
},
|
||||
'expected_warnings': ['Retrying']
|
||||
}, {
|
||||
# Different entries between mobile and web, depending on region
|
||||
'url': 'https://www.tiktok.com/sticker/Elf-Friend-479565',
|
||||
'only_matching': True
|
||||
}]
|
||||
|
||||
|
||||
class TikTokTagIE(TikTokBaseListIE):
|
||||
IE_NAME = 'tiktok:tag'
|
||||
_VALID_URL = r'https?://(?:www\.)?tiktok\.com/tag/(?P<id>[^/?#&]+)'
|
||||
_QUERY_NAME = 'ch_id'
|
||||
_API_ENDPOINT = 'challenge/aweme'
|
||||
_TESTS = [{
|
||||
'url': 'https://tiktok.com/tag/hello2018',
|
||||
'playlist_mincount': 39,
|
||||
'info_dict': {
|
||||
'id': '46294678',
|
||||
'title': 'hello2018',
|
||||
},
|
||||
'expected_warnings': ['Retrying']
|
||||
}, {
|
||||
'url': 'https://tiktok.com/tag/fypシ?is_copy_url=0&is_from_webapp=v1',
|
||||
'only_matching': True
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, display_id, headers={
|
||||
'User-Agent': 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)'
|
||||
})
|
||||
tag_id = self._html_search_regex(r'snssdk\d*://challenge/detail/(\d+)', webpage, 'tag ID')
|
||||
return self.playlist_result(self._entries(tag_id, display_id), tag_id, display_id)
|
||||
|
||||
|
||||
class DouyinIE(TikTokIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?douyin\.com/video/(?P<id>[0-9]+)'
|
||||
_TESTS = [{
|
||||
|
||||
@@ -1777,16 +1777,15 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
self._player_cache = {}
|
||||
|
||||
def _prepare_live_from_start_formats(self, formats, video_id, live_start_time, url, webpage_url, smuggled_data):
|
||||
EXPIRATION_DURATION = 18_000
|
||||
lock = threading.Lock()
|
||||
|
||||
is_live = True
|
||||
expiration_time = time.time() + EXPIRATION_DURATION
|
||||
start_time = time.time()
|
||||
formats = [f for f in formats if f.get('is_from_start')]
|
||||
|
||||
def refetch_manifest(format_id):
|
||||
nonlocal formats, expiration_time, is_live
|
||||
if time.time() <= expiration_time:
|
||||
def refetch_manifest(format_id, delay):
|
||||
nonlocal formats, start_time, is_live
|
||||
if time.time() <= start_time + delay:
|
||||
return
|
||||
|
||||
_, _, prs, player_url = self._download_player_responses(url, smuggled_data, video_id, webpage_url)
|
||||
@@ -1796,19 +1795,22 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
prs, (..., 'microformat', 'playerMicroformatRenderer'),
|
||||
expected_type=dict, default=[])
|
||||
_, is_live, _, formats = self._list_formats(video_id, microformats, video_details, prs, player_url)
|
||||
expiration_time = time.time() + EXPIRATION_DURATION
|
||||
start_time = time.time()
|
||||
|
||||
def mpd_feed(format_id):
|
||||
def mpd_feed(format_id, delay):
|
||||
"""
|
||||
@returns (manifest_url, manifest_stream_number, is_live) or None
|
||||
"""
|
||||
with lock:
|
||||
refetch_manifest(format_id)
|
||||
refetch_manifest(format_id, delay)
|
||||
|
||||
f = next((f for f in formats if f['format_id'] == format_id), None)
|
||||
if not f:
|
||||
self.report_warning(
|
||||
f'Cannot find refreshed manifest for format {format_id}{bug_reports_message()}')
|
||||
if not is_live:
|
||||
self.to_screen(f'{video_id}: Video is no longer live')
|
||||
else:
|
||||
self.report_warning(
|
||||
f'Cannot find refreshed manifest for format {format_id}{bug_reports_message()}')
|
||||
return None
|
||||
return f['manifest_url'], f['manifest_stream_number'], is_live
|
||||
|
||||
@@ -1839,9 +1841,15 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
nonlocal mpd_url, stream_number, is_live, no_fragment_score, fragments, fragment_base_url
|
||||
# Obtain from MPD's maximum seq value
|
||||
old_mpd_url = mpd_url
|
||||
mpd_url, stream_number, is_live = mpd_feed(format_id) or (mpd_url, stream_number, False)
|
||||
if old_mpd_url == mpd_url and not refresh_sequence:
|
||||
return True, last_seq
|
||||
last_error = ctx.pop('last_error', None)
|
||||
expire_fast = last_error and isinstance(last_error, compat_HTTPError) and last_error.code == 403
|
||||
mpd_url, stream_number, is_live = (mpd_feed(format_id, 5 if expire_fast else 18000)
|
||||
or (mpd_url, stream_number, False))
|
||||
if not refresh_sequence:
|
||||
if expire_fast and not is_live:
|
||||
return False, last_seq
|
||||
elif old_mpd_url == mpd_url:
|
||||
return True, last_seq
|
||||
try:
|
||||
fmts, _ = self._extract_mpd_formats_and_subtitles(
|
||||
mpd_url, None, note=False, errnote=False, fatal=False)
|
||||
@@ -1875,8 +1883,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
last_segment_url = None
|
||||
continue
|
||||
else:
|
||||
should_retry, last_seq = _extract_sequence_from_mpd(True)
|
||||
if not should_retry:
|
||||
should_continue, last_seq = _extract_sequence_from_mpd(True)
|
||||
if not should_continue:
|
||||
continue
|
||||
|
||||
if known_idx > last_seq:
|
||||
@@ -1893,9 +1901,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
try:
|
||||
for idx in range(known_idx, last_seq):
|
||||
# do not update sequence here or you'll get skipped some part of it
|
||||
should_retry, _ = _extract_sequence_from_mpd(False)
|
||||
if not should_retry:
|
||||
# retry when it gets weird state
|
||||
should_continue, _ = _extract_sequence_from_mpd(False)
|
||||
if not should_continue:
|
||||
known_idx = idx - 1
|
||||
raise ExtractorError('breaking out of outer loop')
|
||||
last_segment_url = urljoin(fragment_base_url, 'sq/%d' % idx)
|
||||
|
||||
@@ -23,7 +23,7 @@ class Zee5IE(InfoExtractor):
|
||||
zee5:|
|
||||
https?://(?:www\.)?zee5\.com/(?:[^#?]+/)?
|
||||
(?:
|
||||
(?:tvshows|kids|zee5originals)(?:/[^#/?]+){3}
|
||||
(?:tv-shows|kids|zee5originals)(?:/[^#/?]+){3}
|
||||
|movies/[^#/?]+
|
||||
)/(?P<display_id>[^#/?]+)/
|
||||
)
|
||||
@@ -37,48 +37,50 @@ class Zee5IE(InfoExtractor):
|
||||
'display_id': 'krishna-the-birth',
|
||||
'title': 'Krishna - The Birth',
|
||||
'duration': 4368,
|
||||
'average_rating': 4,
|
||||
'description': compat_str,
|
||||
'alt_title': 'Krishna - The Birth',
|
||||
'uploader': 'Zee Entertainment Enterprises Ltd',
|
||||
'release_date': '20060101',
|
||||
'upload_date': '20060101',
|
||||
'timestamp': 1136073600,
|
||||
'thumbnail': 'https://akamaividz.zee5.com/resources/0-0-63098/list/270x152/0063098_list_80888170.jpg',
|
||||
'thumbnail': r're:^https?://.*\.jpg$',
|
||||
'episode_number': 0,
|
||||
'episode': 'Episode 0',
|
||||
'tags': list
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://zee5.com/tvshows/details/krishna-balram/0-6-1871/episode-1-the-test-of-bramha/0-1-233402',
|
||||
'url': 'https://www.zee5.com/kids/kids-shows/bandbudh-aur-budbak/0-6-1899/yoga-se-hoga-bandbudh-aur-budbak/0-1-239839',
|
||||
'info_dict': {
|
||||
'id': '0-1-233402',
|
||||
'id': '0-1-239839',
|
||||
'ext': 'mp4',
|
||||
'display_id': 'episode-1-the-test-of-bramha',
|
||||
'title': 'Episode 1 - The Test Of Bramha',
|
||||
'duration': 1336,
|
||||
'average_rating': 4,
|
||||
'display_id': 'yoga-se-hoga-bandbudh-aur-budbak',
|
||||
'title': 'Yoga Se Hoga-Bandbudh aur Budbak',
|
||||
'duration': 659,
|
||||
'description': compat_str,
|
||||
'alt_title': 'Episode 1 - The Test Of Bramha',
|
||||
'alt_title': 'Yoga Se Hoga-Bandbudh aur Budbak',
|
||||
'uploader': 'Zee Entertainment Enterprises Ltd',
|
||||
'release_date': '20090101',
|
||||
'upload_date': '20090101',
|
||||
'timestamp': 1230768000,
|
||||
'thumbnail': 'https://akamaividz.zee5.com/resources/0-1-233402/list/270x152/01233402_list.jpg',
|
||||
'series': 'Krishna Balram',
|
||||
'release_date': '20150101',
|
||||
'upload_date': '20150101',
|
||||
'timestamp': 1420070400,
|
||||
'thumbnail': r're:^https?://.*\.jpg$',
|
||||
'series': 'Bandbudh Aur Budbak',
|
||||
'season_number': 1,
|
||||
'episode_number': 1,
|
||||
'episode': 'Episode 1',
|
||||
'season': 'Season 1',
|
||||
'tags': list,
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.zee5.com/hi/tvshows/details/kundali-bhagya/0-6-366/kundali-bhagya-march-08-2021/0-1-manual_7g9jv1os7730?country=IN',
|
||||
'url': 'https://www.zee5.com/hi/tv-shows/details/kundali-bhagya/0-6-366/kundali-bhagya-march-08-2021/0-1-manual_7g9jv1os7730?country=IN',
|
||||
'only_matching': True
|
||||
}, {
|
||||
'url': 'https://www.zee5.com/global/hi/tvshows/details/kundali-bhagya/0-6-366/kundali-bhagya-march-08-2021/0-1-manual_7g9jv1os7730',
|
||||
'url': 'https://www.zee5.com/global/hi/tv-shows/details/kundali-bhagya/0-6-366/kundali-bhagya-march-08-2021/0-1-manual_7g9jv1os7730',
|
||||
'only_matching': True
|
||||
}]
|
||||
_DETAIL_API_URL = 'https://spapi.zee5.com/singlePlayback/getDetails?content_id={}&device_id={}&platform_name=desktop_web&country=IN&check_parental_control=false'
|
||||
@@ -153,7 +155,6 @@ class Zee5IE(InfoExtractor):
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
'duration': int_or_none(asset_data.get('duration')),
|
||||
'average_rating': int_or_none(asset_data.get('rating')),
|
||||
'description': str_or_none(asset_data.get('description')),
|
||||
'alt_title': str_or_none(asset_data.get('original_title')),
|
||||
'uploader': str_or_none(asset_data.get('content_owner')),
|
||||
@@ -175,42 +176,42 @@ class Zee5SeriesIE(InfoExtractor):
|
||||
(?:
|
||||
zee5:series:|
|
||||
https?://(?:www\.)?zee5\.com/(?:[^#?]+/)?
|
||||
(?:tvshows|kids|zee5originals)(?:/[^#/?]+){2}/
|
||||
(?:tv-shows|kids|zee5originals)(?:/[^#/?]+){2}/
|
||||
)
|
||||
(?P<id>[^#/?]+)(?:/episodes)?/?(?:$|[?#])
|
||||
'''
|
||||
_TESTS = [{
|
||||
'url': 'https://www.zee5.com/kids/kids-shows/krishna-balram/0-6-1871',
|
||||
'playlist_mincount': 43,
|
||||
'url': 'https://www.zee5.com/kids/kids-shows/bandbudh-aur-budbak/0-6-1899',
|
||||
'playlist_mincount': 156,
|
||||
'info_dict': {
|
||||
'id': '0-6-1871',
|
||||
'id': '0-6-1899',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.zee5.com/tvshows/details/bhabi-ji-ghar-par-hai/0-6-199',
|
||||
'url': 'https://www.zee5.com/tv-shows/details/bhabi-ji-ghar-par-hai/0-6-199',
|
||||
'playlist_mincount': 1500,
|
||||
'info_dict': {
|
||||
'id': '0-6-199',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.zee5.com/tvshows/details/agent-raghav-crime-branch/0-6-965',
|
||||
'url': 'https://www.zee5.com/tv-shows/details/agent-raghav-crime-branch/0-6-965',
|
||||
'playlist_mincount': 24,
|
||||
'info_dict': {
|
||||
'id': '0-6-965',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.zee5.com/ta/tvshows/details/nagabhairavi/0-6-3201',
|
||||
'url': 'https://www.zee5.com/ta/tv-shows/details/nagabhairavi/0-6-3201',
|
||||
'playlist_mincount': 3,
|
||||
'info_dict': {
|
||||
'id': '0-6-3201',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.zee5.com/global/hi/tvshows/details/khwaabon-ki-zamin-par/0-6-270',
|
||||
'url': 'https://www.zee5.com/global/hi/tv-shows/details/khwaabon-ki-zamin-par/0-6-270',
|
||||
'playlist_mincount': 150,
|
||||
'info_dict': {
|
||||
'id': '0-6-270',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.zee5.com/tvshows/details/chala-hawa-yeu-dya-ladies-zindabaad/0-6-2943/episodes',
|
||||
'url': 'https://www.zee5.com/tv-shows/details/chala-hawa-yeu-dya-ladies-zindabaad/0-6-2943/episodes',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from .utils import (
|
||||
remove_end,
|
||||
write_string,
|
||||
)
|
||||
from .cookies import SUPPORTED_BROWSERS
|
||||
from .cookies import SUPPORTED_BROWSERS, SUPPORTED_KEYRINGS
|
||||
from .version import __version__
|
||||
|
||||
from .downloader.external import list_external_downloaders
|
||||
@@ -1023,7 +1023,7 @@ def parseOpts(overrideArguments=None):
|
||||
}, help=(
|
||||
'The paths where the files should be downloaded. '
|
||||
'Specify the type of file and the path separated by a colon ":". '
|
||||
'All the same types as --output are supported. '
|
||||
'All the same TYPES as --output are supported. '
|
||||
'Additionally, you can also provide "home" (default) and "temp" paths. '
|
||||
'All intermediary files are first downloaded to the temp path and '
|
||||
'then the final files are moved over to the home path after download is finished. '
|
||||
@@ -1174,14 +1174,15 @@ def parseOpts(overrideArguments=None):
|
||||
help='Do not read/dump cookies from/to file (default)')
|
||||
filesystem.add_option(
|
||||
'--cookies-from-browser',
|
||||
dest='cookiesfrombrowser', metavar='BROWSER[:PROFILE]',
|
||||
dest='cookiesfrombrowser', metavar='BROWSER[+KEYRING][:PROFILE]',
|
||||
help=(
|
||||
'Load cookies from a user profile of the given web browser. '
|
||||
'Currently supported browsers are: {}. '
|
||||
'You can specify the user profile name or directory using '
|
||||
'"BROWSER:PROFILE_NAME" or "BROWSER:PROFILE_PATH". '
|
||||
'If no profile is given, the most recently accessed one is used'.format(
|
||||
', '.join(sorted(SUPPORTED_BROWSERS)))))
|
||||
'The name of the browser and (optionally) the name/path of '
|
||||
'the profile to load cookies from, separated by a ":". '
|
||||
f'Currently supported browsers are: {", ".join(sorted(SUPPORTED_BROWSERS))}. '
|
||||
'By default, the most recently accessed profile is used. '
|
||||
'The keyring used for decrypting Chromium cookies on Linux can be '
|
||||
'(optionally) specified after the browser name separated by a "+". '
|
||||
f'Currently supported keyrings are: {", ".join(map(str.lower, sorted(SUPPORTED_KEYRINGS)))}'))
|
||||
filesystem.add_option(
|
||||
'--no-cookies-from-browser',
|
||||
action='store_const', const=None, dest='cookiesfrombrowser',
|
||||
|
||||
@@ -145,8 +145,43 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
||||
self.report_warning('unable to embed using mutagen; %s' % error_to_compat_str(err))
|
||||
success = False
|
||||
|
||||
# Method 2: Use ffmpeg+ffprobe
|
||||
if not success and not prefer_atomicparsley:
|
||||
# Method 2: Use AtomicParsley
|
||||
if not success:
|
||||
success = True
|
||||
atomicparsley = next((
|
||||
x for x in ['AtomicParsley', 'atomicparsley']
|
||||
if check_executable(x, ['-v'])), None)
|
||||
if atomicparsley is None:
|
||||
self.to_screen('Neither mutagen nor AtomicParsley was found. Falling back to ffmpeg')
|
||||
success = False
|
||||
else:
|
||||
if not prefer_atomicparsley:
|
||||
self.to_screen('mutagen was not found. Falling back to AtomicParsley')
|
||||
cmd = [encodeFilename(atomicparsley, True),
|
||||
encodeFilename(filename, True),
|
||||
encodeArgument('--artwork'),
|
||||
encodeFilename(thumbnail_filename, True),
|
||||
encodeArgument('-o'),
|
||||
encodeFilename(temp_filename, True)]
|
||||
cmd += [encodeArgument(o) for o in self._configuration_args('AtomicParsley')]
|
||||
|
||||
self._report_run('atomicparsley', filename)
|
||||
self.write_debug('AtomicParsley command line: %s' % shell_quote(cmd))
|
||||
p = Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout, stderr = p.communicate_or_kill()
|
||||
if p.returncode != 0:
|
||||
msg = stderr.decode('utf-8', 'replace').strip()
|
||||
self.report_warning(f'Unable to embed thumbnails using AtomicParsley; {msg}')
|
||||
# for formats that don't support thumbnails (like 3gp) AtomicParsley
|
||||
# won't create to the temporary file
|
||||
if b'No changes' in stdout:
|
||||
self.report_warning('The file format doesn\'t support embedding a thumbnail')
|
||||
success = False
|
||||
|
||||
# Method 3: Use ffmpeg+ffprobe
|
||||
# Thumbnails attached using this method doesn't show up as cover in some cases
|
||||
# See https://github.com/yt-dlp/yt-dlp/issues/2125, https://github.com/yt-dlp/yt-dlp/issues/411
|
||||
if not success:
|
||||
success = True
|
||||
try:
|
||||
options = ['-c', 'copy', '-map', '0', '-dn', '-map', '1']
|
||||
@@ -161,38 +196,8 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
||||
self._report_run('ffmpeg', filename)
|
||||
self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options)
|
||||
except PostProcessingError as err:
|
||||
self.report_warning('unable to embed using ffprobe & ffmpeg; %s' % error_to_compat_str(err))
|
||||
success = False
|
||||
|
||||
# Method 3: Use AtomicParsley
|
||||
if not success:
|
||||
success = True
|
||||
atomicparsley = next((
|
||||
x for x in ['AtomicParsley', 'atomicparsley']
|
||||
if check_executable(x, ['-v'])), None)
|
||||
if atomicparsley is None:
|
||||
raise EmbedThumbnailPPError('AtomicParsley was not found. Please install')
|
||||
|
||||
cmd = [encodeFilename(atomicparsley, True),
|
||||
encodeFilename(filename, True),
|
||||
encodeArgument('--artwork'),
|
||||
encodeFilename(thumbnail_filename, True),
|
||||
encodeArgument('-o'),
|
||||
encodeFilename(temp_filename, True)]
|
||||
cmd += [encodeArgument(o) for o in self._configuration_args('AtomicParsley')]
|
||||
|
||||
self._report_run('atomicparsley', filename)
|
||||
self.write_debug('AtomicParsley command line: %s' % shell_quote(cmd))
|
||||
p = Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout, stderr = p.communicate_or_kill()
|
||||
if p.returncode != 0:
|
||||
msg = stderr.decode('utf-8', 'replace').strip()
|
||||
raise EmbedThumbnailPPError(msg)
|
||||
# for formats that don't support thumbnails (like 3gp) AtomicParsley
|
||||
# won't create to the temporary file
|
||||
if b'No changes' in stdout:
|
||||
self.report_warning('The file format doesn\'t support embedding a thumbnail')
|
||||
success = False
|
||||
raise EmbedThumbnailPPError(f'Unable to embed using ffprobe & ffmpeg; {err}')
|
||||
|
||||
elif info['ext'] in ['ogg', 'opus', 'flac']:
|
||||
if not has_mutagen:
|
||||
|
||||
@@ -296,8 +296,8 @@ class FFmpegPostProcessor(PostProcessor):
|
||||
keys = ['_%s%d' % (name, number), '_%s' % name]
|
||||
if name == 'o':
|
||||
args += ['-movflags', '+faststart']
|
||||
elif number == 1:
|
||||
keys.append('')
|
||||
if number == 1:
|
||||
keys.append('')
|
||||
args += self._configuration_args(self.basename, keys)
|
||||
if name == 'i':
|
||||
args.append('-i')
|
||||
@@ -1092,12 +1092,12 @@ class FFmpegThumbnailsConvertorPP(FFmpegPostProcessor):
|
||||
files_to_delete = []
|
||||
has_thumbnail = False
|
||||
|
||||
for idx, thumbnail_dict in enumerate(info['thumbnails']):
|
||||
if 'filepath' not in thumbnail_dict:
|
||||
for idx, thumbnail_dict in enumerate(info.get('thumbnails') or []):
|
||||
original_thumbnail = thumbnail_dict.get('filepath')
|
||||
if not original_thumbnail:
|
||||
continue
|
||||
has_thumbnail = True
|
||||
self.fixup_webp(info, idx)
|
||||
original_thumbnail = thumbnail_dict['filepath']
|
||||
_, thumbnail_ext = os.path.splitext(original_thumbnail)
|
||||
if thumbnail_ext:
|
||||
thumbnail_ext = thumbnail_ext[1:].lower()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Autogenerated by devscripts/update-version.py
|
||||
|
||||
__version__ = '2021.12.25'
|
||||
__version__ = '2021.12.27'
|
||||
|
||||
RELEASE_GIT_HEAD = '87e049962'
|
||||
RELEASE_GIT_HEAD = '6223f67a8'
|
||||
|
||||
Reference in New Issue
Block a user