Compare commits

...
17 Commits
Author SHA1 Message Date
github-actions 1e5c83b26b [version] update
Created by: pukkandan

:ci skip all
2021-12-27 02:30:03 +00:00
pukkandan 6223f67a8c Release 2021.12.27 2021-12-27 07:36:23 +05:30
pukkandan 6a34813a0d [docs] Add examples for using TYPES: in -P/-o 2021-12-27 07:26:39 +05:30
Matt BroadwayandGitHub f59f5ef8b6 [cookies] Support other keyrings (#2032)
Authored by: mbway
2021-12-27 06:58:44 +05:30
pukkandan f44afb54ef [aria2c] Don't show progress when --no-progress 2021-12-27 04:27:34 +05:30
pukkandan 77cee0f188 [EmbedThumbnail] Prefer AtomicParsley over ffmpeg if available 2021-12-27 03:49:43 +05:30
pukkandan 6a17677577 [ThumbnailsConvertor] Fix for when there are no thumbnails
Closes #2125
2021-12-27 03:18:31 +05:30
Ashish Gupta ee7b9bdf5d [Zee5] Fix VALID_URL for tv-shows 2021-12-26 20:01:43 +05:30
pukkandan 185bf31070 [youtube] End live-from-start properly when stream ends with 403
Closes #2089
2021-12-26 16:14:00 +05:30
pukkandan 0b77924a38 [tiktok] Fix extractor_key used in archive 2021-12-26 15:10:09 +05:30
MinePlayersPEandGitHub 8126298c1b [TikTok] Add music, sticker and tag IEs (#2119)
Closes #1752
Authored by: MinePlayersPE
2021-12-26 14:23:19 +05:30
pukkandan 6da22e7d4f Avoid recursion error when re-extracting info 2021-12-26 04:20:16 +05:30
MinePlayersPEandGitHub c62ecf0d90 [BiliIntl] Fix extractor (#2077)
Closes #1744
Authored by: MinePlayersPE
2021-12-26 04:11:38 +05:30
The Hatsune DaishiandGitHub 3774f4f427 [PixivSketch] Add extractors (#2104)
Authored by: nao20010128nao
2021-12-26 01:46:24 +05:30
git-anony-mouseandGitHub 9980d3d213 [generic] Fix HTTP KVS Player (#2111)
Authored by: git-anony-mouse
2021-12-25 08:48:19 +05:30
pukkandan 8eb4b1bb8e [ffmpeg] Fix position of --ppa
Bug in ca5db158ae
Closes #2112
2021-12-25 08:42:08 +05:30
pukkandan 332da56f52 [CBC] Fix URL regex
Closes #2110
2021-12-25 07:53:38 +05:30
27 changed files with 851 additions and 278 deletions
+3 -3
View File
@@ -11,7 +11,7 @@ body:
options: options:
- label: I'm reporting a broken site - label: I'm reporting a broken site
required: true 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 required: true
- label: I've checked that all provided URLs are alive and playable in a browser - label: I've checked that all provided URLs are alive and playable in a browser
required: true required: true
@@ -51,12 +51,12 @@ body:
[debug] Portable config file: yt-dlp.conf [debug] Portable config file: yt-dlp.conf
[debug] Portable config: ['-i'] [debug] Portable config: ['-i']
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252 [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] 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] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets [debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
[debug] Proxy map: {} [debug] Proxy map: {}
yt-dlp is up to date (2021.12.25) yt-dlp is up to date (2021.12.27)
<more lines> <more lines>
render: shell render: shell
validations: validations:
@@ -11,7 +11,7 @@ body:
options: options:
- label: I'm reporting a new site support request - label: I'm reporting a new site support request
required: true 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 required: true
- label: I've checked that all provided URLs are alive and playable in a browser - label: I've checked that all provided URLs are alive and playable in a browser
required: true required: true
@@ -62,12 +62,12 @@ body:
[debug] Portable config file: yt-dlp.conf [debug] Portable config file: yt-dlp.conf
[debug] Portable config: ['-i'] [debug] Portable config: ['-i']
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252 [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] 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] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets [debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
[debug] Proxy map: {} [debug] Proxy map: {}
yt-dlp is up to date (2021.12.25) yt-dlp is up to date (2021.12.27)
<more lines> <more lines>
render: shell render: shell
validations: validations:
@@ -11,7 +11,7 @@ body:
options: options:
- label: I'm reporting a site feature request - label: I'm reporting a site feature request
required: true 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 required: true
- label: I've checked that all provided URLs are alive and playable in a browser - label: I've checked that all provided URLs are alive and playable in a browser
required: true required: true
@@ -60,12 +60,12 @@ body:
[debug] Portable config file: yt-dlp.conf [debug] Portable config file: yt-dlp.conf
[debug] Portable config: ['-i'] [debug] Portable config: ['-i']
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252 [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] 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] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets [debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
[debug] Proxy map: {} [debug] Proxy map: {}
yt-dlp is up to date (2021.12.25) yt-dlp is up to date (2021.12.27)
<more lines> <more lines>
render: shell render: shell
validations: validations:
+3 -3
View File
@@ -11,7 +11,7 @@ body:
options: options:
- label: I'm reporting a bug unrelated to a specific site - label: I'm reporting a bug unrelated to a specific site
required: true 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 required: true
- label: I've checked that all provided URLs are alive and playable in a browser - label: I've checked that all provided URLs are alive and playable in a browser
required: true required: true
@@ -45,12 +45,12 @@ body:
[debug] Portable config file: yt-dlp.conf [debug] Portable config file: yt-dlp.conf
[debug] Portable config: ['-i'] [debug] Portable config: ['-i']
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252 [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] 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] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets [debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
[debug] Proxy map: {} [debug] Proxy map: {}
yt-dlp is up to date (2021.12.25) yt-dlp is up to date (2021.12.27)
<more lines> <more lines>
render: shell render: shell
validations: validations:
+1 -1
View File
@@ -11,7 +11,7 @@ body:
options: options:
- label: I'm reporting a feature request - label: I'm reporting a feature request
required: true 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 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 - 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 required: true
+1
View File
@@ -177,3 +177,4 @@ Sematre
jaller94 jaller94
r5d r5d
julien-hadleyjack julien-hadleyjack
git-anony-mouse
+19 -1
View File
@@ -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 ### 2021.12.25
* [dash,youtube] **Download live from start to end** by [nao20010128nao](https://github.com/nao20010128nao), [pukkandan](https://github.com/pukkandan) * [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: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 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] 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` * [youtube] Fix `ytsearchdate`
* [zdf] Support videos with different ptmd location by [iw0nderhow](https://github.com/iw0nderhow) * [zdf] Support videos with different ptmd location by [iw0nderhow](https://github.com/iw0nderhow)
* [zee5] Support /episodes in URL * [zee5] Support /episodes in URL
+24 -14
View File
@@ -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)) * 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` * 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` * **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) * [**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) * [**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) * [**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) * [**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) * [**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) * [**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 -P, --paths [TYPES:]PATH The paths where the files should be
downloaded. Specify the type of file and downloaded. Specify the type of file and
the path separated by a colon ":". All the 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" Additionally, you can also provide "home"
(default) and "temp" paths. All (default) and "temp" paths. All
intermediary files are first downloaded to 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 from and dump cookie jar in
--no-cookies Do not read/dump cookies from/to file --no-cookies Do not read/dump cookies from/to file
(default) (default)
--cookies-from-browser BROWSER[:PROFILE] --cookies-from-browser BROWSER[+KEYRING][:PROFILE]
Load cookies from a user profile of the The name of the browser and (optionally)
given web browser. Currently supported the name/path of the profile to load
browsers are: brave, chrome, chromium, cookies from, separated by a ":". Currently
edge, firefox, opera, safari, vivaldi. You supported browsers are: brave, chrome,
can specify the user profile name or chromium, edge, firefox, opera, safari,
directory using "BROWSER:PROFILE_NAME" or vivaldi. By default, the most recently
"BROWSER:PROFILE_PATH". If no profile is accessed profile is used. The keyring used
given, the most recently accessed one is for decrypting Chromium cookies on Linux
used 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) --no-cookies-from-browser Do not load cookies from browser (default)
--cache-dir DIR Location in the filesystem where youtube-dl --cache-dir DIR Location in the filesystem where youtube-dl
can store some downloaded information (such 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" $ 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 # 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: # 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" $ 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 # 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" $ 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 # Stream the video being downloaded to stdout
$ yt-dlp -o - BaW_jenozKc $ yt-dlp -o - BaW_jenozKc
``` ```
+5
View File
@@ -836,6 +836,8 @@
- **Pinkbike** - **Pinkbike**
- **Pinterest** - **Pinterest**
- **PinterestCollection** - **PinterestCollection**
- **pixiv:sketch**
- **pixiv:sketch:user**
- **Pladform** - **Pladform**
- **PlanetMarathi** - **PlanetMarathi**
- **Platzi** - **Platzi**
@@ -1141,6 +1143,9 @@
- **ThreeSpeak** - **ThreeSpeak**
- **ThreeSpeakUser** - **ThreeSpeakUser**
- **TikTok** - **TikTok**
- **tiktok:effect**
- **tiktok:sound**
- **tiktok:tag**
- **tiktok:user** - **tiktok:user**
- **tinypic**: tinypic.com videos - **tinypic**: tinypic.com videos
- **TMZ** - **TMZ**
+34 -2
View File
@@ -8,6 +8,8 @@ from yt_dlp.cookies import (
WindowsChromeCookieDecryptor, WindowsChromeCookieDecryptor,
parse_safari_cookies, parse_safari_cookies,
pbkdf2_sha1, pbkdf2_sha1,
_get_linux_desktop_environment,
_LinuxDesktopEnvironment,
) )
@@ -42,6 +44,37 @@ class MonkeyPatch:
class TestCookies(unittest.TestCase): 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): def test_chrome_cookie_decryptor_linux_derive_key(self):
key = LinuxChromeCookieDecryptor.derive_key(b'abc') key = LinuxChromeCookieDecryptor.derive_key(b'abc')
self.assertEqual(key, b'7\xa1\xec\xd4m\xfcA\xc7\xb19Z\xd0\x19\xdcM\x17') 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) self.assertEqual(decryptor.decrypt(encrypted_value), value)
def test_chrome_cookie_decryptor_linux_v11(self): def test_chrome_cookie_decryptor_linux_v11(self):
with MonkeyPatch(cookies, {'_get_linux_keyring_password': lambda *args, **kwargs: b'', with MonkeyPatch(cookies, {'_get_linux_keyring_password': lambda *args, **kwargs: b''}):
'KEYRING_AVAILABLE': True}):
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' 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' value = 'tz=Europe.London'
decryptor = LinuxChromeCookieDecryptor('Chrome', Logger()) decryptor = LinuxChromeCookieDecryptor('Chrome', Logger())
+32 -30
View File
@@ -317,10 +317,10 @@ class YoutubeDL(object):
break_per_url: Whether break_on_reject and break_on_existing break_per_url: Whether break_on_reject and break_on_existing
should act on each input URL as opposed to for the entire queue 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 cookiefile: File name where cookies should be read from and dumped to
cookiesfrombrowser: A tuple containing the name of the browser and the profile cookiesfrombrowser: A tuple containing the name of the browser, the profile
name/path from where cookies are loaded. name/pathfrom where cookies are loaded, and the name of the
Eg: ('chrome', ) or ('vivaldi', 'default') keyring. Eg: ('chrome', ) or ('vivaldi', 'default', 'BASICTEXT')
nocheckcertificate:Do not verify SSL certificates nocheckcertificate: Do not verify SSL certificates
prefer_insecure: Use HTTP instead of HTTPS to retrieve information. prefer_insecure: Use HTTP instead of HTTPS to retrieve information.
At the moment, this is only supported by YouTube. At the moment, this is only supported by YouTube.
proxy: URL of the proxy server to use proxy: URL of the proxy server to use
@@ -1348,31 +1348,33 @@ class YoutubeDL(object):
def __handle_extraction_exceptions(func): def __handle_extraction_exceptions(func):
@functools.wraps(func) @functools.wraps(func)
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
try: while True:
return func(self, *args, **kwargs) try:
except GeoRestrictedError as e: return func(self, *args, **kwargs)
msg = e.msg except (DownloadCancelled, LazyList.IndexError, PagedList.IndexError):
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:
raise 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 return wrapper
def _wait_for_video(self, ie_result): def _wait_for_video(self, ie_result):
@@ -3540,11 +3542,11 @@ class YoutubeDL(object):
from .downloader.websocket import has_websockets from .downloader.websocket import has_websockets
from .postprocessor.embedthumbnail import has_mutagen from .postprocessor.embedthumbnail import has_mutagen
from .cookies import SQLITE_AVAILABLE, KEYRING_AVAILABLE from .cookies import SQLITE_AVAILABLE, SECRETSTORAGE_AVAILABLE
lib_str = join_nonempty( lib_str = join_nonempty(
compat_pycrypto_AES and compat_pycrypto_AES.__name__.split('.')[0], compat_pycrypto_AES and compat_pycrypto_AES.__name__.split('.')[0],
KEYRING_AVAILABLE and 'keyring', SECRETSTORAGE_AVAILABLE and 'secretstorage',
has_mutagen and 'mutagen', has_mutagen and 'mutagen',
SQLITE_AVAILABLE and 'sqlite', SQLITE_AVAILABLE and 'sqlite',
has_websockets and 'websockets', has_websockets and 'websockets',
+15 -5
View File
@@ -22,7 +22,7 @@ from .compat import (
compat_shlex_quote, compat_shlex_quote,
workaround_optparse_bug9161, workaround_optparse_bug9161,
) )
from .cookies import SUPPORTED_BROWSERS from .cookies import SUPPORTED_BROWSERS, SUPPORTED_KEYRINGS
from .utils import ( from .utils import (
DateRange, DateRange,
decodeOption, decodeOption,
@@ -266,10 +266,20 @@ def _real_main(argv=None):
if opts.convertthumbnails not in FFmpegThumbnailsConvertorPP.SUPPORTED_EXTS: if opts.convertthumbnails not in FFmpegThumbnailsConvertorPP.SUPPORTED_EXTS:
parser.error('invalid thumbnail format specified') parser.error('invalid thumbnail format specified')
if opts.cookiesfrombrowser is not None: if opts.cookiesfrombrowser is not None:
opts.cookiesfrombrowser = [ mobj = re.match(r'(?P<name>[^+:]+)(\s*\+\s*(?P<keyring>[^:]+))?(\s*:(?P<profile>.+))?', opts.cookiesfrombrowser)
part.strip() or None for part in opts.cookiesfrombrowser.split(':', 1)] if mobj is None:
if opts.cookiesfrombrowser[0].lower() not in SUPPORTED_BROWSERS: parser.error(f'invalid cookies from browser arguments: {opts.cookiesfrombrowser}')
parser.error('unsupported browser specified for cookies') 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 geo_bypass_code = opts.geo_bypass_ip_block or opts.geo_bypass_country
if geo_bypass_code is not None: if geo_bypass_code is not None:
try: try:
+264 -56
View File
@@ -1,3 +1,4 @@
import contextlib
import ctypes import ctypes
import json import json
import os import os
@@ -7,6 +8,7 @@ import subprocess
import sys import sys
import tempfile import tempfile
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from enum import Enum, auto
from hashlib import pbkdf2_hmac from hashlib import pbkdf2_hmac
from .aes import aes_cbc_decrypt_bytes, aes_gcm_decrypt_and_verify_bytes from .aes import aes_cbc_decrypt_bytes, aes_gcm_decrypt_and_verify_bytes
@@ -15,7 +17,6 @@ from .compat import (
compat_cookiejar_Cookie, compat_cookiejar_Cookie,
) )
from .utils import ( from .utils import (
bug_reports_message,
expand_path, expand_path,
Popen, Popen,
YoutubeDLCookieJar, YoutubeDLCookieJar,
@@ -31,19 +32,16 @@ except ImportError:
try: try:
import keyring import secretstorage
KEYRING_AVAILABLE = True SECRETSTORAGE_AVAILABLE = True
KEYRING_UNAVAILABLE_REASON = f'due to unknown reasons{bug_reports_message()}'
except ImportError: except ImportError:
KEYRING_AVAILABLE = False SECRETSTORAGE_AVAILABLE = False
KEYRING_UNAVAILABLE_REASON = ( SECRETSTORAGE_UNAVAILABLE_REASON = (
'as the `keyring` module is not installed. ' 'as the `secretstorage` module is not installed. '
'Please install by running `python3 -m pip install keyring`. ' 'Please install by running `python3 -m pip install secretstorage`.')
'Depending on your platform, additional packages may be required '
'to access the keyring; see https://pypi.org/project/keyring')
except Exception as _err: except Exception as _err:
KEYRING_AVAILABLE = False SECRETSTORAGE_AVAILABLE = False
KEYRING_UNAVAILABLE_REASON = 'as the `keyring` module could not be initialized: %s' % _err SECRETSTORAGE_UNAVAILABLE_REASON = f'as the `secretstorage` module could not be initialized. {_err}'
CHROMIUM_BASED_BROWSERS = {'brave', 'chrome', 'chromium', 'edge', 'opera', 'vivaldi'} CHROMIUM_BASED_BROWSERS = {'brave', 'chrome', 'chromium', 'edge', 'opera', 'vivaldi'}
@@ -74,8 +72,8 @@ class YDLLogger:
def load_cookies(cookie_file, browser_specification, ydl): def load_cookies(cookie_file, browser_specification, ydl):
cookie_jars = [] cookie_jars = []
if browser_specification is not None: if browser_specification is not None:
browser_name, profile = _parse_browser_specification(*browser_specification) browser_name, profile, keyring = _parse_browser_specification(*browser_specification)
cookie_jars.append(extract_cookies_from_browser(browser_name, profile, YDLLogger(ydl))) cookie_jars.append(extract_cookies_from_browser(browser_name, profile, YDLLogger(ydl), keyring=keyring))
if cookie_file is not None: if cookie_file is not None:
cookie_file = expand_path(cookie_file) cookie_file = expand_path(cookie_file)
@@ -87,13 +85,13 @@ def load_cookies(cookie_file, browser_specification, ydl):
return _merge_cookie_jars(cookie_jars) 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': if browser_name == 'firefox':
return _extract_firefox_cookies(profile, logger) return _extract_firefox_cookies(profile, logger)
elif browser_name == 'safari': elif browser_name == 'safari':
return _extract_safari_cookies(profile, logger) return _extract_safari_cookies(profile, logger)
elif browser_name in CHROMIUM_BASED_BROWSERS: 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: else:
raise ValueError('unknown browser: {}'.format(browser_name)) 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)) logger.info('Extracting cookies from {}'.format(browser_name))
if not SQLITE_AVAILABLE: 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)) raise FileNotFoundError('could not find {} cookies database in "{}"'.format(browser_name, search_root))
logger.debug('Extracting cookies from: "{}"'.format(cookie_database_path)) 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: with tempfile.TemporaryDirectory(prefix='yt_dlp') as tmpdir:
cursor = None cursor = None
@@ -247,6 +245,7 @@ def _extract_chrome_cookies(browser_name, profile, logger):
'expires_utc, {} FROM cookies'.format(secure_column)) 'expires_utc, {} FROM cookies'.format(secure_column))
jar = YoutubeDLCookieJar() jar = YoutubeDLCookieJar()
failed_cookies = 0 failed_cookies = 0
unencrypted_cookies = 0
for host_key, name, value, encrypted_value, path, expires_utc, is_secure in cursor.fetchall(): for host_key, name, value, encrypted_value, path, expires_utc, is_secure in cursor.fetchall():
host_key = host_key.decode('utf-8') host_key = host_key.decode('utf-8')
name = name.decode('utf-8') name = name.decode('utf-8')
@@ -258,6 +257,8 @@ def _extract_chrome_cookies(browser_name, profile, logger):
if value is None: if value is None:
failed_cookies += 1 failed_cookies += 1
continue continue
else:
unencrypted_cookies += 1
cookie = compat_cookiejar_Cookie( cookie = compat_cookiejar_Cookie(
version=0, name=name, value=value, port=None, port_specified=False, version=0, name=name, value=value, port=None, port_specified=False,
@@ -270,6 +271,9 @@ def _extract_chrome_cookies(browser_name, profile, logger):
else: else:
failed_message = '' failed_message = ''
logger.info('Extracted {} cookies from {}{}'.format(len(jar), browser_name, 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 return jar
finally: finally:
if cursor is not None: if cursor is not None:
@@ -305,10 +309,14 @@ class ChromeCookieDecryptor:
def decrypt(self, encrypted_value): def decrypt(self, encrypted_value):
raise NotImplementedError 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'): if sys.platform in ('linux', 'linux2'):
return LinuxChromeCookieDecryptor(browser_keyring_name, logger) return LinuxChromeCookieDecryptor(browser_keyring_name, logger, keyring=keyring)
elif sys.platform == 'darwin': elif sys.platform == 'darwin':
return MacChromeCookieDecryptor(browser_keyring_name, logger) return MacChromeCookieDecryptor(browser_keyring_name, logger)
elif sys.platform == 'win32': elif sys.platform == 'win32':
@@ -319,13 +327,12 @@ def get_cookie_decryptor(browser_root, browser_keyring_name, logger):
class LinuxChromeCookieDecryptor(ChromeCookieDecryptor): class LinuxChromeCookieDecryptor(ChromeCookieDecryptor):
def __init__(self, browser_keyring_name, logger): def __init__(self, browser_keyring_name, logger, *, keyring=None):
self._logger = logger self._logger = logger
self._v10_key = self.derive_key(b'peanuts') self._v10_key = self.derive_key(b'peanuts')
if KEYRING_AVAILABLE: password = _get_linux_keyring_password(browser_keyring_name, keyring, logger)
self._v11_key = self.derive_key(_get_linux_keyring_password(browser_keyring_name)) self._v11_key = None if password is None else self.derive_key(password)
else: self._cookie_counts = {'v10': 0, 'v11': 0, 'other': 0}
self._v11_key = None
@staticmethod @staticmethod
def derive_key(password): 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 # 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) 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): def decrypt(self, encrypted_value):
version = encrypted_value[:3] version = encrypted_value[:3]
ciphertext = encrypted_value[3:] ciphertext = encrypted_value[3:]
if version == b'v10': if version == b'v10':
self._cookie_counts['v10'] += 1
return _decrypt_aes_cbc(ciphertext, self._v10_key, self._logger) return _decrypt_aes_cbc(ciphertext, self._v10_key, self._logger)
elif version == b'v11': elif version == b'v11':
self._cookie_counts['v11'] += 1
if self._v11_key is None: 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 None
return _decrypt_aes_cbc(ciphertext, self._v11_key, self._logger) return _decrypt_aes_cbc(ciphertext, self._v11_key, self._logger)
else: else:
self._cookie_counts['other'] += 1
return None return None
@@ -355,6 +369,7 @@ class MacChromeCookieDecryptor(ChromeCookieDecryptor):
self._logger = logger self._logger = logger
password = _get_mac_keyring_password(browser_keyring_name, logger) password = _get_mac_keyring_password(browser_keyring_name, logger)
self._v10_key = None if password is None else self.derive_key(password) self._v10_key = None if password is None else self.derive_key(password)
self._cookie_counts = {'v10': 0, 'other': 0}
@staticmethod @staticmethod
def derive_key(password): 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 # 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) 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): def decrypt(self, encrypted_value):
version = encrypted_value[:3] version = encrypted_value[:3]
ciphertext = encrypted_value[3:] ciphertext = encrypted_value[3:]
if version == b'v10': if version == b'v10':
self._cookie_counts['v10'] += 1
if self._v10_key is None: if self._v10_key is None:
self._logger.warning('cannot decrypt v10 cookies: no key found', only_once=True) self._logger.warning('cannot decrypt v10 cookies: no key found', only_once=True)
return None return None
@@ -374,6 +394,7 @@ class MacChromeCookieDecryptor(ChromeCookieDecryptor):
return _decrypt_aes_cbc(ciphertext, self._v10_key, self._logger) return _decrypt_aes_cbc(ciphertext, self._v10_key, self._logger)
else: else:
self._cookie_counts['other'] += 1
# other prefixes are considered 'old data' which were stored as plaintext # 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 # https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/os_crypt_mac.mm
return encrypted_value return encrypted_value
@@ -383,12 +404,18 @@ class WindowsChromeCookieDecryptor(ChromeCookieDecryptor):
def __init__(self, browser_root, logger): def __init__(self, browser_root, logger):
self._logger = logger self._logger = logger
self._v10_key = _get_windows_v10_key(browser_root, 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): def decrypt(self, encrypted_value):
version = encrypted_value[:3] version = encrypted_value[:3]
ciphertext = encrypted_value[3:] ciphertext = encrypted_value[3:]
if version == b'v10': if version == b'v10':
self._cookie_counts['v10'] += 1
if self._v10_key is None: if self._v10_key is None:
self._logger.warning('cannot decrypt v10 cookies: no key found', only_once=True) self._logger.warning('cannot decrypt v10 cookies: no key found', only_once=True)
return None return None
@@ -408,6 +435,7 @@ class WindowsChromeCookieDecryptor(ChromeCookieDecryptor):
return _decrypt_aes_gcm(ciphertext, self._v10_key, nonce, authentication_tag, self._logger) return _decrypt_aes_gcm(ciphertext, self._v10_key, nonce, authentication_tag, self._logger)
else: else:
self._cookie_counts['other'] += 1
# any other prefix means the data is DPAPI encrypted # 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 # 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') 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 return jar
def _get_linux_keyring_password(browser_keyring_name): class _LinuxDesktopEnvironment(Enum):
password = keyring.get_password('{} Keys'.format(browser_keyring_name), """
'{} Safe Storage'.format(browser_keyring_name)) https://chromium.googlesource.com/chromium/src/+/refs/heads/main/base/nix/xdg_util.h
if password is None: DesktopEnvironment
# 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 OTHER = auto()
# to verify this: CINNAMON = auto()
# dbus-monitor "interface='org.kde.KWallet'" "type=method_return" GNOME = auto()
# while starting chrome. KDE = auto()
# this may be a bug as the intended behaviour is to generate a random password and store PANTHEON = auto()
# it, but that doesn't matter here. UNITY = auto()
password = '' XFCE = auto()
return password.encode('utf-8')
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): def _get_mac_keyring_password(browser_keyring_name, logger):
if KEYRING_AVAILABLE: logger.debug('using find-generic-password to obtain password from OSX keychain')
logger.debug('using keyring to obtain password') try:
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')
proc = Popen( proc = Popen(
['security', 'find-generic-password', ['security', 'find-generic-password',
'-w', # write password to stdout '-w', # write password to stdout
'-a', browser_keyring_name, # match 'account' '-a', browser_keyring_name, # match 'account'
'-s', '{} Safe Storage'.format(browser_keyring_name)], # match 'service' '-s', '{} Safe Storage'.format(browser_keyring_name)], # match 'service'
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
try:
stdout, stderr = proc.communicate_or_kill() stdout, stderr = proc.communicate_or_kill()
if stdout[-1:] == b'\n': if stdout[-1:] == b'\n':
stdout = stdout[:-1] stdout = stdout[:-1]
return stdout return stdout
except BaseException as e: except BaseException as e:
logger.warning(f'exception running find-generic-password: {type(e).__name__}({e})') logger.warning(f'exception running find-generic-password: {type(e).__name__}({e})')
return None return None
def _get_windows_v10_key(browser_root, logger): def _get_windows_v10_key(browser_root, logger):
@@ -736,10 +943,11 @@ def _is_path(value):
return os.path.sep in value return os.path.sep in value
def _parse_browser_specification(browser_name, profile=None): def _parse_browser_specification(browser_name, profile=None, keyring=None):
browser_name = browser_name.lower()
if browser_name not in SUPPORTED_BROWSERS: if browser_name not in SUPPORTED_BROWSERS:
raise ValueError(f'unsupported browser: "{browser_name}"') 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): if profile is not None and _is_path(profile):
profile = os.path.expanduser(profile) profile = os.path.expanduser(profile)
return browser_name, profile return browser_name, profile, keyring
+1
View File
@@ -265,6 +265,7 @@ class Aria2cFD(ExternalFD):
cmd += self._option('--all-proxy', 'proxy') cmd += self._option('--all-proxy', 'proxy')
cmd += self._bool_option('--check-certificate', 'nocheckcertificate', 'false', 'true', '=') cmd += self._bool_option('--check-certificate', 'nocheckcertificate', 'false', 'true', '=')
cmd += self._bool_option('--remote-time', 'updatetime', 'true', 'false', '=') cmd += self._bool_option('--remote-time', 'updatetime', 'true', 'false', '=')
cmd += self._bool_option('--show-console-readout', 'noprogress', 'false', 'true', '=')
cmd += self._configuration_args() cmd += self._configuration_args()
# aria2c strips out spaces from the beginning/end of filenames and paths. # aria2c strips out spaces from the beginning/end of filenames and paths.
+2
View File
@@ -433,6 +433,7 @@ class FragmentFD(FileDownloader):
def download_fragment(fragment, ctx): def download_fragment(fragment, ctx):
frag_index = ctx['fragment_index'] = fragment['frag_index'] frag_index = ctx['fragment_index'] = fragment['frag_index']
ctx['last_error'] = None
if not interrupt_trigger[0]: if not interrupt_trigger[0]:
return False, frag_index return False, frag_index
headers = info_dict.get('http_headers', {}).copy() headers = info_dict.get('http_headers', {}).copy()
@@ -455,6 +456,7 @@ class FragmentFD(FileDownloader):
# See https://github.com/ytdl-org/youtube-dl/issues/10165, # See https://github.com/ytdl-org/youtube-dl/issues/10165,
# https://github.com/ytdl-org/youtube-dl/issues/10448). # https://github.com/ytdl-org/youtube-dl/issues/10448).
count += 1 count += 1
ctx['last_error'] = err
if count <= fragment_retries: if count <= fragment_retries:
self.report_retry_fragment(err, frag_index, count, fragment_retries) self.report_retry_fragment(err, frag_index, count, fragment_retries)
except DownloadError: except DownloadError:
+80 -60
View File
@@ -19,14 +19,15 @@ from ..utils import (
parse_iso8601, parse_iso8601,
traverse_obj, traverse_obj,
try_get, try_get,
parse_count,
smuggle_url, smuggle_url,
srt_subtitles_timecode, srt_subtitles_timecode,
str_or_none, str_or_none,
str_to_int,
strip_jsonp, strip_jsonp,
unified_timestamp, unified_timestamp,
unsmuggle_url, unsmuggle_url,
urlencode_postdata, urlencode_postdata,
url_or_none,
OnDemandPagedList OnDemandPagedList
) )
@@ -722,10 +723,10 @@ class BiliBiliPlayerIE(InfoExtractor):
class BiliIntlBaseIE(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): def _call_api(self, endpoint, *args, **kwargs):
return self._download_json(self._API_URL.format(type, endpoint), id)['data'] return self._download_json(self._API_URL + endpoint, *args, **kwargs)['data']
def json2srt(self, json): def json2srt(self, json):
data = '\n\n'.join( data = '\n\n'.join(
@@ -733,29 +734,40 @@ class BiliIntlBaseIE(InfoExtractor):
for i, line in enumerate(json['body'])) for i, line in enumerate(json['body']))
return data return data
def _get_subtitles(self, type, ep_id): def _get_subtitles(self, ep_id):
sub_json = self._call_api(type, f'/m/subtitle?ep_id={ep_id}&platform=web', ep_id) sub_json = self._call_api(f'/web/v2/subtitle?episode_id={ep_id}&platform=web', ep_id)
subtitles = {} subtitles = {}
for sub in sub_json.get('subtitles', []): for sub in sub_json.get('subtitles') or []:
sub_url = sub.get('url') sub_url = sub.get('url')
if not sub_url: if not sub_url:
continue 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: if not sub_data:
continue continue
subtitles.setdefault(sub.get('key', 'en'), []).append({ subtitles.setdefault(sub.get('lang_key', 'en'), []).append({
'ext': 'srt', 'ext': 'srt',
'data': self.json2srt(sub_data) 'data': self.json2srt(sub_data)
}) })
return subtitles return subtitles
def _get_formats(self, type, ep_id): def _get_formats(self, ep_id):
video_json = self._call_api(type, f'/web/playurl?ep_id={ep_id}&platform=web', ep_id) video_json = self._call_api(f'/web/playurl?ep_id={ep_id}&platform=web', ep_id,
if not video_json: note='Downloading video formats', errnote='Unable to download video formats')
self.raise_login_required(method='cookies') 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'] video_json = video_json['playurl']
formats = [] formats = []
for vid in video_json.get('video', []): for vid in video_json.get('video') or []:
video_res = vid.get('video_resource') or {} video_res = vid.get('video_resource') or {}
video_info = vid.get('stream_info') or {} video_info = vid.get('stream_info') or {}
if not video_res.get('url'): if not video_res.get('url'):
@@ -771,7 +783,7 @@ class BiliIntlBaseIE(InfoExtractor):
'vcodec': video_res.get('codecs'), 'vcodec': video_res.get('codecs'),
'filesize': video_res.get('size'), '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'): if not aud.get('url'):
continue continue
formats.append({ formats.append({
@@ -786,85 +798,93 @@ class BiliIntlBaseIE(InfoExtractor):
self._sort_formats(formats) self._sort_formats(formats)
return formats return formats
def _extract_ep_info(self, type, episode_data, ep_id): def _extract_ep_info(self, episode_data, ep_id):
return { return {
'id': ep_id, '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'), 'thumbnail': episode_data.get('cover'),
'episode_number': str_to_int(episode_data.get('title')), 'episode_number': int_or_none(self._search_regex(
'formats': self._get_formats(type, ep_id), r'^E(\d+)(?:$| - )', episode_data.get('title_display'), 'episode number', default=None)),
'subtitles': self._get_subtitles(type, ep_id), 'formats': self._get_formats(ep_id),
'subtitles': self._get_subtitles(ep_id),
'extractor_key': BiliIntlIE.ie_key(), 'extractor_key': BiliIntlIE.ie_key(),
} }
class BiliIntlIE(BiliIntlBaseIE): 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 = [{ _TESTS = [{
'url': 'https://www.bilibili.tv/en/play/34613/341736', 'url': 'https://www.bilibili.tv/en/play/34613/341736',
'info_dict': { 'info_dict': {
'id': '341736', 'id': '341736',
'ext': 'mp4', 'ext': 'mp4',
'title': 'The First Night', 'title': 'E2 - The First Night',
'thumbnail': 'https://i0.hdslb.com/bfs/intl/management/91e30e5521235d9b163339a26a0b030ebda54310.png', 'thumbnail': r're:^https://pic\.bstarstatic\.com/ogv/.+\.png$',
'episode_number': 2, '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', 'url': 'https://www.biliintl.com/en/play/34613/341736',
'info_dict': { 'only_matching': True,
'id': '341736',
'ext': 'mp4',
'title': 'The First Night',
'thumbnail': 'https://i0.hdslb.com/bfs/intl/management/91e30e5521235d9b163339a26a0b030ebda54310.png',
'episode_number': 2,
},
'params': {
'format': 'bv',
},
}] }]
def _real_extract(self, url): def _real_extract(self, url):
type, season_id, id = self._match_valid_url(url).groups() season_id, video_id = self._match_valid_url(url).groups()
data_json = self._call_api(type, f'/web/view/ogv_collection?season_id={season_id}', id) webpage = self._download_webpage(url, video_id)
episode_data = next( # Bstation layout
episode for episode in data_json.get('episodes', []) initial_data = self._parse_json(self._search_regex(
if str(episode.get('ep_id')) == id) r'window\.__INITIAL_DATA__\s*=\s*({.+?});', webpage,
return self._extract_ep_info(type, episode_data, id) '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): 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 = [{ _TESTS = [{
'url': 'https://www.bilibili.tv/en/play/34613', 'url': 'https://www.bilibili.tv/en/play/34613',
'playlist_mincount': 15, 'playlist_mincount': 15,
'info_dict': { 'info_dict': {
'id': '34613', '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': { 'params': {
'skip_download': True, 'skip_download': True,
'format': 'bv',
}, },
}, { }, {
'url': 'https://www.biliintl.com/en/play/34613', 'url': 'https://www.biliintl.com/en/play/34613',
'playlist_mincount': 15, 'only_matching': True,
'info_dict': {
'id': '34613',
},
'params': {
'skip_download': True,
'format': 'bv',
},
}] }]
def _entries(self, id, type): def _entries(self, series_id):
data_json = self._call_api(type, f'/web/view/ogv_collection?season_id={id}', id) series_json = self._call_api(f'/web/v2/ogv/play/episodes?season_id={series_id}&platform=web', series_id)
for episode in data_json.get('episodes', []): for episode in traverse_obj(series_json, ('sections', ..., 'episodes', ...), expected_type=dict, default=[]):
episode_id = str(episode.get('ep_id')) episode_id = str(episode.get('episode_id'))
yield self._extract_ep_info(type, episode, episode_id) yield self._extract_ep_info(episode, episode_id)
def _real_extract(self, url): def _real_extract(self, url):
type, id = self._match_valid_url(url).groups() series_id = self._match_id(url)
return self.playlist_result(self._entries(id, type), playlist_id=id) 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')))
+2 -1
View File
@@ -340,7 +340,8 @@ class CBCGemIE(InfoExtractor):
yield { yield {
**base_format, **base_format,
'format_id': join_nonempty('sec', height), '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')), 'width': int_or_none(video_quality.attrib.get('MaxWidth')),
'tbr': bitrate / 1000.0, 'tbr': bitrate / 1000.0,
'height': height, 'height': height,
+7
View File
@@ -1126,6 +1126,10 @@ from .pinterest import (
PinterestIE, PinterestIE,
PinterestCollectionIE, PinterestCollectionIE,
) )
from .pixivsketch import (
PixivSketchIE,
PixivSketchUserIE,
)
from .pladform import PladformIE from .pladform import PladformIE
from .planetmarathi import PlanetMarathiIE from .planetmarathi import PlanetMarathiIE
from .platzi import ( from .platzi import (
@@ -1534,6 +1538,9 @@ from .threeqsdn import ThreeQSDNIE
from .tiktok import ( from .tiktok import (
TikTokIE, TikTokIE,
TikTokUserIE, TikTokUserIE,
TikTokSoundIE,
TikTokEffectIE,
TikTokTagIE,
DouyinIE, DouyinIE,
) )
from .tinypic import TinyPicIE from .tinypic import TinyPicIE
+13 -1
View File
@@ -2344,6 +2344,18 @@ class GenericIE(InfoExtractor):
'thumbnail': 'https://bogmedia.org/contents/videos_screenshots/21000/21217/preview_480p.mp4.jpg', '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 # 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/ # 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') self.report_detected('JW Player embed')
if not found: if not found:
# Look for generic KVS player # 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: if found:
self.report_detected('KWS Player') self.report_detected('KWS Player')
if found.group('maj_ver') not in ['4', '5']: if found.group('maj_ver') not in ['4', '5']:
+122
View 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"]}')
+109 -1
View File
@@ -447,7 +447,7 @@ class TikTokUserIE(TikTokBaseIE):
for video in post_list.get('aweme_list', []): for video in post_list.get('aweme_list', []):
yield { yield {
**self._parse_aweme_video_app(video), **self._parse_aweme_video_app(video),
'ie_key': TikTokIE.ie_key(), 'extractor_key': TikTokIE.ie_key(),
'extractor': 'TikTok', 'extractor': 'TikTok',
'webpage_url': f'https://tiktok.com/@{user_id}/video/{video["aweme_id"]}', '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) 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): class DouyinIE(TikTokIE):
_VALID_URL = r'https?://(?:www\.)?douyin\.com/video/(?P<id>[0-9]+)' _VALID_URL = r'https?://(?:www\.)?douyin\.com/video/(?P<id>[0-9]+)'
_TESTS = [{ _TESTS = [{
+25 -18
View File
@@ -1777,16 +1777,15 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
self._player_cache = {} self._player_cache = {}
def _prepare_live_from_start_formats(self, formats, video_id, live_start_time, url, webpage_url, smuggled_data): 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() lock = threading.Lock()
is_live = True 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')] formats = [f for f in formats if f.get('is_from_start')]
def refetch_manifest(format_id): def refetch_manifest(format_id, delay):
nonlocal formats, expiration_time, is_live nonlocal formats, start_time, is_live
if time.time() <= expiration_time: if time.time() <= start_time + delay:
return return
_, _, prs, player_url = self._download_player_responses(url, smuggled_data, video_id, webpage_url) _, _, prs, player_url = self._download_player_responses(url, smuggled_data, video_id, webpage_url)
@@ -1796,19 +1795,22 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
prs, (..., 'microformat', 'playerMicroformatRenderer'), prs, (..., 'microformat', 'playerMicroformatRenderer'),
expected_type=dict, default=[]) expected_type=dict, default=[])
_, is_live, _, formats = self._list_formats(video_id, microformats, video_details, prs, player_url) _, 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 @returns (manifest_url, manifest_stream_number, is_live) or None
""" """
with lock: 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) f = next((f for f in formats if f['format_id'] == format_id), None)
if not f: if not f:
self.report_warning( if not is_live:
f'Cannot find refreshed manifest for format {format_id}{bug_reports_message()}') 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 None
return f['manifest_url'], f['manifest_stream_number'], is_live 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 nonlocal mpd_url, stream_number, is_live, no_fragment_score, fragments, fragment_base_url
# Obtain from MPD's maximum seq value # Obtain from MPD's maximum seq value
old_mpd_url = mpd_url old_mpd_url = mpd_url
mpd_url, stream_number, is_live = mpd_feed(format_id) or (mpd_url, stream_number, False) last_error = ctx.pop('last_error', None)
if old_mpd_url == mpd_url and not refresh_sequence: expire_fast = last_error and isinstance(last_error, compat_HTTPError) and last_error.code == 403
return True, last_seq 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: try:
fmts, _ = self._extract_mpd_formats_and_subtitles( fmts, _ = self._extract_mpd_formats_and_subtitles(
mpd_url, None, note=False, errnote=False, fatal=False) mpd_url, None, note=False, errnote=False, fatal=False)
@@ -1875,8 +1883,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
last_segment_url = None last_segment_url = None
continue continue
else: else:
should_retry, last_seq = _extract_sequence_from_mpd(True) should_continue, last_seq = _extract_sequence_from_mpd(True)
if not should_retry: if not should_continue:
continue continue
if known_idx > last_seq: if known_idx > last_seq:
@@ -1893,9 +1901,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
try: try:
for idx in range(known_idx, last_seq): for idx in range(known_idx, last_seq):
# do not update sequence here or you'll get skipped some part of it # do not update sequence here or you'll get skipped some part of it
should_retry, _ = _extract_sequence_from_mpd(False) should_continue, _ = _extract_sequence_from_mpd(False)
if not should_retry: if not should_continue:
# retry when it gets weird state
known_idx = idx - 1 known_idx = idx - 1
raise ExtractorError('breaking out of outer loop') raise ExtractorError('breaking out of outer loop')
last_segment_url = urljoin(fragment_base_url, 'sq/%d' % idx) last_segment_url = urljoin(fragment_base_url, 'sq/%d' % idx)
+28 -27
View File
@@ -23,7 +23,7 @@ class Zee5IE(InfoExtractor):
zee5:| zee5:|
https?://(?:www\.)?zee5\.com/(?:[^#?]+/)? https?://(?:www\.)?zee5\.com/(?:[^#?]+/)?
(?: (?:
(?:tvshows|kids|zee5originals)(?:/[^#/?]+){3} (?:tv-shows|kids|zee5originals)(?:/[^#/?]+){3}
|movies/[^#/?]+ |movies/[^#/?]+
)/(?P<display_id>[^#/?]+)/ )/(?P<display_id>[^#/?]+)/
) )
@@ -37,48 +37,50 @@ class Zee5IE(InfoExtractor):
'display_id': 'krishna-the-birth', 'display_id': 'krishna-the-birth',
'title': 'Krishna - The Birth', 'title': 'Krishna - The Birth',
'duration': 4368, 'duration': 4368,
'average_rating': 4,
'description': compat_str, 'description': compat_str,
'alt_title': 'Krishna - The Birth', 'alt_title': 'Krishna - The Birth',
'uploader': 'Zee Entertainment Enterprises Ltd', 'uploader': 'Zee Entertainment Enterprises Ltd',
'release_date': '20060101', 'release_date': '20060101',
'upload_date': '20060101', 'upload_date': '20060101',
'timestamp': 1136073600, '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 'tags': list
}, },
'params': { 'params': {
'format': 'bv', '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': { 'info_dict': {
'id': '0-1-233402', 'id': '0-1-239839',
'ext': 'mp4', 'ext': 'mp4',
'display_id': 'episode-1-the-test-of-bramha', 'display_id': 'yoga-se-hoga-bandbudh-aur-budbak',
'title': 'Episode 1 - The Test Of Bramha', 'title': 'Yoga Se Hoga-Bandbudh aur Budbak',
'duration': 1336, 'duration': 659,
'average_rating': 4,
'description': compat_str, 'description': compat_str,
'alt_title': 'Episode 1 - The Test Of Bramha', 'alt_title': 'Yoga Se Hoga-Bandbudh aur Budbak',
'uploader': 'Zee Entertainment Enterprises Ltd', 'uploader': 'Zee Entertainment Enterprises Ltd',
'release_date': '20090101', 'release_date': '20150101',
'upload_date': '20090101', 'upload_date': '20150101',
'timestamp': 1230768000, 'timestamp': 1420070400,
'thumbnail': 'https://akamaividz.zee5.com/resources/0-1-233402/list/270x152/01233402_list.jpg', 'thumbnail': r're:^https?://.*\.jpg$',
'series': 'Krishna Balram', 'series': 'Bandbudh Aur Budbak',
'season_number': 1, 'season_number': 1,
'episode_number': 1, 'episode_number': 1,
'episode': 'Episode 1',
'season': 'Season 1',
'tags': list, 'tags': list,
}, },
'params': { 'params': {
'format': 'bv', '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 '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 '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' _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, 'formats': formats,
'subtitles': subtitles, 'subtitles': subtitles,
'duration': int_or_none(asset_data.get('duration')), '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')), 'description': str_or_none(asset_data.get('description')),
'alt_title': str_or_none(asset_data.get('original_title')), 'alt_title': str_or_none(asset_data.get('original_title')),
'uploader': str_or_none(asset_data.get('content_owner')), 'uploader': str_or_none(asset_data.get('content_owner')),
@@ -175,42 +176,42 @@ class Zee5SeriesIE(InfoExtractor):
(?: (?:
zee5:series:| zee5:series:|
https?://(?:www\.)?zee5\.com/(?:[^#?]+/)? https?://(?:www\.)?zee5\.com/(?:[^#?]+/)?
(?:tvshows|kids|zee5originals)(?:/[^#/?]+){2}/ (?:tv-shows|kids|zee5originals)(?:/[^#/?]+){2}/
) )
(?P<id>[^#/?]+)(?:/episodes)?/?(?:$|[?#]) (?P<id>[^#/?]+)(?:/episodes)?/?(?:$|[?#])
''' '''
_TESTS = [{ _TESTS = [{
'url': 'https://www.zee5.com/kids/kids-shows/krishna-balram/0-6-1871', 'url': 'https://www.zee5.com/kids/kids-shows/bandbudh-aur-budbak/0-6-1899',
'playlist_mincount': 43, 'playlist_mincount': 156,
'info_dict': { '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, 'playlist_mincount': 1500,
'info_dict': { 'info_dict': {
'id': '0-6-199', '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, 'playlist_mincount': 24,
'info_dict': { 'info_dict': {
'id': '0-6-965', '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, 'playlist_mincount': 3,
'info_dict': { 'info_dict': {
'id': '0-6-3201', '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, 'playlist_mincount': 150,
'info_dict': { 'info_dict': {
'id': '0-6-270', '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, 'only_matching': True,
}] }]
+10 -9
View File
@@ -20,7 +20,7 @@ from .utils import (
remove_end, remove_end,
write_string, write_string,
) )
from .cookies import SUPPORTED_BROWSERS from .cookies import SUPPORTED_BROWSERS, SUPPORTED_KEYRINGS
from .version import __version__ from .version import __version__
from .downloader.external import list_external_downloaders from .downloader.external import list_external_downloaders
@@ -1023,7 +1023,7 @@ def parseOpts(overrideArguments=None):
}, help=( }, help=(
'The paths where the files should be downloaded. ' 'The paths where the files should be downloaded. '
'Specify the type of file and the path separated by a colon ":". ' '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. ' 'Additionally, you can also provide "home" (default) and "temp" paths. '
'All intermediary files are first downloaded to the temp path and ' '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. ' '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)') help='Do not read/dump cookies from/to file (default)')
filesystem.add_option( filesystem.add_option(
'--cookies-from-browser', '--cookies-from-browser',
dest='cookiesfrombrowser', metavar='BROWSER[:PROFILE]', dest='cookiesfrombrowser', metavar='BROWSER[+KEYRING][:PROFILE]',
help=( help=(
'Load cookies from a user profile of the given web browser. ' 'The name of the browser and (optionally) the name/path of '
'Currently supported browsers are: {}. ' 'the profile to load cookies from, separated by a ":". '
'You can specify the user profile name or directory using ' f'Currently supported browsers are: {", ".join(sorted(SUPPORTED_BROWSERS))}. '
'"BROWSER:PROFILE_NAME" or "BROWSER:PROFILE_PATH". ' 'By default, the most recently accessed profile is used. '
'If no profile is given, the most recently accessed one is used'.format( 'The keyring used for decrypting Chromium cookies on Linux can be '
', '.join(sorted(SUPPORTED_BROWSERS))))) '(optionally) specified after the browser name separated by a "+". '
f'Currently supported keyrings are: {", ".join(map(str.lower, sorted(SUPPORTED_KEYRINGS)))}'))
filesystem.add_option( filesystem.add_option(
'--no-cookies-from-browser', '--no-cookies-from-browser',
action='store_const', const=None, dest='cookiesfrombrowser', action='store_const', const=None, dest='cookiesfrombrowser',
+38 -33
View File
@@ -145,8 +145,43 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
self.report_warning('unable to embed using mutagen; %s' % error_to_compat_str(err)) self.report_warning('unable to embed using mutagen; %s' % error_to_compat_str(err))
success = False success = False
# Method 2: Use ffmpeg+ffprobe # Method 2: Use AtomicParsley
if not success and not prefer_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 success = True
try: try:
options = ['-c', 'copy', '-map', '0', '-dn', '-map', '1'] options = ['-c', 'copy', '-map', '0', '-dn', '-map', '1']
@@ -161,38 +196,8 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
self._report_run('ffmpeg', filename) self._report_run('ffmpeg', filename)
self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options) self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options)
except PostProcessingError as err: 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 success = False
raise EmbedThumbnailPPError(f'Unable to embed using ffprobe & ffmpeg; {err}')
elif info['ext'] in ['ogg', 'opus', 'flac']: elif info['ext'] in ['ogg', 'opus', 'flac']:
if not has_mutagen: if not has_mutagen:
+5 -5
View File
@@ -296,8 +296,8 @@ class FFmpegPostProcessor(PostProcessor):
keys = ['_%s%d' % (name, number), '_%s' % name] keys = ['_%s%d' % (name, number), '_%s' % name]
if name == 'o': if name == 'o':
args += ['-movflags', '+faststart'] args += ['-movflags', '+faststart']
elif number == 1: if number == 1:
keys.append('') keys.append('')
args += self._configuration_args(self.basename, keys) args += self._configuration_args(self.basename, keys)
if name == 'i': if name == 'i':
args.append('-i') args.append('-i')
@@ -1092,12 +1092,12 @@ class FFmpegThumbnailsConvertorPP(FFmpegPostProcessor):
files_to_delete = [] files_to_delete = []
has_thumbnail = False has_thumbnail = False
for idx, thumbnail_dict in enumerate(info['thumbnails']): for idx, thumbnail_dict in enumerate(info.get('thumbnails') or []):
if 'filepath' not in thumbnail_dict: original_thumbnail = thumbnail_dict.get('filepath')
if not original_thumbnail:
continue continue
has_thumbnail = True has_thumbnail = True
self.fixup_webp(info, idx) self.fixup_webp(info, idx)
original_thumbnail = thumbnail_dict['filepath']
_, thumbnail_ext = os.path.splitext(original_thumbnail) _, thumbnail_ext = os.path.splitext(original_thumbnail)
if thumbnail_ext: if thumbnail_ext:
thumbnail_ext = thumbnail_ext[1:].lower() thumbnail_ext = thumbnail_ext[1:].lower()
+2 -2
View File
@@ -1,5 +1,5 @@
# Autogenerated by devscripts/update-version.py # Autogenerated by devscripts/update-version.py
__version__ = '2021.12.25' __version__ = '2021.12.27'
RELEASE_GIT_HEAD = '87e049962' RELEASE_GIT_HEAD = '6223f67a8'