Compare commits

..
Author SHA1 Message Date
pukkandan 9963cda115 Release 2021.03.03 2021-03-03 16:25:04 +05:30
pukkandan c1be5231b9 [build] fix bug from da7f321e93 2021-03-03 16:25:04 +05:30
77 changed files with 1904 additions and 3985 deletions
+3 -3
View File
@@ -21,7 +21,7 @@ assignees: ''
<!-- <!--
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp: Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.03.24. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED. - First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.03.01. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser. - Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/yt-dlp/yt-dlp. - Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/yt-dlp/yt-dlp.
- Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates. - Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
@@ -29,7 +29,7 @@ Carefully read and work through this check list in order to prevent the most com
--> -->
- [ ] I'm reporting a broken site support - [ ] I'm reporting a broken site support
- [ ] I've verified that I'm running yt-dlp version **2021.03.24** - [ ] I've verified that I'm running yt-dlp version **2021.03.01**
- [ ] I've checked that all provided URLs are alive and playable in a browser - [ ] I've checked that all provided URLs are alive and playable in a browser
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped - [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
- [ ] I've searched the bugtracker for similar issues including closed ones - [ ] I've searched the bugtracker for similar issues including closed ones
@@ -44,7 +44,7 @@ Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your com
[debug] User config: [] [debug] User config: []
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj'] [debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251 [debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
[debug] yt-dlp version 2021.03.24 [debug] yt-dlp version 2021.03.01
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2 [debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4 [debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
[debug] Proxy map: {} [debug] Proxy map: {}
@@ -21,7 +21,7 @@ assignees: ''
<!-- <!--
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp: Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.03.24. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED. - First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.03.01. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser. - Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
- Make sure that site you are requesting is not dedicated to copyright infringement, see https://github.com/yt-dlp/yt-dlp. yt-dlp does not support such sites. In order for site support request to be accepted all provided example URLs should not violate any copyrights. - Make sure that site you are requesting is not dedicated to copyright infringement, see https://github.com/yt-dlp/yt-dlp. yt-dlp does not support such sites. In order for site support request to be accepted all provided example URLs should not violate any copyrights.
- Search the bugtracker for similar site support requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates. - Search the bugtracker for similar site support requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
@@ -29,7 +29,7 @@ Carefully read and work through this check list in order to prevent the most com
--> -->
- [ ] I'm reporting a new site support request - [ ] I'm reporting a new site support request
- [ ] I've verified that I'm running yt-dlp version **2021.03.24** - [ ] I've verified that I'm running yt-dlp version **2021.03.01**
- [ ] I've checked that all provided URLs are alive and playable in a browser - [ ] I've checked that all provided URLs are alive and playable in a browser
- [ ] I've checked that none of provided URLs violate any copyrights - [ ] I've checked that none of provided URLs violate any copyrights
- [ ] I've searched the bugtracker for similar site support requests including closed ones - [ ] I've searched the bugtracker for similar site support requests including closed ones
@@ -21,13 +21,13 @@ assignees: ''
<!-- <!--
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp: Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.03.24. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED. - First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.03.01. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
- Search the bugtracker for similar site feature requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates. - Search the bugtracker for similar site feature requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space) - Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
--> -->
- [ ] I'm reporting a site feature request - [ ] I'm reporting a site feature request
- [ ] I've verified that I'm running yt-dlp version **2021.03.24** - [ ] I've verified that I'm running yt-dlp version **2021.03.01**
- [ ] I've searched the bugtracker for similar site feature requests including closed ones - [ ] I've searched the bugtracker for similar site feature requests including closed ones
+3 -3
View File
@@ -21,7 +21,7 @@ assignees: ''
<!-- <!--
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp: Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.03.24. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED. - First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.03.01. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser. - Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/yt-dlp/yt-dlp. - Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/yt-dlp/yt-dlp.
- Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates. - Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
@@ -30,7 +30,7 @@ Carefully read and work through this check list in order to prevent the most com
--> -->
- [ ] I'm reporting a broken site support issue - [ ] I'm reporting a broken site support issue
- [ ] I've verified that I'm running yt-dlp version **2021.03.24** - [ ] I've verified that I'm running yt-dlp version **2021.03.01**
- [ ] I've checked that all provided URLs are alive and playable in a browser - [ ] I've checked that all provided URLs are alive and playable in a browser
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped - [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
- [ ] I've searched the bugtracker for similar bug reports including closed ones - [ ] I've searched the bugtracker for similar bug reports including closed ones
@@ -46,7 +46,7 @@ Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your com
[debug] User config: [] [debug] User config: []
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj'] [debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251 [debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
[debug] yt-dlp version 2021.03.24 [debug] yt-dlp version 2021.03.01
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2 [debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4 [debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
[debug] Proxy map: {} [debug] Proxy map: {}
+2 -2
View File
@@ -21,13 +21,13 @@ assignees: ''
<!-- <!--
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp: Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.03.24. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED. - First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.03.01. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
- Search the bugtracker for similar feature requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates. - Search the bugtracker for similar feature requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space) - Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
--> -->
- [ ] I'm reporting a feature request - [ ] I'm reporting a feature request
- [ ] I've verified that I'm running yt-dlp version **2021.03.24** - [ ] I've verified that I'm running yt-dlp version **2021.03.01**
- [ ] I've searched the bugtracker for similar feature requests including closed ones - [ ] I've searched the bugtracker for similar feature requests including closed ones
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
- name: Print version - name: Print version
run: echo "${{ steps.bump_version.outputs.ytdlp_version }}" run: echo "${{ steps.bump_version.outputs.ytdlp_version }}"
- name: Run Make - name: Run Make
run: make run: make yt-dlp
- name: Create Release - name: Create Release
id: create_release id: create_release
uses: actions/create-release@v1 uses: actions/create-release@v1
-1
View File
@@ -60,7 +60,6 @@ yt-dlp.zip
*.mkv *.mkv
*.swf *.swf
*.part *.part
*.part-*
*.ytdl *.ytdl
*.dump *.dump
*.frag *.frag
+2 -9
View File
@@ -1,6 +1,5 @@
pukkandan (owner) pukkandan (owner)
shirt-dev (collaborator) shirt-dev (collaborator)
colethedj (collaborator)
h-h-h-h h-h-h-h
pauldubois98 pauldubois98
nixxo nixxo
@@ -22,16 +21,10 @@ nao20010128nao
kurumigi kurumigi
tsukumi tsukumi
bbepis bbepis
animelover1984
Pccode66 Pccode66
Ashish0804 Ashish
RobinD42 RobinD42
hseg hseg
colethedj
DennyDai DennyDai
codeasashu codeasashu
teesid
kevinoconnor7
damianoamatruda
2ShedsJackson
CXwudi
xtkoba
+40 -127
View File
@@ -17,102 +17,16 @@
--> -->
### 2021.03.24.1
* Revert [commit/8562218](https://github.com/ytdl-org/youtube-dl/commit/8562218350a79d4709da8593bb0c538aa0824acf)
### 2021.03.24
* Merge youtube-dl: Upto [commit/8562218](https://github.com/ytdl-org/youtube-dl/commit/8562218350a79d4709da8593bb0c538aa0824acf)
* Parse metadata from multiple fields using `--parse-metadata`
* Ability to load playlist infojson using `--load-info-json`
* Write current epoch to infojson when using `--no-clean-infojson`
* [youtube_live_chat] fix bug when trying to set cookies
* [niconico] Fix for when logged in by: @CXwudi and @xtkoba
* [linuxacadamy] Fix login
### 2021.03.21
* Merge youtube-dl: Upto [commit/7e79ba7](https://github.com/ytdl-org/youtube-dl/commit/7e79ba7dd6e6649dd2ce3a74004b2044f2182881)
* Option `--no-clean-infojson` to keep private keys in the infojson
* [aria2c] Support retry/abort unavailable fragments by [damianoamatruda](https://github.com/damianoamatruda)
* [aria2c] Better default arguments
* [movefiles] Fix bugs and make more robust
* [formatSort] Fix `quality` being ignored
* [splitchapters] Fix for older ffmpeg
* [sponskrub] Pass proxy to sponskrub
* Make sure `post_hook` gets the final filename
* Recursively remove any private keys from infojson
* Embed video URL metadata inside `mp4` by [damianoamatruda](https://github.com/damianoamatruda) and [pukkandan](https://github.com/pukkandan)
* Merge `webm` formats into `mkv` if thumbnails are to be embedded by [damianoamatruda](https://github.com/damianoamatruda)
* Use headers and cookies when downloading subtitles by [damianoamatruda](https://github.com/damianoamatruda)
* Parse resolution in info dictionary by [damianoamatruda](https://github.com/damianoamatruda)
* More consistent warning messages by [damianoamatruda](https://github.com/damianoamatruda) and [pukkandan](https://github.com/pukkandan)
* [documentation] Add deprecated options and aliases in readme
* [documentation] Fix some minor mistakes
* [niconico] Partial fix adapted from [animelover1984/youtube-dl@b5eff52](https://github.com/animelover1984/youtube-dl/commit/b5eff52dd9ed5565672ea1694b38c9296db3fade) (login and smile formats still don't work)
* [niconico] Add user extractor by [animelover1984](https://github.com/animelover1984)
* [bilibili] Add anthology support by [animelover1984](https://github.com/animelover1984)
* [amcnetworks] Fix extractor by [2ShedsJackson](https://github.com/2ShedsJackson)
* [stitcher] Merge from youtube-dl by [nixxo](https://github.com/nixxo)
* [rcs] Improved extraction by [nixxo](https://github.com/nixxo)
* [linuxacadamy] Improve regex
* [youtube] Show if video is `private`, `unlisted` etc in info (`availability`) by [colethedj](https://github.com/colethedj) and [pukkandan](https://github.com/pukkandan)
* [youtube] bugfix for channel playlist extraction
* [nbc] Improve metadata extraction by [2ShedsJackson](https://github.com/2ShedsJackson)
### 2021.03.15
* **Split video by chapters**: using option `--split-chapters`
* The output file of the split files can be set with `-o`/`-P` using the prefix `chapter:`
* Additional keys `section_title`, `section_number`, `section_start`, `section_end` are available in the output template
* **Parallel fragment downloads** by [shirt](https://github.com/shirt-dev)
* Use option `--concurrent-fragments` (`-N`) to set the number of threads (default 1)
* Merge youtube-dl: Upto [commit/3be0980](https://github.com/ytdl-org/youtube-dl/commit/3be098010f667b14075e3dfad1e74e5e2becc8ea)
* [zee5] Add Show Extractor by [Ashish0804](https://github.com/Ashish0804) and [pukkandan](https://github.com/pukkandan)
* [rai] fix drm check [nixxo](https://github.com/nixxo)
* [wimtv] Add extractor by [nixxo](https://github.com/nixxo)
* [mtv] Add mtv.it and extract series metadata by [nixxo](https://github.com/nixxo)
* [pluto.tv] Add extractor by [kevinoconnor7](https://github.com/kevinoconnor7)
* [youtube] Rewrite comment extraction by [colethedj](https://github.com/colethedj)
* [embedthumbnail] Set mtime correctly
* Refactor some postprocessor/downloader code by [pukkandan](https://github.com/pukkandan) and [shirt](https://github.com/shirt-dev)
### 2021.03.07
* [youtube] Fix history, mixes, community pages and trending by [pukkandan](https://github.com/pukkandan) and [colethedj](https://github.com/colethedj)
* [youtube] Fix private feeds/playlists on multi-channel accounts by [colethedj](https://github.com/colethedj)
* [youtube] Extract alerts from continuation by [colethedj](https://github.com/colethedj)
* [cbs] Add support for ParamountPlus by [shirt](https://github.com/shirt-dev)
* [mxplayer] Rewrite extractor with show support by [pukkandan](https://github.com/pukkandan) and [Ashish0804](https://github.com/Ashish0804)
* [gedi] Improvements from youtube-dl by [nixxo](https://github.com/nixxo)
* [vimeo] Fix videos with password by [teesid](https://github.com/teesid)
* [lbry] Support `lbry://` url by [nixxo](https://github.com/nixxo)
* [bilibili] Change `Accept` header by [pukkandan](https://github.com/pukkandan) and [animelover1984](https://github.com/animelover1984)
* [trovo] Pass origin header
* [rai] Check for DRM by [nixxo](https://github.com/nixxo)
* [downloader] Fix bug for `ffmpeg`/`httpie`
* [update] Fix updater removing the executable bit on some UNIX distros
* [update] Fix current build hash for UNIX
* [documentation] Include wget/curl/aria2c install instructions for Unix by [Ashish0804](https://github.com/Ashish0804)
* Fix some videos downloading with `m3u8` extension
* Remove "fixup is ignored" warning when fixup wasn't passed by user
### 2021.03.03.2
* [build] Fix bug
### 2021.03.03 ### 2021.03.03
* [youtube] Use new browse API for continuation page extraction by [colethedj](https://github.com/colethedj) and [pukkandan](https://github.com/pukkandan) * [youtube] Use new browse API for continuation page extraction by @colethedj and @pukkandan
* Fix HLS playlist downloading by [shirt](https://github.com/shirt-dev) * Fix HLS playlist downloading by @shirt
* Merge youtube-dl: Upto [2021.03.03](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.03.03) * **Merge youtube-dl:** Upto [2021.03.03](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.03.03)
* [mtv] Fix extractor * [mtv] Fix extractor
* [nick] Fix extractor by [DennyDai](https://github.com/DennyDai) * [nick] Fix extractor by @DennyDai
* [mxplayer] Add new extractor by [codeasashu](https://github.com/codeasashu) * [mxplayer] Add new extractor by@codeasashu
* [youtube] Throw error when `--extractor-retries` are exhausted * [youtube] Throw error when `--extractor-retries` are exhausted
* Reduce default of `--extractor-retries` to 3 * Reduce default of `--extractor-retries` to 3
* Fix packaging bugs by [hseg](https://github.com/hseg) * Fix packaging bugs by @hseg
### 2021.03.01 ### 2021.03.01
@@ -141,10 +55,10 @@
* Moved project to an organization [yt-dlp](https://github.com/yt-dlp) * Moved project to an organization [yt-dlp](https://github.com/yt-dlp)
* **Completely changed project name to yt-dlp** by [Pccode66](https://github.com/Pccode66) and [pukkandan](https://github.com/pukkandan) * **Completely changed project name to yt-dlp** by [Pccode66](https://github.com/Pccode66) and [pukkandan](https://github.com/pukkandan)
* Also, `youtube-dlc` config files are no longer loaded * Also, `youtube-dlc` config files are no longer loaded
* Merge youtube-dl: Upto [commit/4460329](https://github.com/ytdl-org/youtube-dl/commit/44603290e5002153f3ebad6230cc73aef42cc2cd) (except tmz, gedi) * **Merge youtube-dl:** Upto [commit/4460329](https://github.com/ytdl-org/youtube-dl/commit/44603290e5002153f3ebad6230cc73aef42cc2cd) (except tmz, gedi)
* [Readthedocs](https://yt-dlp.readthedocs.io) support by [shirt](https://github.com/shirt-dev) * [Readthedocs](https://yt-dlp.readthedocs.io) support by [shirt](https://github.com/shirt-dev)
* [youtube] Show if video was a live stream in info (`was_live`) * [youtube] Show if video was a live stream in info (`was_live`)
* [Zee5] Add new extractor by [Ashish0804](https://github.com/Ashish0804) and [pukkandan](https://github.com/pukkandan) * [Zee5] Add new extractor by [Ashish](https://github.com/Ashish) and [pukkandan](https://github.com/pukkandan)
* [jwplatform] Add support for `hyland.com` * [jwplatform] Add support for `hyland.com`
* [tennistv] Fix extractor * [tennistv] Fix extractor
* [hls] Support media initialization by [shirt](https://github.com/shirt-dev) * [hls] Support media initialization by [shirt](https://github.com/shirt-dev)
@@ -159,7 +73,7 @@
### 2021.02.19 ### 2021.02.19
* Merge youtube-dl: Upto [commit/cf2dbec](https://github.com/ytdl-org/youtube-dl/commit/cf2dbec6301177a1fddf72862de05fa912d9869d) (except kakao) * **Merge youtube-dl:** Upto [commit/cf2dbec](https://github.com/ytdl-org/youtube-dl/commit/cf2dbec6301177a1fddf72862de05fa912d9869d) (except kakao)
* [viki] Fix extractor * [viki] Fix extractor
* [niconico] Extract `channel` and `channel_id` by [kurumigi](https://github.com/kurumigi) * [niconico] Extract `channel` and `channel_id` by [kurumigi](https://github.com/kurumigi)
* [youtube] Multiple page support for hashtag URLs * [youtube] Multiple page support for hashtag URLs
@@ -184,7 +98,7 @@
### 2021.02.15 ### 2021.02.15
* Merge youtube-dl: Upto [2021.02.10](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.02.10) (except archive.org) * **Merge youtube-dl:** Upto [2021.02.10](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.02.10) (except archive.org)
* [niconico] Improved extraction and support encrypted/SMILE movies by [kurumigi](https://github.com/kurumigi), [tsukumi](https://github.com/tsukumi), [bbepis](https://github.com/bbepis), [pukkandan](https://github.com/pukkandan) * [niconico] Improved extraction and support encrypted/SMILE movies by [kurumigi](https://github.com/kurumigi), [tsukumi](https://github.com/tsukumi), [bbepis](https://github.com/bbepis), [pukkandan](https://github.com/pukkandan)
* Fix HLS AES-128 with multiple keys in external downloaders by [shirt](https://github.com/shirt-dev) * Fix HLS AES-128 with multiple keys in external downloaders by [shirt](https://github.com/shirt-dev)
* [youtube_live_chat] Fix by using POST API by [siikamiika](https://github.com/siikamiika) * [youtube_live_chat] Fix by using POST API by [siikamiika](https://github.com/siikamiika)
@@ -227,7 +141,7 @@
### 2021.02.04 ### 2021.02.04
* Merge youtube-dl: Upto [2021.02.04.1](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.02.04.1) * **Merge youtube-dl:** Upto [2021.02.04.1](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.02.04.1)
* **Date/time formatting in output template:** * **Date/time formatting in output template:**
* You can use [`strftime`](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) to format date/time fields. Example: `%(upload_date>%Y-%m-%d)s` * You can use [`strftime`](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) to format date/time fields. Example: `%(upload_date>%Y-%m-%d)s`
* **Multiple output templates:** * **Multiple output templates:**
@@ -281,7 +195,7 @@
### 2021.01.24 ### 2021.01.24
* Merge youtube-dl: Upto [2021.01.24](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.01.16) * **Merge youtube-dl:** Upto [2021.01.24](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.01.16)
* Plugin support ([documentation](https://github.com/yt-dlp/yt-dlp#plugins)) * Plugin support ([documentation](https://github.com/yt-dlp/yt-dlp#plugins))
* **Multiple paths**: New option `-P`/`--paths` to give different paths for different types of files * **Multiple paths**: New option `-P`/`--paths` to give different paths for different types of files
* The syntax is `-P "type:path" -P "type:path"` ([documentation](https://github.com/yt-dlp/yt-dlp#:~:text=-P,%20--paths%20TYPE:PATH)) * The syntax is `-P "type:path" -P "type:path"` ([documentation](https://github.com/yt-dlp/yt-dlp#:~:text=-P,%20--paths%20TYPE:PATH))
@@ -310,7 +224,7 @@
### 2021.01.16 ### 2021.01.16
* Merge youtube-dl: Upto [2021.01.16](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.01.16) * **Merge youtube-dl:** Upto [2021.01.16](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.01.16)
* **Configuration files:** * **Configuration files:**
* Portable configuration file: `./yt-dlp.conf` * Portable configuration file: `./yt-dlp.conf`
* Allow the configuration files to be named `yt-dlp` instead of `youtube-dlc`. See [this](https://github.com/yt-dlp/yt-dlp#configuration) for details * Allow the configuration files to be named `yt-dlp` instead of `youtube-dlc`. See [this](https://github.com/yt-dlp/yt-dlp#configuration) for details
@@ -340,7 +254,7 @@
* [archive.org] Fix extractor and add support for audio and playlists by [wporr](https://github.com/wporr) * [archive.org] Fix extractor and add support for audio and playlists by [wporr](https://github.com/wporr)
* [Animelab] Added by [mariuszskon](https://github.com/mariuszskon) * [Animelab] Added by [mariuszskon](https://github.com/mariuszskon)
* [youtube:search] Fix view_count by [ohnonot](https://github.com/ohnonot) * [youtube:search] Fix view_count by [ohnonot](https://github.com/ohnonot)
* [youtube] Show if video is embeddable in info (`playable_in_embed`) * [youtube] Show if video is embeddable in info
* Update version badge automatically in README * Update version badge automatically in README
* Enable `test_youtube_search_matching` * Enable `test_youtube_search_matching`
* Create `to_screen` and similar functions in postprocessor/common * Create `to_screen` and similar functions in postprocessor/common
@@ -356,7 +270,8 @@
### 2021.01.08 ### 2021.01.08
* Merge youtube-dl: Upto [2021.01.08](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.01.08) except stitcher ([1](https://github.com/ytdl-org/youtube-dl/commit/bb38a1215718cdf36d73ff0a7830a64cd9fa37cc), [2](https://github.com/ytdl-org/youtube-dl/commit/a563c97c5cddf55f8989ed7ea8314ef78e30107f)) * **Merge youtube-dl:** Upto [2021.01.08](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.01.08)
* Extractor stitcher ([1](https://github.com/ytdl-org/youtube-dl/commit/bb38a1215718cdf36d73ff0a7830a64cd9fa37cc), [2](https://github.com/ytdl-org/youtube-dl/commit/a563c97c5cddf55f8989ed7ea8314ef78e30107f)) have not been merged
* Moved changelog to seperate file * Moved changelog to seperate file
@@ -395,7 +310,7 @@
* Changed video format sorting to show video only files and video+audio files together. * Changed video format sorting to show video only files and video+audio files together.
* Added `--video-multistreams`, `--no-video-multistreams`, `--audio-multistreams`, `--no-audio-multistreams` * Added `--video-multistreams`, `--no-video-multistreams`, `--audio-multistreams`, `--no-audio-multistreams`
* Added `b`,`w`,`v`,`a` as alias for `best`, `worst`, `video` and `audio` respectively * Added `b`,`w`,`v`,`a` as alias for `best`, `worst`, `video` and `audio` respectively
* Shortcut Options: Added `--write-link`, `--write-url-link`, `--write-webloc-link`, `--write-desktop-link` by [h-h-h-h](https://github.com/h-h-h-h) - See [Internet Shortcut Options](README.md#internet-shortcut-options) for details * **Shortcut Options:** Added `--write-link`, `--write-url-link`, `--write-webloc-link`, `--write-desktop-link` by [h-h-h-h](https://github.com/h-h-h-h) - See [Internet Shortcut Options](README.md#internet-shortcut-options) for details
* **Sponskrub integration:** Added `--sponskrub`, `--sponskrub-cut`, `--sponskrub-force`, `--sponskrub-location`, `--sponskrub-args` - See [SponSkrub Options](README.md#sponskrub-sponsorblock-options) for details * **Sponskrub integration:** Added `--sponskrub`, `--sponskrub-cut`, `--sponskrub-force`, `--sponskrub-location`, `--sponskrub-args` - See [SponSkrub Options](README.md#sponskrub-sponsorblock-options) for details
* Added `--force-download-archive` (`--force-write-archive`) by [h-h-h-h](https://github.com/h-h-h-h) * Added `--force-download-archive` (`--force-write-archive`) by [h-h-h-h](https://github.com/h-h-h-h)
* Added `--list-formats-as-table`, `--list-formats-old` * Added `--list-formats-as-table`, `--list-formats-old`
@@ -405,38 +320,36 @@
* Relaxed validation for format filters so that any arbitrary field can be used * Relaxed validation for format filters so that any arbitrary field can be used
* Fix for embedding thumbnail in mp3 by [pauldubois98](https://github.com/pauldubois98) ([ytdl-org/youtube-dl#21569](https://github.com/ytdl-org/youtube-dl/pull/21569)) * Fix for embedding thumbnail in mp3 by [pauldubois98](https://github.com/pauldubois98) ([ytdl-org/youtube-dl#21569](https://github.com/ytdl-org/youtube-dl/pull/21569))
* Make Twitch Video ID output from Playlist and VOD extractor same. This is only a temporary fix * Make Twitch Video ID output from Playlist and VOD extractor same. This is only a temporary fix
* Merge youtube-dl: Upto [2021.01.03](https://github.com/ytdl-org/youtube-dl/commit/8e953dcbb10a1a42f4e12e4e132657cb0100a1f8) - See [blackjack4494/yt-dlc#280](https://github.com/blackjack4494/yt-dlc/pull/280) for details * **Merge youtube-dl:** Upto [2021.01.03](https://github.com/ytdl-org/youtube-dl/commit/8e953dcbb10a1a42f4e12e4e132657cb0100a1f8) - See [blackjack4494/yt-dlc#280](https://github.com/blackjack4494/yt-dlc/pull/280) for details
* Extractors [tiktok](https://github.com/ytdl-org/youtube-dl/commit/fb626c05867deab04425bad0c0b16b55473841a2) and [hotstar](https://github.com/ytdl-org/youtube-dl/commit/bb38a1215718cdf36d73ff0a7830a64cd9fa37cc) have not been merged * Extractors [tiktok](https://github.com/ytdl-org/youtube-dl/commit/fb626c05867deab04425bad0c0b16b55473841a2) and [hotstar](https://github.com/ytdl-org/youtube-dl/commit/bb38a1215718cdf36d73ff0a7830a64cd9fa37cc) have not been merged
* Cleaned up the fork for public use * Cleaned up the fork for public use
**PS**: All uncredited changes above this point are authored by [pukkandan](https://github.com/pukkandan)
### Unreleased changes in [blackjack4494/yt-dlc](https://github.com/blackjack4494/yt-dlc) ### Unreleased changes in [blackjack4494/yt-dlc](https://github.com/blackjack4494/yt-dlc)
* Updated to youtube-dl release 2020.11.26 by [pukkandan](https://github.com/pukkandan) * Updated to youtube-dl release 2020.11.26
* Youtube improvements by [pukkandan](https://github.com/pukkandan) * [youtube]
* Implemented all Youtube Feeds (ytfav, ytwatchlater, ytsubs, ythistory, ytrec) and SearchURL * Implemented all Youtube Feeds (ytfav, ytwatchlater, ytsubs, ythistory, ytrec) and SearchURL
* Fix ytsearch not returning results sometimes due to promoted content
* Temporary fix for automatic captions - disable json3
* Fix some improper Youtube URLs * Fix some improper Youtube URLs
* Redirect channel home to /video * Redirect channel home to /video
* Print youtube's warning message * Print youtube's warning message
* Handle Multiple pages for feeds better * Multiple pages are handled better for feeds
* [youtube] Fix ytsearch not returning results sometimes due to promoted content by [colethedj](https://github.com/colethedj)
* [youtube] Temporary fix for automatic captions - disable json3 by [blackjack4494](https://github.com/blackjack4494)
* Add --break-on-existing by [gergesh](https://github.com/gergesh) * Add --break-on-existing by [gergesh](https://github.com/gergesh)
* Pre-check video IDs in the archive before downloading by [pukkandan](https://github.com/pukkandan) * Pre-check video IDs in the archive before downloading
* [bitwave.tv] New extractor by [lorpus](https://github.com/lorpus) * [bitwave.tv] New extractor
* [Gedi] Add extractor by [nixxo](https://github.com/nixxo) * [Gedi] Add extractor
* [Rcs] Add new extractor by [nixxo](https://github.com/nixxo) * [Rcs] Add new extractor
* [skyit] New skyitalia extractor by [nixxo](https://github.com/nixxo) * [skyit] Add support for multiple Sky Italia website and removed old skyitalia extractor
* [france.tv] Fix thumbnail URL by [renalid](https://github.com/renalid) * [france.tv] Fix thumbnail URL
* [ina] support mobile links by [B0pol](https://github.com/B0pol) * [ina] support mobile links
* [instagram] Fix thumbnail extractor by [nao20010128nao](https://github.com/nao20010128nao) * [instagram] Fix extractor
* [SouthparkDe] Support for English URLs by [xypwn](https://github.com/xypwn) * [itv] BTCC new pages' URL update (articles instead of races)
* [spreaker] fix SpreakerShowIE test URL by [pukkandan](https://github.com/pukkandan) * [SouthparkDe] Support for English URLs
* [Vlive] Fix playlist handling when downloading a channel by [kyuyeunk](https://github.com/kyuyeunk) * [spreaker] fix SpreakerShowIE test URL
* [tmz] Fix extractor by [diegorodriguezv](https://github.com/diegorodriguezv) * [Vlive] Fix playlist handling when downloading a channel
* [generic] Detect embedded bitchute videos by [pukkandan](https://github.com/pukkandan) * [generic] Detect embedded bitchute videos
* [generic] Extract embedded youtube and twitter videos by [diegorodriguezv](https://github.com/diegorodriguezv) * [generic] Extract embedded youtube and twitter videos
* [ffmpeg] Ensure all streams are copied by [pukkandan](https://github.com/pukkandan) * [ffmpeg] Ensure all streams are copied
* [embedthumbnail] Fix for os.rename error by [pukkandan](https://github.com/pukkandan) * Fix for os.rename error when embedding thumbnail to video in a different drive
* make_win.bat: don't use UPX to pack vcruntime140.dll by [jbruchon](https://github.com/jbruchon) * make_win.bat: don't use UPX to pack vcruntime140.dll
+1 -1
View File
@@ -1,4 +1,4 @@
all: yt-dlp doc pypi-files all: yt-dlp doc
clean: clean-test clean-dist clean-cache clean: clean-test clean-dist clean-cache
completions: completion-bash completion-fish completion-zsh completions: completion-bash completion-fish completion-zsh
doc: README.md CONTRIBUTING.md issuetemplates supportedsites doc: README.md CONTRIBUTING.md issuetemplates supportedsites
+52 -134
View File
@@ -3,7 +3,7 @@
[![Release version](https://img.shields.io/github/v/release/yt-dlp/yt-dlp?color=brightgreen&label=Release)](https://github.com/yt-dlp/yt-dlp/releases/latest) [![Release version](https://img.shields.io/github/v/release/yt-dlp/yt-dlp?color=brightgreen&label=Release)](https://github.com/yt-dlp/yt-dlp/releases/latest)
[![License: Unlicense](https://img.shields.io/badge/License-Unlicense-blue.svg)](LICENSE) [![License: Unlicense](https://img.shields.io/badge/License-Unlicense-blue.svg)](LICENSE)
[![CI Status](https://github.com/yt-dlp/yt-dlp/workflows/Core%20Tests/badge.svg?branch=master)](https://github.com/yt-dlp/yt-dlp/actions) [![CI Status](https://github.com/yt-dlp/yt-dlp/workflows/Core%20Tests/badge.svg?branch=master)](https://github.com/yt-dlp/yt-dlp/actions)
[![Discord](https://img.shields.io/discord/807245652072857610?color=blue&label=discord&logo=discord)](https://discord.gg/H5MNcFW63r) [![Discord](https://img.shields.io/discord/807245652072857610?color=blue&label=discord&logo=discord)](https://discord.gg/S75JaBna)
[![Commits](https://img.shields.io/github/commit-activity/m/yt-dlp/yt-dlp?label=commits)](https://github.com/yt-dlp/yt-dlp/commits) [![Commits](https://img.shields.io/github/commit-activity/m/yt-dlp/yt-dlp?label=commits)](https://github.com/yt-dlp/yt-dlp/commits)
[![Last Commit](https://img.shields.io/github/last-commit/yt-dlp/yt-dlp/master)](https://github.com/yt-dlp/yt-dlp/commits) [![Last Commit](https://img.shields.io/github/last-commit/yt-dlp/yt-dlp/master)](https://github.com/yt-dlp/yt-dlp/commits)
@@ -13,7 +13,7 @@
A command-line program to download videos from youtube.com and many other [video platforms](supportedsites.md) A command-line program to download videos from youtube.com and many other [video platforms](supportedsites.md)
This is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on the now inactive [youtube-dlc](https://github.com/blackjack4494/yt-dlc). The main focus of this project is adding new features and patches while also keeping up to date with the original project This is a fork of [youtube-dlc](https://github.com/blackjack4494/yt-dlc) which is inturn a fork of [youtube-dl](https://github.com/ytdl-org/youtube-dl)
* [NEW FEATURES](#new-features) * [NEW FEATURES](#new-features)
* [INSTALLATION](#installation) * [INSTALLATION](#installation)
@@ -47,7 +47,6 @@ This is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on the
* [Sorting Formats](#sorting-formats) * [Sorting Formats](#sorting-formats)
* [Format Selection examples](#format-selection-examples) * [Format Selection examples](#format-selection-examples)
* [PLUGINS](#plugins) * [PLUGINS](#plugins)
* [DEPRECATED OPTIONS](#deprecated-options)
* [MORE](#more) * [MORE](#more)
@@ -58,7 +57,7 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
* **[Format Sorting](#sorting-formats)**: The default format sorting options have been changed so that higher resolution and better codecs will be now preferred instead of simply using larger bitrate. Furthermore, you can now specify the sort order using `-S`. This allows for much easier format selection that what is possible by simply using `--format` ([examples](#format-selection-examples)) * **[Format Sorting](#sorting-formats)**: The default format sorting options have been changed so that higher resolution and better codecs will be now preferred instead of simply using larger bitrate. Furthermore, you can now specify the sort order using `-S`. This allows for much easier format selection that what is possible by simply using `--format` ([examples](#format-selection-examples))
* **Merged with youtube-dl v2021.03.25**: You get all the latest features and patches of [youtube-dl](https://github.com/ytdl-org/youtube-dl) in addition to all the features of [youtube-dlc](https://github.com/blackjack4494/yt-dlc) * **Merged with youtube-dl v2021.03.03**: You get all the latest features and patches of [youtube-dl](https://github.com/ytdl-org/youtube-dl) in addition to all the features of [youtube-dlc](https://github.com/blackjack4494/yt-dlc)
* **Merged with animelover1984/youtube-dl**: You get most of the features and improvements from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl) including `--get-comments`, `BiliBiliSearch`, `BilibiliChannel`, Embedding thumbnail in mp4/ogg/opus, Playlist infojson etc. Note that the NicoNico improvements are not available. See [#31](https://github.com/yt-dlp/yt-dlp/pull/31) for details. * **Merged with animelover1984/youtube-dl**: You get most of the features and improvements from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl) including `--get-comments`, `BiliBiliSearch`, `BilibiliChannel`, Embedding thumbnail in mp4/ogg/opus, Playlist infojson etc. Note that the NicoNico improvements are not available. See [#31](https://github.com/yt-dlp/yt-dlp/pull/31) for details.
@@ -67,19 +66,17 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
* Youtube search (`ytsearch:`, `ytsearchdate:`) along with Search URLs works correctly * Youtube search (`ytsearch:`, `ytsearchdate:`) along with Search URLs works correctly
* Redirect channel's home URL automatically to `/video` to preserve the old behaviour * Redirect channel's home URL automatically to `/video` to preserve the old behaviour
* **Split video by chapters**: Videos can be split into multiple files based on chapters using `--split-chapters`
* **Multithreaded fragment downloads**: Fragment downloads can be natively multi-threaded. Use `--concurrent-fragments` (`-N`) option to set the number of threads used
* **Aria2c with HLS/DASH**: You can use aria2c as the external downloader for DASH(mpd) and HLS(m3u8) formats. No more slow ffmpeg/native downloads * **Aria2c with HLS/DASH**: You can use aria2c as the external downloader for DASH(mpd) and HLS(m3u8) formats. No more slow ffmpeg/native downloads
* **New extractors**: AnimeLab, Philo MSO, Rcs, Gedi, bitwave.tv, mildom, audius, zee5, mtv.it, wimtv, pluto.tv * **New extractors**: AnimeLab, Philo MSO, Rcs, Gedi, bitwave.tv, mildom, audius, zee5
* **Fixed extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, tiktok, akamai, ina, rumble, tennistv, amcnetworks * **Fixed extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, tiktok, akamai, ina, rumble, tennistv
* **Plugin extractors**: Extractors can be loaded from an external file. See [plugins](#plugins) for details * **Plugin support**: Extractors can be loaded from an external file. See [plugins](#plugins) for details
* **Multiple paths and output templates**: You can give different [output templates](#output-template) and download paths for different types of files. You can also set a temporary path where intermediary files are downloaded to using `--paths` (`-P`) * **Multiple paths and output templates**: You can give different [output templates](#output-template) and download paths for different types of files. You can also set a temporary path where intermediary files are downloaded to. See [`--paths`](https://github.com/yt-dlp/yt-dlp/#:~:text=-P,%20--paths%20TYPE:PATH) for details
<!-- Relative link doesn't work for "#:~:text=" -->
* **Portable Configuration**: Configuration files are automatically loaded from the home and root directories. See [configuration](#configuration) for details * **Portable Configuration**: Configuration files are automatically loaded from the home and root directories. See [configuration](#configuration) for details
@@ -106,23 +103,6 @@ You can install yt-dlp using one of the following methods:
* Use pip+git: `python -m pip install --upgrade git+https://github.com/yt-dlp/yt-dlp.git@release` * Use pip+git: `python -m pip install --upgrade git+https://github.com/yt-dlp/yt-dlp.git@release`
* Install master branch: `python -m pip install --upgrade git+https://github.com/yt-dlp/yt-dlp` * Install master branch: `python -m pip install --upgrade git+https://github.com/yt-dlp/yt-dlp`
UNIX users (Linux, macOS, BSD) can also install the [latest release](https://github.com/yt-dlp/yt-dlp/releases/latest) one of the following ways:
```
sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
sudo chmod a+rx /usr/local/bin/yt-dlp
```
```
sudo wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp
sudo chmod a+rx /usr/local/bin/yt-dlp
```
```
sudo aria2c https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
sudo chmod a+rx /usr/local/bin/yt-dlp
```
### UPDATE ### UPDATE
Starting from version `2021.02.09`, you can use `yt-dlp -U` to update if you are using the provided release. Starting from version `2021.02.09`, you can use `yt-dlp -U` to update if you are using the provided release.
If you are using `pip`, simply re-run the same command that was used to install the program. If you are using `pip`, simply re-run the same command that was used to install the program.
@@ -197,7 +177,7 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
only list them only list them
--no-flat-playlist Extract the videos of a playlist --no-flat-playlist Extract the videos of a playlist
--mark-watched Mark videos watched (YouTube only) --mark-watched Mark videos watched (YouTube only)
--no-mark-watched Do not mark videos watched (default) --no-mark-watched Do not mark videos watched
--no-colors Do not emit color codes in output --no-colors Do not emit color codes in output
## Network Options: ## Network Options:
@@ -300,8 +280,6 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--no-include-ads Do not download advertisements (default) --no-include-ads Do not download advertisements (default)
## Download Options: ## Download Options:
-N, --concurrent-fragments N Number of fragments to download
concurrently (default is 1)
-r, --limit-rate RATE Maximum download rate in bytes per second -r, --limit-rate RATE Maximum download rate in bytes per second
(e.g. 50K or 4.2M) (e.g. 50K or 4.2M)
-R, --retries RETRIES Number of retries (default is 10), or -R, --retries RETRIES Number of retries (default is 10), or
@@ -424,11 +402,6 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--write-description etc. (default) --write-description etc. (default)
--no-write-playlist-metafiles Do not write playlist metadata when using --no-write-playlist-metafiles Do not write playlist metadata when using
--write-info-json, --write-description etc. --write-info-json, --write-description etc.
--clean-infojson Remove some private fields such as
filenames from the infojson. Note that it
could still contain some personal
information (default)
--no-clean-infojson Write all fields to the infojson
--get-comments Retrieve video comments to be placed in the --get-comments Retrieve video comments to be placed in the
.info.json file. The comments are fetched .info.json file. The comments are fetched
even without this option if the extraction even without this option if the extraction
@@ -472,8 +445,7 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--no-warnings Ignore warnings --no-warnings Ignore warnings
-s, --simulate Do not download the video and do not write -s, --simulate Do not download the video and do not write
anything to disk anything to disk
--skip-download Do not download the video but write all --skip-download Do not download the video
related files (Alias: --no-download)
-g, --get-url Simulate, quiet but print URL -g, --get-url Simulate, quiet but print URL
-e, --get-title Simulate, quiet but print title -e, --get-title Simulate, quiet but print title
--get-id Simulate, quiet but print id --get-id Simulate, quiet but print id
@@ -510,7 +482,7 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--encoding ENCODING Force the specified encoding (experimental) --encoding ENCODING Force the specified encoding (experimental)
--no-check-certificate Suppress HTTPS certificate validation --no-check-certificate Suppress HTTPS certificate validation
--prefer-insecure Use an unencrypted connection to retrieve --prefer-insecure Use an unencrypted connection to retrieve
information about the video (Currently information about the video. (Currently
supported only for YouTube) supported only for YouTube)
--user-agent UA Specify a custom user agent --user-agent UA Specify a custom user agent
--referer URL Specify a custom referer, use if the video --referer URL Specify a custom referer, use if the video
@@ -524,11 +496,15 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--sleep-requests SECONDS Number of seconds to sleep between requests --sleep-requests SECONDS Number of seconds to sleep between requests
during data extraction during data extraction
--sleep-interval SECONDS Number of seconds to sleep before each --sleep-interval SECONDS Number of seconds to sleep before each
download. This is the minimum time to sleep download when used alone or a lower bound
when used along with --max-sleep-interval of a range for randomized sleep before each
(Alias: --min-sleep-interval) download (minimum possible number of
--max-sleep-interval SECONDS Maximum number of seconds to sleep. Can seconds to sleep) when used along with
only be used along with --min-sleep-interval --max-sleep-interval
--max-sleep-interval SECONDS Upper bound of a range for randomized sleep
before each download (maximum possible
number of seconds to sleep). Must only be
used along with --min-sleep-interval
--sleep-subtitles SECONDS Number of seconds to sleep before each --sleep-subtitles SECONDS Number of seconds to sleep before each
subtitle download subtitle download
@@ -578,16 +554,16 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--write-subs Write subtitle file --write-subs Write subtitle file
--no-write-subs Do not write subtitle file (default) --no-write-subs Do not write subtitle file (default)
--write-auto-subs Write automatically generated subtitle file --write-auto-subs Write automatically generated subtitle file
(Alias: --write-automatic-subs) (YouTube only)
--no-write-auto-subs Do not write auto-generated subtitles --no-write-auto-subs Do not write automatically generated
(default) (Alias: --no-write-automatic-subs) subtitle file (default)
--all-subs Download all the available subtitles of the --all-subs Download all the available subtitles of the
video video
--list-subs List all available subtitles for the video --list-subs List all available subtitles for the video
--sub-format FORMAT Subtitle format, accepts formats --sub-format FORMAT Subtitle format, accepts formats
preference, for example: "srt" or preference, for example: "srt" or
"ass/srt/best" "ass/srt/best"
--sub-langs LANGS Languages of the subtitles to download --sub-lang LANGS Languages of the subtitles to download
(optional) separated by commas, use --list- (optional) separated by commas, use --list-
subs for available language tags subs for available language tags
@@ -641,19 +617,18 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
ExtractAudio, VideoRemuxer, VideoConvertor, ExtractAudio, VideoRemuxer, VideoConvertor,
EmbedSubtitle, Metadata, Merger, EmbedSubtitle, Metadata, Merger,
FixupStretched, FixupM4a, FixupM3u8, FixupStretched, FixupM4a, FixupM3u8,
SubtitlesConvertor, EmbedThumbnail and SubtitlesConvertor and EmbedThumbnail. The
SplitChapters. The supported executables supported executables are: SponSkrub,
are: SponSkrub, FFmpeg, FFprobe, and FFmpeg, FFprobe, and AtomicParsley. You can
AtomicParsley. You can also specify also specify "PP+EXE:ARGS" to give the
"PP+EXE:ARGS" to give the arguments to the arguments to the specified executable only
specified executable only when being used when being used by the specified
by the specified postprocessor. postprocessor. Additionally, for
Additionally, for ffmpeg/ffprobe, "_i"/"_o" ffmpeg/ffprobe, a number can be appended to
can be appended to the prefix optionally the exe name seperated by "_i" to pass the
followed by a number to pass the argument argument before the specified input file.
before the specified input/output file. Eg: Eg: --ppa "Merger+ffmpeg_i1:-v quiet". You
--ppa "Merger+ffmpeg_i1:-v quiet". You can can use this option multiple times to give
use this option multiple times to give
different arguments to different different arguments to different
postprocessors. (Alias: --ppa) postprocessors. (Alias: --ppa)
-k, --keep-video Keep the intermediate video file on disk -k, --keep-video Keep the intermediate video file on disk
@@ -670,24 +645,18 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--add-metadata Write metadata to the video file --add-metadata Write metadata to the video file
--no-add-metadata Do not write metadata (default) --no-add-metadata Do not write metadata (default)
--parse-metadata FIELD:FORMAT Parse additional metadata like title/artist --parse-metadata FIELD:FORMAT Parse additional metadata like title/artist
from other fields. Give a template or field from other fields. Give field name to
name to extract data from and the format to extract data from, and format of the field
interpret it as, seperated by a ":". Either seperated by a ":". Either regular
regular expression with named capture expression with named capture groups or a
groups or a similar syntax to the output similar syntax to the output template can
template can be used for the FORMAT. also be used. The parsed parameters replace
Similarly, the syntax for output template any existing values and can be use in
can be used for FIELD to parse the data output template. This option can be used
from multiple fields. The parsed parameters multiple times. Example: --parse-metadata
replace any existing values and can be used "title:%(artist)s - %(title)s" matches a
in output templates. This option can be title like "Coldplay - Paradise". Example
used multiple times. Example: --parse- (regex): --parse-metadata
metadata "title:%(artist)s - %(title)s"
matches a title like "Coldplay - Paradise".
Example: --parse-metadata "%(series)s
%(episode_number)s:%(title)s" sets the
title using series and episode number.
Example (regex): --parse-metadata
"description:Artist - (?P<artist>.+?)" "description:Artist - (?P<artist>.+?)"
--xattrs Write metadata to the video file's xattrs --xattrs Write metadata to the video file's xattrs
(using dublin core and xdg standards) (using dublin core and xdg standards)
@@ -703,16 +672,8 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
downloading and post-processing, similar to downloading and post-processing, similar to
find's -exec syntax. Example: --exec 'adb find's -exec syntax. Example: --exec 'adb
push {} /sdcard/Music/ && rm {}' push {} /sdcard/Music/ && rm {}'
--convert-subs FORMAT Convert the subtitles to another format --convert-subs FORMAT Convert the subtitles to other format
(currently supported: srt|ass|vtt|lrc) (currently supported: srt|ass|vtt|lrc)
(Alias: --convert-subtitles)
--split-chapters Split video into multiple files based on
internal chapters. The "chapter:" prefix
can be used with "--paths" and "--output"
to set the output filename for the split
files. See "OUTPUT TEMPLATE" for details
--no-split-chapters Do not split video based on chapters
(default)
## SponSkrub (SponsorBlock) Options: ## SponSkrub (SponsorBlock) Options:
[SponSkrub](https://github.com/yt-dlp/SponSkrub) is a utility to [SponSkrub](https://github.com/yt-dlp/SponSkrub) is a utility to
@@ -828,9 +789,9 @@ The `-o` option is used to indicate a template for the output file names while `
**tl;dr:** [navigate me to examples](#output-template-examples). **tl;dr:** [navigate me to examples](#output-template-examples).
The basic usage of `-o` is not to set any template arguments when downloading a single file, like in `yt-dlp -o funny_video.flv "https://some/video"` (hard-coding file extension like this is not recommended). However, it may contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [python string formatting operations](https://docs.python.org/2/library/stdtypes.html#string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations. Date/time fields can also be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it inside the parantheses seperated from the field name using a `>`. For example, `%(duration>%H-%M-%S)s`. The basic usage of `-o` is not to set any template arguments when downloading a single file, like in `yt-dlp -o funny_video.flv "https://some/video"`. However, it may contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [python string formatting operations](https://docs.python.org/2/library/stdtypes.html#string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations. Date/time fields can also be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it inside the parantheses seperated from the field name using a `>`. For example, `%(duration>%H-%M-%S)s`.
Additionally, you can set different output templates for the various metadata files seperately from the general output template by specifying the type of file followed by the template seperated by a colon ":". The different filetypes supported are `subtitle`, `thumbnail`, `description`, `annotation`, `infojson`, `pl_description`, `pl_infojson`, `chapter`. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video. Additionally, you can set different output templates for the various metadata files seperately from the general output template by specifying the type of file followed by the template seperated by a colon ":". The different filetypes supported are `subtitle|thumbnail|description|annotation|infojson|pl_description|pl_infojson`. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video.
The available fields are: The available fields are:
@@ -839,7 +800,6 @@ The available fields are:
- `url` (string): Video URL - `url` (string): Video URL
- `ext` (string): Video filename extension - `ext` (string): Video filename extension
- `alt_title` (string): A secondary title of the video - `alt_title` (string): A secondary title of the video
- `description` (string): The description of the video
- `display_id` (string): An alternative identifier for the video - `display_id` (string): An alternative identifier for the video
- `uploader` (string): Full name of the video uploader - `uploader` (string): Full name of the video uploader
- `license` (string): License name the video is licensed under - `license` (string): License name the video is licensed under
@@ -863,7 +823,6 @@ The available fields are:
- `is_live` (boolean): Whether this video is a live stream or a fixed-length video - `is_live` (boolean): Whether this video is a live stream or a fixed-length video
- `was_live` (boolean): Whether this video was originally a live stream - `was_live` (boolean): Whether this video was originally a live stream
- `playable_in_embed` (string): Whether this video is allowed to play in embedded players on other sites - `playable_in_embed` (string): Whether this video is allowed to play in embedded players on other sites
- `availability` (string): Whether the video is 'private', 'premium_only', 'subscriber_only', 'needs_auth', 'unlisted' or 'public'
- `start_time` (numeric): Time in seconds where the reproduction should start, as specified in the URL - `start_time` (numeric): Time in seconds where the reproduction should start, as specified in the URL
- `end_time` (numeric): Time in seconds where the reproduction should end, as specified in the URL - `end_time` (numeric): Time in seconds where the reproduction should end, as specified in the URL
- `format` (string): A human-readable description of the format - `format` (string): A human-readable description of the format
@@ -923,13 +882,6 @@ Available for the media that is a track or a part of a music album:
- `disc_number` (numeric): Number of the disc or other physical medium the track belongs to - `disc_number` (numeric): Number of the disc or other physical medium the track belongs to
- `release_year` (numeric): Year (YYYY) when the album was released - `release_year` (numeric): Year (YYYY) when the album was released
Available when using `--split-chapters` for videos with internal chapters:
- `section_title` (string): Title of the chapter
- `section_number` (numeric): Number of the chapter within the file
- `section_start` (numeric): Start time of the chapter in seconds
- `section_end` (numeric): End time of the chapter in seconds
Each aforementioned sequence when referenced in an output template will be replaced by the actual value corresponding to the sequence name. Note that some of the sequences are not guaranteed to be present since they depend on the metadata obtained by a particular extractor. Such sequences will be replaced with placeholder value provided with `--output-na-placeholder` (`NA` by default). Each aforementioned sequence when referenced in an output template will be replaced by the actual value corresponding to the sequence name. Note that some of the sequences are not guaranteed to be present since they depend on the metadata obtained by a particular extractor. Such sequences will be replaced with placeholder value provided with `--output-na-placeholder` (`NA` by default).
For example for `-o %(title)s-%(id)s.%(ext)s` and an mp4 video with title `yt-dlp test video` and id `BaW_jenozKcj`, this will result in a `yt-dlp test video-BaW_jenozKcj.mp4` file created in the current directory. For example for `-o %(title)s-%(id)s.%(ext)s` and an mp4 video with title `yt-dlp test video` and id `BaW_jenozKcj`, this will result in a `yt-dlp test video-BaW_jenozKcj.mp4` file created in the current directory.
@@ -1209,39 +1161,5 @@ Plugins are loaded from `<root-dir>/ytdlp_plugins/<type>/__init__.py`. Currently
**Note**: `<root-dir>` is the directory of the binary (`<root-dir>/yt-dlp`), or the root directory of the module if you are running directly from source-code (`<root dir>/yt_dlp/__main__.py`) **Note**: `<root-dir>` is the directory of the binary (`<root-dir>/yt-dlp`), or the root directory of the module if you are running directly from source-code (`<root dir>/yt_dlp/__main__.py`)
# DEPRECATED OPTIONS
These are all the deprecated options and the current alternative to achieve the same effect
--cn-verification-proxy URL --geo-verification-proxy URL
--id -o "%(id)s.%(ext)s"
-A, --auto-number -o "%(autonumber)s-%(id)s.%(ext)s"
-t, --title -o "%(title)s-%(id)s.%(ext)s"
-l, --literal -o accepts literal names
--autonumber-size NUMBER Use string formatting. Eg: %(autonumber)03d
--metadata-from-title FORMAT --parse-metadata "title:FORMAT"
--prefer-avconv avconv is no longer officially supported (Alias: --no-prefer-ffmpeg)
--prefer-ffmpeg Default (Alias: --no-prefer-avconv)
--avconv-location avconv is no longer officially supported
-C, --call-home Not implemented
--no-call-home Default
--write-srt --write-subs
--no-write-srt --no-write-subs
--srt-lang LANGS --sub-langs LANGS
--prefer-unsecure --prefer-insecure
--rate-limit RATE --limit-rate RATE
--force-write-download-archive --force-write-archive
--dump-intermediate-pages --dump-pages
--dump-headers --print-traffic
--youtube-print-sig-code No longer supported
--trim-file-names LENGTH --trim-filenames LENGTH
--yes-overwrites --force-overwrites
--load-info --load-info-json
--split-tracks --split-chapters
--no-split-tracks --no-split-chapters
--sponskrub-args ARGS --ppa "sponskrub:ARGS"
--test Only used for testing extractors
# MORE # MORE
For FAQ, Developer Instructions etc., see the [original README](https://github.com/ytdl-org/youtube-dl#faq) For FAQ, Developer Instructions etc., see the [original README](https://github.com/ytdl-org/youtube-dl#faq)
+3 -12
View File
@@ -97,8 +97,7 @@
- **bbc**: BBC - **bbc**: BBC
- **bbc.co.uk**: BBC iPlayer - **bbc.co.uk**: BBC iPlayer
- **bbc.co.uk:article**: BBC articles - **bbc.co.uk:article**: BBC articles
- **bbc.co.uk:iplayer:episodes** - **bbc.co.uk:iplayer:playlist**
- **bbc.co.uk:iplayer:group**
- **bbc.co.uk:playlist** - **bbc.co.uk:playlist**
- **BBVTV** - **BBVTV**
- **Beatport** - **Beatport**
@@ -348,7 +347,8 @@
- **Gaskrank** - **Gaskrank**
- **Gazeta** - **Gazeta**
- **GDCVault** - **GDCVault**
- **GediDigital** - **Gedi**
- **GediEmbeds**
- **generic**: Generic downloader that works on some sites - **generic**: Generic downloader that works on some sites
- **Gfycat** - **Gfycat**
- **GiantBomb** - **GiantBomb**
@@ -544,7 +544,6 @@
- **mixcloud:playlist** - **mixcloud:playlist**
- **mixcloud:user** - **mixcloud:user**
- **MLB** - **MLB**
- **MLBVideo**
- **Mnet** - **Mnet**
- **MNetTV** - **MNetTV**
- **MoeVideo**: LetitBit video services: moevideo.net, playreplay.net and videochart.net - **MoeVideo**: LetitBit video services: moevideo.net, playreplay.net and videochart.net
@@ -563,8 +562,6 @@
- **mtg**: MTG services - **mtg**: MTG services
- **mtv** - **mtv**
- **mtv.de** - **mtv.de**
- **mtv.it**
- **mtv.it:programma**
- **mtv:video** - **mtv:video**
- **mtvjapan** - **mtvjapan**
- **mtvservices:embedded** - **mtvservices:embedded**
@@ -636,7 +633,6 @@
- **nicknight** - **nicknight**
- **niconico**: ニコニコ動画 - **niconico**: ニコニコ動画
- **NiconicoPlaylist** - **NiconicoPlaylist**
- **NiconicoUser**
- **Nintendo** - **Nintendo**
- **Nitter** - **Nitter**
- **njoy**: N-JOY - **njoy**: N-JOY
@@ -739,7 +735,6 @@
- **Playwire** - **Playwire**
- **pluralsight** - **pluralsight**
- **pluralsight:course** - **pluralsight:course**
- **PlutoTV**
- **podomatic** - **podomatic**
- **Pokemon** - **Pokemon**
- **PokemonWatch** - **PokemonWatch**
@@ -920,7 +915,6 @@
- **stanfordoc**: Stanford Open ClassRoom - **stanfordoc**: Stanford Open ClassRoom
- **Steam** - **Steam**
- **Stitcher** - **Stitcher**
- **StitcherShow**
- **StoryFire** - **StoryFire**
- **StoryFireSeries** - **StoryFireSeries**
- **StoryFireUser** - **StoryFireUser**
@@ -1178,7 +1172,6 @@
- **Weibo** - **Weibo**
- **WeiboMobile** - **WeiboMobile**
- **WeiqiTV**: WQTV - **WeiqiTV**: WQTV
- **WimTV**
- **Wistia** - **Wistia**
- **WistiaPlaylist** - **WistiaPlaylist**
- **wnl**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl - **wnl**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
@@ -1249,9 +1242,7 @@
- **ZDF** - **ZDF**
- **ZDFChannel** - **ZDFChannel**
- **Zee5** - **Zee5**
- **zee5:series**
- **Zhihu** - **Zhihu**
- **zingmp3**: mp3.zing.vn - **zingmp3**: mp3.zing.vn
- **zingmp3:album**
- **zoom** - **zoom**
- **Zype** - **Zype**
+1
View File
@@ -37,6 +37,7 @@ class TestAllURLsMatching(unittest.TestCase):
assertPlaylist('PL63F0C78739B09958') assertPlaylist('PL63F0C78739B09958')
assertTab('https://www.youtube.com/AsapSCIENCE') assertTab('https://www.youtube.com/AsapSCIENCE')
assertTab('https://www.youtube.com/embedded') assertTab('https://www.youtube.com/embedded')
assertTab('https://www.youtube.com/feed') # Own channel's home page
assertTab('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q') assertTab('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')
assertTab('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8') assertTab('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
assertTab('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC') assertTab('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')
+205 -243
View File
@@ -60,14 +60,12 @@ from .utils import (
encode_compat_str, encode_compat_str,
encodeFilename, encodeFilename,
error_to_compat_str, error_to_compat_str,
EntryNotInPlaylist,
ExistingVideoReached, ExistingVideoReached,
expand_path, expand_path,
ExtractorError, ExtractorError,
float_or_none, float_or_none,
format_bytes, format_bytes,
format_field, format_field,
FORMAT_RE,
formatSeconds, formatSeconds,
GeoRestrictedError, GeoRestrictedError,
int_or_none, int_or_none,
@@ -218,7 +216,6 @@ class YoutubeDL(object):
logtostderr: Log messages to stderr instead of stdout. logtostderr: Log messages to stderr instead of stdout.
writedescription: Write the video description to a .description file writedescription: Write the video description to a .description file
writeinfojson: Write the video description to a .info.json file writeinfojson: Write the video description to a .info.json file
clean_infojson: Remove private fields from the infojson
writecomments: Extract video comments. This will not be written to disk writecomments: Extract video comments. This will not be written to disk
unless writeinfojson is also given unless writeinfojson is also given
writeannotations: Write the video annotations to a .annotations.xml file writeannotations: Write the video annotations to a .annotations.xml file
@@ -773,93 +770,95 @@ class YoutubeDL(object):
'Put from __future__ import unicode_literals at the top of your code file or consider switching to Python 3.x.') 'Put from __future__ import unicode_literals at the top of your code file or consider switching to Python 3.x.')
return outtmpl_dict return outtmpl_dict
def prepare_outtmpl(self, outtmpl, info_dict, sanitize=None):
""" Make the template and info_dict suitable for substitution (outtmpl % info_dict)"""
template_dict = dict(info_dict)
# duration_string
template_dict['duration_string'] = ( # %(duration>%H-%M-%S)s is wrong if duration > 24hrs
formatSeconds(info_dict['duration'], '-')
if info_dict.get('duration', None) is not None
else None)
# epoch
template_dict['epoch'] = int(time.time())
# autonumber
autonumber_size = self.params.get('autonumber_size')
if autonumber_size is None:
autonumber_size = 5
template_dict['autonumber'] = self.params.get('autonumber_start', 1) - 1 + self._num_downloads
# resolution if not defined
if template_dict.get('resolution') is None:
if template_dict.get('width') and template_dict.get('height'):
template_dict['resolution'] = '%dx%d' % (template_dict['width'], template_dict['height'])
elif template_dict.get('height'):
template_dict['resolution'] = '%sp' % template_dict['height']
elif template_dict.get('width'):
template_dict['resolution'] = '%dx?' % template_dict['width']
if sanitize is None:
sanitize = lambda k, v: v
template_dict = dict((k, v if isinstance(v, compat_numeric_types) else sanitize(k, v))
for k, v in template_dict.items()
if v is not None and not isinstance(v, (list, tuple, dict)))
na = self.params.get('outtmpl_na_placeholder', 'NA')
template_dict = collections.defaultdict(lambda: na, template_dict)
# For fields playlist_index and autonumber convert all occurrences
# of %(field)s to %(field)0Nd for backward compatibility
field_size_compat_map = {
'playlist_index': len(str(template_dict['n_entries'])),
'autonumber': autonumber_size,
}
FIELD_SIZE_COMPAT_RE = r'(?<!%)%\((?P<field>autonumber|playlist_index)\)s'
mobj = re.search(FIELD_SIZE_COMPAT_RE, outtmpl)
if mobj:
outtmpl = re.sub(
FIELD_SIZE_COMPAT_RE,
r'%%(\1)0%dd' % field_size_compat_map[mobj.group('field')],
outtmpl)
numeric_fields = list(self._NUMERIC_FIELDS)
# Format date
FORMAT_DATE_RE = FORMAT_RE.format(r'(?P<key>(?P<field>\w+)>(?P<format>.+?))')
for mobj in re.finditer(FORMAT_DATE_RE, outtmpl):
conv_type, field, frmt, key = mobj.group('type', 'field', 'format', 'key')
if key in template_dict:
continue
value = strftime_or_none(template_dict.get(field), frmt, na)
if conv_type in 'crs': # string
value = sanitize(field, value)
else: # number
numeric_fields.append(key)
value = float_or_none(value, default=None)
if value is not None:
template_dict[key] = value
# Missing numeric fields used together with integer presentation types
# in format specification will break the argument substitution since
# string NA placeholder is returned for missing fields. We will patch
# output template for missing fields to meet string presentation type.
for numeric_field in numeric_fields:
if numeric_field not in template_dict:
outtmpl = re.sub(
FORMAT_RE.format(re.escape(numeric_field)),
r'%({0})s'.format(numeric_field), outtmpl)
return outtmpl, template_dict
def _prepare_filename(self, info_dict, tmpl_type='default'): def _prepare_filename(self, info_dict, tmpl_type='default'):
try: try:
template_dict = dict(info_dict)
template_dict['duration_string'] = ( # %(duration>%H-%M-%S)s is wrong if duration > 24hrs
formatSeconds(info_dict['duration'], '-')
if info_dict.get('duration', None) is not None
else None)
template_dict['epoch'] = int(time.time())
autonumber_size = self.params.get('autonumber_size')
if autonumber_size is None:
autonumber_size = 5
template_dict['autonumber'] = self.params.get('autonumber_start', 1) - 1 + self._num_downloads
if template_dict.get('resolution') is None:
if template_dict.get('width') and template_dict.get('height'):
template_dict['resolution'] = '%dx%d' % (template_dict['width'], template_dict['height'])
elif template_dict.get('height'):
template_dict['resolution'] = '%sp' % template_dict['height']
elif template_dict.get('width'):
template_dict['resolution'] = '%dx?' % template_dict['width']
sanitize = lambda k, v: sanitize_filename( sanitize = lambda k, v: sanitize_filename(
compat_str(v), compat_str(v),
restricted=self.params.get('restrictfilenames'), restricted=self.params.get('restrictfilenames'),
is_id=(k == 'id' or k.endswith('_id'))) is_id=(k == 'id' or k.endswith('_id')))
template_dict = dict((k, v if isinstance(v, compat_numeric_types) else sanitize(k, v))
for k, v in template_dict.items()
if v is not None and not isinstance(v, (list, tuple, dict)))
na = self.params.get('outtmpl_na_placeholder', 'NA')
template_dict = collections.defaultdict(lambda: na, template_dict)
outtmpl = self.outtmpl_dict.get(tmpl_type, self.outtmpl_dict['default']) outtmpl = self.outtmpl_dict.get(tmpl_type, self.outtmpl_dict['default'])
outtmpl, template_dict = self.prepare_outtmpl(outtmpl, info_dict, sanitize) force_ext = OUTTMPL_TYPES.get(tmpl_type)
# For fields playlist_index and autonumber convert all occurrences
# of %(field)s to %(field)0Nd for backward compatibility
field_size_compat_map = {
'playlist_index': len(str(template_dict['n_entries'])),
'autonumber': autonumber_size,
}
FIELD_SIZE_COMPAT_RE = r'(?<!%)%\((?P<field>autonumber|playlist_index)\)s'
mobj = re.search(FIELD_SIZE_COMPAT_RE, outtmpl)
if mobj:
outtmpl = re.sub(
FIELD_SIZE_COMPAT_RE,
r'%%(\1)0%dd' % field_size_compat_map[mobj.group('field')],
outtmpl)
# As of [1] format syntax is:
# %[mapping_key][conversion_flags][minimum_width][.precision][length_modifier]type
# 1. https://docs.python.org/2/library/stdtypes.html#string-formatting
FORMAT_RE = r'''(?x)
(?<!%)
%
\({0}\) # mapping key
(?:[#0\-+ ]+)? # conversion flags (optional)
(?:\d+)? # minimum field width (optional)
(?:\.\d+)? # precision (optional)
[hlL]? # length modifier (optional)
(?P<type>[diouxXeEfFgGcrs%]) # conversion type
'''
numeric_fields = list(self._NUMERIC_FIELDS)
# Format date
FORMAT_DATE_RE = FORMAT_RE.format(r'(?P<key>(?P<field>\w+)>(?P<format>.+?))')
for mobj in re.finditer(FORMAT_DATE_RE, outtmpl):
conv_type, field, frmt, key = mobj.group('type', 'field', 'format', 'key')
if key in template_dict:
continue
value = strftime_or_none(template_dict.get(field), frmt, na)
if conv_type in 'crs': # string
value = sanitize(field, value)
else: # number
numeric_fields.append(key)
value = float_or_none(value, default=None)
if value is not None:
template_dict[key] = value
# Missing numeric fields used together with integer presentation types
# in format specification will break the argument substitution since
# string NA placeholder is returned for missing fields. We will patch
# output template for missing fields to meet string presentation type.
for numeric_field in numeric_fields:
if numeric_field not in template_dict:
outtmpl = re.sub(
FORMAT_RE.format(re.escape(numeric_field)),
r'%({0})s'.format(numeric_field), outtmpl)
# expand_path translates '%%' into '%' and '$$' into '$' # expand_path translates '%%' into '%' and '$$' into '$'
# correspondingly that is not what we want since we need to keep # correspondingly that is not what we want since we need to keep
@@ -874,7 +873,6 @@ class YoutubeDL(object):
# title "Hello $PATH", we don't want `$PATH` to be expanded. # title "Hello $PATH", we don't want `$PATH` to be expanded.
filename = expand_path(outtmpl).replace(sep, '') % template_dict filename = expand_path(outtmpl).replace(sep, '') % template_dict
force_ext = OUTTMPL_TYPES.get(tmpl_type)
if force_ext is not None: if force_ext is not None:
filename = replace_extension(filename, force_ext, template_dict.get('ext')) filename = replace_extension(filename, force_ext, template_dict.get('ext'))
@@ -1173,24 +1171,57 @@ class YoutubeDL(object):
else: else:
raise Exception('Invalid result type: %s' % result_type) raise Exception('Invalid result type: %s' % result_type)
def _ensure_dir_exists(self, path):
return make_dir(path, self.report_error)
def __process_playlist(self, ie_result, download): def __process_playlist(self, ie_result, download):
# We process each entry in the playlist # We process each entry in the playlist
playlist = ie_result.get('title') or ie_result.get('id') playlist = ie_result.get('title') or ie_result.get('id')
self.to_screen('[download] Downloading playlist: %s' % playlist) self.to_screen('[download] Downloading playlist: %s' % playlist)
if 'entries' not in ie_result: if self.params.get('allow_playlist_files', True):
raise EntryNotInPlaylist() ie_copy = {
incomplete_entries = bool(ie_result.get('requested_entries')) 'playlist': playlist,
if incomplete_entries: 'playlist_id': ie_result.get('id'),
def fill_missing_entries(entries, indexes): 'playlist_title': ie_result.get('title'),
ret = [None] * max(*indexes) 'playlist_uploader': ie_result.get('uploader'),
for i, entry in zip(indexes, entries): 'playlist_uploader_id': ie_result.get('uploader_id'),
ret[i - 1] = entry 'playlist_index': 0
return ret }
ie_result['entries'] = fill_missing_entries(ie_result['entries'], ie_result['requested_entries']) ie_copy.update(dict(ie_result))
def ensure_dir_exists(path):
return make_dir(path, self.report_error)
if self.params.get('writeinfojson', False):
infofn = self.prepare_filename(ie_copy, 'pl_infojson')
if not ensure_dir_exists(encodeFilename(infofn)):
return
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
self.to_screen('[info] Playlist metadata is already present')
else:
playlist_info = dict(ie_result)
# playlist_info['entries'] = list(playlist_info['entries']) # Entries is a generator which shouldnot be resolved here
del playlist_info['entries']
self.to_screen('[info] Writing playlist metadata as JSON to: ' + infofn)
try:
write_json_file(self.filter_requested_info(playlist_info), infofn)
except (OSError, IOError):
self.report_error('Cannot write playlist metadata to JSON file ' + infofn)
if self.params.get('writedescription', False):
descfn = self.prepare_filename(ie_copy, 'pl_description')
if not ensure_dir_exists(encodeFilename(descfn)):
return
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
self.to_screen('[info] Playlist description is already present')
elif ie_result.get('description') is None:
self.report_warning('There\'s no playlist description to write.')
else:
try:
self.to_screen('[info] Writing playlist description to: ' + descfn)
with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
descfile.write(ie_result['description'])
except (OSError, IOError):
self.report_error('Cannot write playlist description file ' + descfn)
return
playlist_results = [] playlist_results = []
@@ -1217,20 +1248,25 @@ class YoutubeDL(object):
def make_playlistitems_entries(list_ie_entries): def make_playlistitems_entries(list_ie_entries):
num_entries = len(list_ie_entries) num_entries = len(list_ie_entries)
for i in playlistitems: return [
if -num_entries < i <= num_entries: list_ie_entries[i - 1] for i in playlistitems
yield list_ie_entries[i - 1] if -num_entries <= i - 1 < num_entries]
elif incomplete_entries:
raise EntryNotInPlaylist() def report_download(num_entries):
self.to_screen(
'[%s] playlist %s: Downloading %d videos' %
(ie_result['extractor'], playlist, num_entries))
if isinstance(ie_entries, list): if isinstance(ie_entries, list):
n_all_entries = len(ie_entries) n_all_entries = len(ie_entries)
if playlistitems: if playlistitems:
entries = list(make_playlistitems_entries(ie_entries)) entries = make_playlistitems_entries(ie_entries)
else: else:
entries = ie_entries[playliststart:playlistend] entries = ie_entries[playliststart:playlistend]
n_entries = len(entries) n_entries = len(entries)
msg = 'Collected %d videos; downloading %d of them' % (n_all_entries, n_entries) self.to_screen(
'[%s] playlist %s: Collected %d video ids (downloading %d of them)' %
(ie_result['extractor'], playlist, n_all_entries, n_entries))
elif isinstance(ie_entries, PagedList): elif isinstance(ie_entries, PagedList):
if playlistitems: if playlistitems:
entries = [] entries = []
@@ -1242,73 +1278,25 @@ class YoutubeDL(object):
entries = ie_entries.getslice( entries = ie_entries.getslice(
playliststart, playlistend) playliststart, playlistend)
n_entries = len(entries) n_entries = len(entries)
msg = 'Downloading %d videos' % n_entries report_download(n_entries)
else: # iterable else: # iterable
if playlistitems: if playlistitems:
entries = list(make_playlistitems_entries(list(itertools.islice( entries = make_playlistitems_entries(list(itertools.islice(
ie_entries, 0, max(playlistitems))))) ie_entries, 0, max(playlistitems))))
else: else:
entries = list(itertools.islice( entries = list(itertools.islice(
ie_entries, playliststart, playlistend)) ie_entries, playliststart, playlistend))
n_entries = len(entries) n_entries = len(entries)
msg = 'Downloading %d videos' % n_entries report_download(n_entries)
if any((entry is None for entry in entries)):
raise EntryNotInPlaylist()
if not playlistitems and (playliststart or playlistend):
playlistitems = list(range(1 + playliststart, 1 + playliststart + len(entries)))
ie_result['entries'] = entries
ie_result['requested_entries'] = playlistitems
if self.params.get('allow_playlist_files', True):
ie_copy = {
'playlist': playlist,
'playlist_id': ie_result.get('id'),
'playlist_title': ie_result.get('title'),
'playlist_uploader': ie_result.get('uploader'),
'playlist_uploader_id': ie_result.get('uploader_id'),
'playlist_index': 0
}
ie_copy.update(dict(ie_result))
if self.params.get('writeinfojson', False):
infofn = self.prepare_filename(ie_copy, 'pl_infojson')
if not self._ensure_dir_exists(encodeFilename(infofn)):
return
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
self.to_screen('[info] Playlist metadata is already present')
else:
self.to_screen('[info] Writing playlist metadata as JSON to: ' + infofn)
try:
write_json_file(self.filter_requested_info(ie_result, self.params.get('clean_infojson', True)), infofn)
except (OSError, IOError):
self.report_error('Cannot write playlist metadata to JSON file ' + infofn)
if self.params.get('writedescription', False):
descfn = self.prepare_filename(ie_copy, 'pl_description')
if not self._ensure_dir_exists(encodeFilename(descfn)):
return
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
self.to_screen('[info] Playlist description is already present')
elif ie_result.get('description') is None:
self.report_warning('There\'s no playlist description to write.')
else:
try:
self.to_screen('[info] Writing playlist description to: ' + descfn)
with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
descfile.write(ie_result['description'])
except (OSError, IOError):
self.report_error('Cannot write playlist description file ' + descfn)
return
if self.params.get('playlistreverse', False): if self.params.get('playlistreverse', False):
entries = entries[::-1] entries = entries[::-1]
if self.params.get('playlistrandom', False): if self.params.get('playlistrandom', False):
random.shuffle(entries) random.shuffle(entries)
x_forwarded_for = ie_result.get('__x_forwarded_for_ip') x_forwarded_for = ie_result.get('__x_forwarded_for_ip')
self.to_screen('[%s] playlist %s: %s' % (ie_result['extractor'], playlist, msg))
for i, entry in enumerate(entries, 1): for i, entry in enumerate(entries, 1):
self.to_screen('[download] Downloading video %s of %s' % (i, n_entries)) self.to_screen('[download] Downloading video %s of %s' % (i, n_entries))
# This __x_forwarded_for_ip thing is a bit ugly but requires # This __x_forwarded_for_ip thing is a bit ugly but requires
@@ -1322,7 +1310,7 @@ class YoutubeDL(object):
'playlist_title': ie_result.get('title'), 'playlist_title': ie_result.get('title'),
'playlist_uploader': ie_result.get('uploader'), 'playlist_uploader': ie_result.get('uploader'),
'playlist_uploader_id': ie_result.get('uploader_id'), 'playlist_uploader_id': ie_result.get('uploader_id'),
'playlist_index': playlistitems[i - 1] if playlistitems else i, 'playlist_index': playlistitems[i - 1] if playlistitems else i + playliststart,
'extractor': ie_result['extractor'], 'extractor': ie_result['extractor'],
'webpage_url': ie_result['webpage_url'], 'webpage_url': ie_result['webpage_url'],
'webpage_url_basename': url_basename(ie_result['webpage_url']), 'webpage_url_basename': url_basename(ie_result['webpage_url']),
@@ -1656,7 +1644,7 @@ class YoutubeDL(object):
new_dict.update({ new_dict.update({
'width': the_only_video.get('width'), 'width': the_only_video.get('width'),
'height': the_only_video.get('height'), 'height': the_only_video.get('height'),
'resolution': the_only_video.get('resolution') or self.format_resolution(the_only_video), 'resolution': the_only_video.get('resolution'),
'fps': the_only_video.get('fps'), 'fps': the_only_video.get('fps'),
'vcodec': the_only_video.get('vcodec'), 'vcodec': the_only_video.get('vcodec'),
'vbr': the_only_video.get('vbr'), 'vbr': the_only_video.get('vbr'),
@@ -1806,18 +1794,14 @@ class YoutubeDL(object):
if 'display_id' not in info_dict and 'id' in info_dict: if 'display_id' not in info_dict and 'id' in info_dict:
info_dict['display_id'] = info_dict['id'] info_dict['display_id'] = info_dict['id']
for ts_key, date_key in ( if info_dict.get('upload_date') is None and info_dict.get('timestamp') is not None:
('timestamp', 'upload_date'), # Working around out-of-range timestamp values (e.g. negative ones on Windows,
('release_timestamp', 'release_date'), # see http://bugs.python.org/issue1646728)
): try:
if info_dict.get(date_key) is None and info_dict.get(ts_key) is not None: upload_date = datetime.datetime.utcfromtimestamp(info_dict['timestamp'])
# Working around out-of-range timestamp values (e.g. negative ones on Windows, info_dict['upload_date'] = upload_date.strftime('%Y%m%d')
# see http://bugs.python.org/issue1646728) except (ValueError, OverflowError, OSError):
try: pass
upload_date = datetime.datetime.utcfromtimestamp(info_dict[ts_key])
info_dict[date_key] = upload_date.strftime('%Y%m%d')
except (ValueError, OverflowError, OSError):
pass
# Auto generate title fields corresponding to the *_number fields when missing # Auto generate title fields corresponding to the *_number fields when missing
# in order to always have clean titles. This is very common for TV series. # in order to always have clean titles. This is very common for TV series.
@@ -2059,7 +2043,7 @@ class YoutubeDL(object):
print_mandatory('format') print_mandatory('format')
if self.params.get('forcejson', False): if self.params.get('forcejson', False):
self.post_extract(info_dict) self.post_extract(info_dict)
self.to_stdout(json.dumps(info_dict, default=repr)) self.to_stdout(json.dumps(info_dict))
def process_info(self, info_dict): def process_info(self, info_dict):
"""Process a single resolved IE result.""" """Process a single resolved IE result."""
@@ -2087,7 +2071,6 @@ class YoutubeDL(object):
info_dict = self.pre_process(info_dict) info_dict = self.pre_process(info_dict)
# info_dict['_filename'] needs to be set for backward compatibility
info_dict['_filename'] = full_filename = self.prepare_filename(info_dict, warn=True) info_dict['_filename'] = full_filename = self.prepare_filename(info_dict, warn=True)
temp_filename = self.prepare_filename(info_dict, 'temp') temp_filename = self.prepare_filename(info_dict, 'temp')
files_to_move = {} files_to_move = {}
@@ -2106,14 +2089,17 @@ class YoutubeDL(object):
if full_filename is None: if full_filename is None:
return return
if not self._ensure_dir_exists(encodeFilename(full_filename)): def ensure_dir_exists(path):
return make_dir(path, self.report_error)
if not ensure_dir_exists(encodeFilename(full_filename)):
return return
if not self._ensure_dir_exists(encodeFilename(temp_filename)): if not ensure_dir_exists(encodeFilename(temp_filename)):
return return
if self.params.get('writedescription', False): if self.params.get('writedescription', False):
descfn = self.prepare_filename(info_dict, 'description') descfn = self.prepare_filename(info_dict, 'description')
if not self._ensure_dir_exists(encodeFilename(descfn)): if not ensure_dir_exists(encodeFilename(descfn)):
return return
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)): if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
self.to_screen('[info] Video description is already present') self.to_screen('[info] Video description is already present')
@@ -2130,7 +2116,7 @@ class YoutubeDL(object):
if self.params.get('writeannotations', False): if self.params.get('writeannotations', False):
annofn = self.prepare_filename(info_dict, 'annotation') annofn = self.prepare_filename(info_dict, 'annotation')
if not self._ensure_dir_exists(encodeFilename(annofn)): if not ensure_dir_exists(encodeFilename(annofn)):
return return
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(annofn)): if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(annofn)):
self.to_screen('[info] Video annotations are already present') self.to_screen('[info] Video annotations are already present')
@@ -2153,10 +2139,7 @@ class YoutubeDL(object):
fd.add_progress_hook(ph) fd.add_progress_hook(ph)
if self.params.get('verbose'): if self.params.get('verbose'):
self.to_screen('[debug] Invoking downloader on %r' % info.get('url')) self.to_screen('[debug] Invoking downloader on %r' % info.get('url'))
new_info = dict(info) return fd.download(name, info, subtitle)
if new_info.get('http_headers') is None:
new_info['http_headers'] = self._calc_headers(new_info)
return fd.download(name, new_info, subtitle)
subtitles_are_requested = any([self.params.get('writesubtitles', False), subtitles_are_requested = any([self.params.get('writesubtitles', False),
self.params.get('writeautomaticsub')]) self.params.get('writeautomaticsub')])
@@ -2175,7 +2158,6 @@ class YoutubeDL(object):
sub_filename_final = subtitles_filename(sub_fn, sub_lang, sub_format, info_dict.get('ext')) sub_filename_final = subtitles_filename(sub_fn, sub_lang, sub_format, info_dict.get('ext'))
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(sub_filename)): if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(sub_filename)):
self.to_screen('[info] Video subtitle %s.%s is already present' % (sub_lang, sub_format)) self.to_screen('[info] Video subtitle %s.%s is already present' % (sub_lang, sub_format))
sub_info['filepath'] = sub_filename
files_to_move[sub_filename] = sub_filename_final files_to_move[sub_filename] = sub_filename_final
else: else:
self.to_screen('[info] Writing video subtitles to: ' + sub_filename) self.to_screen('[info] Writing video subtitles to: ' + sub_filename)
@@ -2185,15 +2167,13 @@ class YoutubeDL(object):
# See https://github.com/ytdl-org/youtube-dl/issues/10268 # See https://github.com/ytdl-org/youtube-dl/issues/10268
with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8', newline='') as subfile: with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8', newline='') as subfile:
subfile.write(sub_info['data']) subfile.write(sub_info['data'])
sub_info['filepath'] = sub_filename
files_to_move[sub_filename] = sub_filename_final files_to_move[sub_filename] = sub_filename_final
except (OSError, IOError): except (OSError, IOError):
self.report_error('Cannot write subtitles file ' + sub_filename) self.report_error('Cannot write subtitles file ' + sub_filename)
return return
else: else:
try: try:
dl(sub_filename, sub_info.copy(), subtitle=True) dl(sub_filename, sub_info, subtitle=True)
sub_info['filepath'] = sub_filename
files_to_move[sub_filename] = sub_filename_final files_to_move[sub_filename] = sub_filename_final
except (ExtractorError, IOError, OSError, ValueError, compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: except (ExtractorError, IOError, OSError, ValueError, compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
self.report_warning('Unable to download subtitle for "%s": %s' % self.report_warning('Unable to download subtitle for "%s": %s' %
@@ -2224,14 +2204,14 @@ class YoutubeDL(object):
if self.params.get('writeinfojson', False): if self.params.get('writeinfojson', False):
infofn = self.prepare_filename(info_dict, 'infojson') infofn = self.prepare_filename(info_dict, 'infojson')
if not self._ensure_dir_exists(encodeFilename(infofn)): if not ensure_dir_exists(encodeFilename(infofn)):
return return
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)): if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
self.to_screen('[info] Video metadata is already present') self.to_screen('[info] Video metadata is already present')
else: else:
self.to_screen('[info] Writing video metadata as JSON to: ' + infofn) self.to_screen('[info] Writing video metadata as JSON to: ' + infofn)
try: try:
write_json_file(self.filter_requested_info(info_dict, self.params.get('clean_infojson', True)), infofn) write_json_file(self.filter_requested_info(info_dict), infofn)
except (OSError, IOError): except (OSError, IOError):
self.report_error('Cannot write video metadata to JSON file ' + infofn) self.report_error('Cannot write video metadata to JSON file ' + infofn)
return return
@@ -2242,7 +2222,7 @@ class YoutubeDL(object):
for thumb_ext in self._write_thumbnails(info_dict, thumb_fn_temp): for thumb_ext in self._write_thumbnails(info_dict, thumb_fn_temp):
thumb_filename_temp = replace_extension(thumb_fn_temp, thumb_ext, info_dict.get('ext')) thumb_filename_temp = replace_extension(thumb_fn_temp, thumb_ext, info_dict.get('ext'))
thumb_filename = replace_extension(thumbfn, thumb_ext, info_dict.get('ext')) thumb_filename = replace_extension(thumbfn, thumb_ext, info_dict.get('ext'))
files_to_move[thumb_filename_temp] = thumb_filename files_to_move[thumb_filename_temp] = info_dict['__thumbnail_filename'] = thumb_filename
# Write internet shortcut files # Write internet shortcut files
url_link = webloc_link = desktop_link = False url_link = webloc_link = desktop_link = False
@@ -2355,17 +2335,10 @@ class YoutubeDL(object):
requested_formats = info_dict['requested_formats'] requested_formats = info_dict['requested_formats']
old_ext = info_dict['ext'] old_ext = info_dict['ext']
if self.params.get('merge_output_format') is None: if self.params.get('merge_output_format') is None and not compatible_formats(requested_formats):
if not compatible_formats(requested_formats): info_dict['ext'] = 'mkv'
info_dict['ext'] = 'mkv' self.report_warning(
self.report_warning( 'Requested formats are incompatible for merge and will be merged into mkv.')
'Requested formats are incompatible for merge and will be merged into mkv.')
if (info_dict['ext'] == 'webm'
and self.params.get('writethumbnail', False)
and info_dict.get('thumbnails')):
info_dict['ext'] = 'mkv'
self.report_warning(
'webm doesn\'t support embedding a thumbnail, mkv will be used.')
def correct_ext(filename): def correct_ext(filename):
filename_real_ext = os.path.splitext(filename)[1][1:] filename_real_ext = os.path.splitext(filename)[1][1:]
@@ -2387,7 +2360,7 @@ class YoutubeDL(object):
fname = prepend_extension( fname = prepend_extension(
self.prepare_filename(new_info, 'temp'), self.prepare_filename(new_info, 'temp'),
'f%s' % f['format_id'], new_info['ext']) 'f%s' % f['format_id'], new_info['ext'])
if not self._ensure_dir_exists(fname): if not ensure_dir_exists(fname):
return return
downloaded.append(fname) downloaded.append(fname)
partial_success, real_download = dl(fname, new_info) partial_success, real_download = dl(fname, new_info)
@@ -2464,8 +2437,9 @@ class YoutubeDL(object):
else: else:
assert fixup_policy in ('ignore', 'never') assert fixup_policy in ('ignore', 'never')
if ('protocol' in info_dict if (info_dict.get('protocol') == 'm3u8_native'
and get_suitable_downloader(info_dict, self.params).__name__ == 'HlsFD'): or info_dict.get('protocol') == 'm3u8'
and self.params.get('hls_prefer_native')):
if fixup_policy == 'warn': if fixup_policy == 'warn':
self.report_warning('%s: malformed AAC bitstream detected.' % ( self.report_warning('%s: malformed AAC bitstream detected.' % (
info_dict['id'])) info_dict['id']))
@@ -2481,13 +2455,13 @@ class YoutubeDL(object):
assert fixup_policy in ('ignore', 'never') assert fixup_policy in ('ignore', 'never')
try: try:
info_dict = self.post_process(dl_filename, info_dict, files_to_move) self.post_process(dl_filename, info_dict, files_to_move)
except PostProcessingError as err: except PostProcessingError as err:
self.report_error('Postprocessing: %s' % str(err)) self.report_error('Postprocessing: %s' % str(err))
return return
try: try:
for ph in self._post_hooks: for ph in self._post_hooks:
ph(info_dict['filepath']) ph(full_filename)
except Exception as err: except Exception as err:
self.report_error('post hooks: %s' % str(err)) self.report_error('post hooks: %s' % str(err))
return return
@@ -2527,7 +2501,7 @@ class YoutubeDL(object):
else: else:
if self.params.get('dump_single_json', False): if self.params.get('dump_single_json', False):
self.post_extract(res) self.post_extract(res)
self.to_stdout(json.dumps(res, default=repr)) self.to_stdout(json.dumps(res))
return self._download_retcode return self._download_retcode
@@ -2536,10 +2510,10 @@ class YoutubeDL(object):
[info_filename], mode='r', [info_filename], mode='r',
openhook=fileinput.hook_encoded('utf-8'))) as f: openhook=fileinput.hook_encoded('utf-8'))) as f:
# FileInput doesn't have a read method, we can't call json.load # FileInput doesn't have a read method, we can't call json.load
info = self.filter_requested_info(json.loads('\n'.join(f)), self.params.get('clean_infojson', True)) info = self.filter_requested_info(json.loads('\n'.join(f)))
try: try:
self.process_ie_result(info, download=True) self.process_ie_result(info, download=True)
except (DownloadError, EntryNotInPlaylist): except DownloadError:
webpage_url = info.get('webpage_url') webpage_url = info.get('webpage_url')
if webpage_url is not None: if webpage_url is not None:
self.report_warning('The info failed to download, trying with "%s"' % webpage_url) self.report_warning('The info failed to download, trying with "%s"' % webpage_url)
@@ -2549,32 +2523,21 @@ class YoutubeDL(object):
return self._download_retcode return self._download_retcode
@staticmethod @staticmethod
def filter_requested_info(info_dict, actually_filter=True): def filter_requested_info(info_dict):
if not actually_filter: fields_to_remove = ('requested_formats', 'requested_subtitles')
info_dict['epoch'] = int(time.time()) return dict(
return info_dict (k, v) for k, v in info_dict.items()
exceptions = { if (k[0] != '_' or k == '_type') and k not in fields_to_remove)
'remove': ['requested_formats', 'requested_subtitles', 'requested_entries', 'filepath', 'entries'],
'keep': ['_type'],
}
keep_key = lambda k: k in exceptions['keep'] or not (k.startswith('_') or k in exceptions['remove'])
filter_fn = lambda obj: (
list(map(filter_fn, obj)) if isinstance(obj, (list, tuple))
else obj if not isinstance(obj, dict)
else dict((k, filter_fn(v)) for k, v in obj.items() if keep_key(k)))
return filter_fn(info_dict)
def run_pp(self, pp, infodict): def run_pp(self, pp, infodict, files_to_move={}):
files_to_delete = [] files_to_delete = []
if '__files_to_move' not in infodict:
infodict['__files_to_move'] = {}
files_to_delete, infodict = pp.run(infodict) files_to_delete, infodict = pp.run(infodict)
if not files_to_delete: if not files_to_delete:
return infodict return files_to_move, infodict
if self.params.get('keepvideo', False): if self.params.get('keepvideo', False):
for f in files_to_delete: for f in files_to_delete:
infodict['__files_to_move'].setdefault(f, '') files_to_move.setdefault(f, '')
else: else:
for old_filename in set(files_to_delete): for old_filename in set(files_to_delete):
self.to_screen('Deleting original file %s (pass -k to keep)' % old_filename) self.to_screen('Deleting original file %s (pass -k to keep)' % old_filename)
@@ -2582,9 +2545,9 @@ class YoutubeDL(object):
os.remove(encodeFilename(old_filename)) os.remove(encodeFilename(old_filename))
except (IOError, OSError): except (IOError, OSError):
self.report_warning('Unable to remove downloaded original file') self.report_warning('Unable to remove downloaded original file')
if old_filename in infodict['__files_to_move']: if old_filename in files_to_move:
del infodict['__files_to_move'][old_filename] del files_to_move[old_filename]
return infodict return files_to_move, infodict
@staticmethod @staticmethod
def post_extract(info_dict): def post_extract(info_dict):
@@ -2607,22 +2570,20 @@ class YoutubeDL(object):
def pre_process(self, ie_info): def pre_process(self, ie_info):
info = dict(ie_info) info = dict(ie_info)
for pp in self._pps['beforedl']: for pp in self._pps['beforedl']:
info = self.run_pp(pp, info) info = self.run_pp(pp, info)[1]
return info return info
def post_process(self, filename, ie_info, files_to_move=None): def post_process(self, filename, ie_info, files_to_move={}):
"""Run all the postprocessors on the given file.""" """Run all the postprocessors on the given file."""
info = dict(ie_info) info = dict(ie_info)
info['filepath'] = filename info['filepath'] = filename
info['__files_to_move'] = files_to_move or {} info['__files_to_move'] = {}
for pp in ie_info.get('__postprocessors', []) + self._pps['normal']: for pp in ie_info.get('__postprocessors', []) + self._pps['normal']:
info = self.run_pp(pp, info) files_to_move, info = self.run_pp(pp, info, files_to_move)
info = self.run_pp(MoveFilesAfterDownloadPP(self), info) info = self.run_pp(MoveFilesAfterDownloadPP(self, files_to_move), info)[1]
del info['__files_to_move']
for pp in self._pps['aftermove']: for pp in self._pps['aftermove']:
info = self.run_pp(pp, info) info = self.run_pp(pp, info, {})[1]
return info
def _make_archive_id(self, info_dict): def _make_archive_id(self, info_dict):
video_id = info_dict.get('id') video_id = info_dict.get('id')
@@ -2671,11 +2632,12 @@ class YoutubeDL(object):
return 'audio only' return 'audio only'
if format.get('resolution') is not None: if format.get('resolution') is not None:
return format['resolution'] return format['resolution']
if format.get('width') and format.get('height'): if format.get('height') is not None:
res = '%dx%d' % (format['width'], format['height']) if format.get('width') is not None:
elif format.get('height'): res = '%sx%s' % (format['width'], format['height'])
res = '%sp' % format['height'] else:
elif format.get('width'): res = '%sp' % format['height']
elif format.get('width') is not None:
res = '%dx?' % format['width'] res = '%dx?' % format['width']
else: else:
res = default res = default
@@ -2989,7 +2951,7 @@ class YoutubeDL(object):
thumb_ext = determine_ext(t['url'], 'jpg') thumb_ext = determine_ext(t['url'], 'jpg')
suffix = '%s.' % t['id'] if multiple else '' suffix = '%s.' % t['id'] if multiple else ''
thumb_display_id = '%s ' % t['id'] if multiple else '' thumb_display_id = '%s ' % t['id'] if multiple else ''
t['filepath'] = thumb_filename = replace_extension(filename, suffix + thumb_ext, info_dict.get('ext')) t['filename'] = thumb_filename = replace_extension(filename, suffix + thumb_ext, info_dict.get('ext'))
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(thumb_filename)): if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(thumb_filename)):
ret.append(suffix + thumb_ext) ret.append(suffix + thumb_ext)
+5 -15
View File
@@ -180,8 +180,6 @@ def _real_main(argv=None):
if opts.overwrites: if opts.overwrites:
# --yes-overwrites implies --no-continue # --yes-overwrites implies --no-continue
opts.continue_dl = False opts.continue_dl = False
if opts.concurrent_fragment_downloads <= 0:
raise ValueError('Concurrent fragments must be positive')
def parse_retries(retries, name=''): def parse_retries(retries, name=''):
if retries in ('inf', 'infinite'): if retries in ('inf', 'infinite'):
@@ -279,14 +277,9 @@ def _real_main(argv=None):
def report_conflict(arg1, arg2): def report_conflict(arg1, arg2):
write_string('WARNING: %s is ignored since %s was given\n' % (arg2, arg1), out=sys.stderr) write_string('WARNING: %s is ignored since %s was given\n' % (arg2, arg1), out=sys.stderr)
if opts.remuxvideo and opts.recodevideo: if opts.remuxvideo and opts.recodevideo:
report_conflict('--recode-video', '--remux-video') report_conflict('--recode-video', '--remux-video')
opts.remuxvideo = False opts.remuxvideo = False
if opts.sponskrub_cut and opts.split_chapters and opts.sponskrub is not False:
report_conflict('--split-chapter', '--sponskrub-cut')
opts.sponskrub_cut = False
if opts.allow_unplayable_formats: if opts.allow_unplayable_formats:
if opts.extractaudio: if opts.extractaudio:
report_conflict('--allow-unplayable-formats', '--extract-audio') report_conflict('--allow-unplayable-formats', '--extract-audio')
@@ -376,7 +369,11 @@ def _real_main(argv=None):
}) })
if not already_have_thumbnail: if not already_have_thumbnail:
opts.writethumbnail = True opts.writethumbnail = True
# This should be below most ffmpeg PP because it may cut parts out from the video # XAttrMetadataPP should be run after post-processors that may change file
# contents
if opts.xattrs:
postprocessors.append({'key': 'XAttrMetadata'})
# This should be below all ffmpeg PP because it may cut parts out from the video
# If opts.sponskrub is None, sponskrub is used, but it silently fails if the executable can't be found # If opts.sponskrub is None, sponskrub is used, but it silently fails if the executable can't be found
if opts.sponskrub is not False: if opts.sponskrub is not False:
postprocessors.append({ postprocessors.append({
@@ -387,11 +384,6 @@ def _real_main(argv=None):
'force': opts.sponskrub_force, 'force': opts.sponskrub_force,
'ignoreerror': opts.sponskrub is None, 'ignoreerror': opts.sponskrub is None,
}) })
if opts.split_chapters:
postprocessors.append({'key': 'FFmpegSplitChapters'})
# XAttrMetadataPP should be run after post-processors that may change file contents
if opts.xattrs:
postprocessors.append({'key': 'XAttrMetadata'})
# ExecAfterDownload must be the last PP # ExecAfterDownload must be the last PP
if opts.exec_cmd: if opts.exec_cmd:
postprocessors.append({ postprocessors.append({
@@ -471,7 +463,6 @@ def _real_main(argv=None):
'extractor_retries': opts.extractor_retries, 'extractor_retries': opts.extractor_retries,
'skip_unavailable_fragments': opts.skip_unavailable_fragments, 'skip_unavailable_fragments': opts.skip_unavailable_fragments,
'keep_fragments': opts.keep_fragments, 'keep_fragments': opts.keep_fragments,
'concurrent_fragment_downloads': opts.concurrent_fragment_downloads,
'buffersize': opts.buffersize, 'buffersize': opts.buffersize,
'noresizebuffer': opts.noresizebuffer, 'noresizebuffer': opts.noresizebuffer,
'http_chunk_size': opts.http_chunk_size, 'http_chunk_size': opts.http_chunk_size,
@@ -491,7 +482,6 @@ def _real_main(argv=None):
'writeannotations': opts.writeannotations, 'writeannotations': opts.writeannotations,
'writeinfojson': opts.writeinfojson, 'writeinfojson': opts.writeinfojson,
'allow_playlist_files': opts.allow_playlist_files, 'allow_playlist_files': opts.allow_playlist_files,
'clean_infojson': opts.clean_infojson,
'getcomments': opts.getcomments, 'getcomments': opts.getcomments,
'writethumbnail': opts.writethumbnail, 'writethumbnail': opts.writethumbnail,
'write_all_thumbnails': opts.write_all_thumbnails, 'write_all_thumbnails': opts.write_all_thumbnails,
-6
View File
@@ -326,12 +326,6 @@ class FileDownloader(object):
"""Report it was impossible to resume download.""" """Report it was impossible to resume download."""
self.to_screen('[download] Unable to resume') self.to_screen('[download] Unable to resume')
@staticmethod
def supports_manifest(manifest):
""" Whether the downloader can download the fragments from the manifest.
Redefine in subclasses if needed. """
pass
def download(self, filename, info_dict, subtitle=False): def download(self, filename, info_dict, subtitle=False):
"""Download to a filename using the info from info_dict """Download to a filename using the info from info_dict
Return True on success and False otherwise Return True on success and False otherwise
+42 -117
View File
@@ -1,26 +1,18 @@
from __future__ import unicode_literals from __future__ import unicode_literals
try:
import concurrent.futures
can_threaded_download = True
except ImportError:
can_threaded_download = False
from ..downloader import _get_real_downloader from ..downloader import _get_real_downloader
from .fragment import FragmentFD from .fragment import FragmentFD
from ..compat import compat_urllib_error from ..compat import compat_urllib_error
from ..utils import ( from ..utils import (
DownloadError, DownloadError,
sanitize_open,
urljoin, urljoin,
) )
class DashSegmentsFD(FragmentFD): class DashSegmentsFD(FragmentFD):
""" """
Download segments in a DASH manifest. External downloaders can take over Download segments in a DASH manifest
the fragment downloads by supporting the 'frag_urls' protocol
""" """
FD_NAME = 'dashsegments' FD_NAME = 'dashsegments'
@@ -45,7 +37,7 @@ class DashSegmentsFD(FragmentFD):
fragment_retries = self.params.get('fragment_retries', 0) fragment_retries = self.params.get('fragment_retries', 0)
skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True) skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True)
fragments_to_download = [] fragment_urls = []
frag_index = 0 frag_index = 0
for i, fragment in enumerate(fragments): for i, fragment in enumerate(fragments):
frag_index += 1 frag_index += 1
@@ -56,17 +48,49 @@ class DashSegmentsFD(FragmentFD):
assert fragment_base_url assert fragment_base_url
fragment_url = urljoin(fragment_base_url, fragment['path']) fragment_url = urljoin(fragment_base_url, fragment['path'])
fragments_to_download.append({ if real_downloader:
'frag_index': frag_index, fragment_urls.append(fragment_url)
'index': i, continue
'url': fragment_url,
}) # In DASH, the first segment contains necessary headers to
# generate a valid MP4 file, so always abort for the first segment
fatal = i == 0 or not skip_unavailable_fragments
count = 0
while count <= fragment_retries:
try:
success, frag_content = self._download_fragment(ctx, fragment_url, info_dict)
if not success:
return False
self._append_fragment(ctx, frag_content)
break
except compat_urllib_error.HTTPError as err:
# YouTube may often return 404 HTTP error for a fragment causing the
# whole download to fail. However if the same fragment is immediately
# retried with the same request data this usually succeeds (1-2 attempts
# is usually enough) thus allowing to download the whole file successfully.
# To be future-proof we will retry all fragments that fail with any
# HTTP error.
count += 1
if count <= fragment_retries:
self.report_retry_fragment(err, frag_index, count, fragment_retries)
except DownloadError:
# Don't retry fragment if error occurred during HTTP downloading
# itself since it has own retry settings
if not fatal:
self.report_skip_fragment(frag_index)
break
raise
if count > fragment_retries:
if not fatal:
self.report_skip_fragment(frag_index)
continue
self.report_error('giving up after %s fragment retries' % fragment_retries)
return False
if real_downloader: if real_downloader:
self.to_screen(
'[%s] Fragment downloads will be delegated to %s' % (self.FD_NAME, real_downloader.get_basename()))
info_copy = info_dict.copy() info_copy = info_dict.copy()
info_copy['fragments'] = fragments_to_download info_copy['url_list'] = fragment_urls
fd = real_downloader(self.ydl, self.params) fd = real_downloader(self.ydl, self.params)
# TODO: Make progress updates work without hooking twice # TODO: Make progress updates work without hooking twice
# for ph in self._progress_hooks: # for ph in self._progress_hooks:
@@ -75,104 +99,5 @@ class DashSegmentsFD(FragmentFD):
if not success: if not success:
return False return False
else: else:
def download_fragment(fragment):
i = fragment['index']
frag_index = fragment['frag_index']
fragment_url = fragment['url']
ctx['fragment_index'] = frag_index
# In DASH, the first segment contains necessary headers to
# generate a valid MP4 file, so always abort for the first segment
fatal = i == 0 or not skip_unavailable_fragments
count = 0
while count <= fragment_retries:
try:
success, frag_content = self._download_fragment(ctx, fragment_url, info_dict)
if not success:
return False, frag_index
break
except compat_urllib_error.HTTPError as err:
# YouTube may often return 404 HTTP error for a fragment causing the
# whole download to fail. However if the same fragment is immediately
# retried with the same request data this usually succeeds (1-2 attempts
# is usually enough) thus allowing to download the whole file successfully.
# To be future-proof we will retry all fragments that fail with any
# HTTP error.
count += 1
if count <= fragment_retries:
self.report_retry_fragment(err, frag_index, count, fragment_retries)
except DownloadError:
# Don't retry fragment if error occurred during HTTP downloading
# itself since it has own retry settings
if not fatal:
break
raise
if count > fragment_retries:
if not fatal:
return False, frag_index
self.report_error('Giving up after %s fragment retries' % fragment_retries)
return False, frag_index
return frag_content, frag_index
def append_fragment(frag_content, frag_index):
if frag_content:
fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], frag_index)
try:
file, frag_sanitized = sanitize_open(fragment_filename, 'rb')
ctx['fragment_filename_sanitized'] = frag_sanitized
file.close()
self._append_fragment(ctx, frag_content)
return True
except FileNotFoundError:
if skip_unavailable_fragments:
self.report_skip_fragment(frag_index)
return True
else:
self.report_error(
'fragment %s not found, unable to continue' % frag_index)
return False
else:
if skip_unavailable_fragments:
self.report_skip_fragment(frag_index)
return True
else:
self.report_error(
'fragment %s not found, unable to continue' % frag_index)
return False
max_workers = self.params.get('concurrent_fragment_downloads', 1)
if can_threaded_download and max_workers > 1:
self.report_warning('The download speed shown is only of one thread. This is a known issue')
with concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
futures = [pool.submit(download_fragment, fragment) for fragment in fragments_to_download]
# timeout must be 0 to return instantly
done, not_done = concurrent.futures.wait(futures, timeout=0)
try:
while not_done:
# Check every 1 second for KeyboardInterrupt
freshly_done, not_done = concurrent.futures.wait(not_done, timeout=1)
done |= freshly_done
except KeyboardInterrupt:
for future in not_done:
future.cancel()
# timeout must be none to cancel
concurrent.futures.wait(not_done, timeout=None)
raise KeyboardInterrupt
results = [future.result() for future in futures]
for frag_content, frag_index in results:
result = append_fragment(frag_content, frag_index)
if not result:
return False
else:
for fragment in fragments_to_download:
frag_content, frag_index = download_fragment(fragment)
result = append_fragment(frag_content, frag_index)
if not result:
return False
self._finish_frag_download(ctx) self._finish_frag_download(ctx)
return True return True
+48 -77
View File
@@ -24,6 +24,7 @@ from ..utils import (
cli_bool_option, cli_bool_option,
cli_configuration_args, cli_configuration_args,
encodeFilename, encodeFilename,
error_to_compat_str,
encodeArgument, encodeArgument,
handle_youtubedl_headers, handle_youtubedl_headers,
check_executable, check_executable,
@@ -107,8 +108,7 @@ class ExternalFD(FileDownloader):
def _configuration_args(self, *args, **kwargs): def _configuration_args(self, *args, **kwargs):
return cli_configuration_args( return cli_configuration_args(
self.params.get('external_downloader_args'), self.params.get('external_downloader_args'),
[self.get_basename(), 'default'], self.get_basename(), *args, **kwargs)
*args, **kwargs)
def _call_downloader(self, tmpfilename, info_dict): def _call_downloader(self, tmpfilename, info_dict):
""" Either overwrite this or implement _make_cmd """ """ Either overwrite this or implement _make_cmd """
@@ -116,43 +116,24 @@ class ExternalFD(FileDownloader):
self._debug_cmd(cmd) self._debug_cmd(cmd)
if 'fragments' in info_dict: p = subprocess.Popen(
fragment_retries = self.params.get('fragment_retries', 0) cmd, stderr=subprocess.PIPE)
skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True) _, stderr = process_communicate_or_kill(p)
if p.returncode != 0:
count = 0 self.to_stderr(stderr.decode('utf-8', 'replace'))
while count <= fragment_retries:
p = subprocess.Popen(
cmd, stderr=subprocess.PIPE)
_, stderr = process_communicate_or_kill(p)
if p.returncode == 0:
break
# TODO: Decide whether to retry based on error code
# https://aria2.github.io/manual/en/html/aria2c.html#exit-status
self.to_stderr(stderr.decode('utf-8', 'replace'))
count += 1
if count <= fragment_retries:
self.to_screen(
'[%s] Got error. Retrying fragments (attempt %d of %s)...'
% (self.get_basename(), count, self.format_retries(fragment_retries)))
if count > fragment_retries:
if not skip_unavailable_fragments:
self.report_error('Giving up after %s fragment retries' % fragment_retries)
return -1
if 'url_list' in info_dict:
file_list = []
for [i, url] in enumerate(info_dict['url_list']):
tmpsegmentname = '%s_%s.frag' % (tmpfilename, i)
file_list.append(tmpsegmentname)
key_list = info_dict.get('key_list')
decrypt_info = None
dest, _ = sanitize_open(tmpfilename, 'wb') dest, _ = sanitize_open(tmpfilename, 'wb')
for frag_index, fragment in enumerate(info_dict['fragments']): for i, file in enumerate(file_list):
fragment_filename = '%s-Frag%d' % (tmpfilename, frag_index) src, _ = sanitize_open(file, 'rb')
try: if key_list:
src, _ = sanitize_open(fragment_filename, 'rb') decrypt_info = next((x for x in key_list if x['INDEX'] == i), decrypt_info)
except IOError:
if skip_unavailable_fragments and frag_index > 1:
self.to_screen('[%s] Skipping fragment %d ...' % (self.get_basename(), frag_index))
continue
self.report_error('Unable to open fragment %d' % frag_index)
return -1
decrypt_info = fragment.get('decrypt_info')
if decrypt_info:
if decrypt_info['METHOD'] == 'AES-128': if decrypt_info['METHOD'] == 'AES-128':
iv = decrypt_info.get('IV') iv = decrypt_info.get('IV')
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen( decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
@@ -168,16 +149,19 @@ class ExternalFD(FileDownloader):
fragment_data = src.read() fragment_data = src.read()
dest.write(fragment_data) dest.write(fragment_data)
src.close() src.close()
if not self.params.get('keep_fragments', False):
os.remove(encodeFilename(fragment_filename))
dest.close() dest.close()
os.remove(encodeFilename('%s.frag.urls' % tmpfilename)) if not self.params.get('keep_fragments', False):
else: for file_path in file_list:
p = subprocess.Popen( try:
cmd, stderr=subprocess.PIPE) os.remove(file_path)
_, stderr = process_communicate_or_kill(p) except OSError as ose:
if p.returncode != 0: self.report_error("Unable to delete file %s; %s" % (file_path, error_to_compat_str(ose)))
self.to_stderr(stderr.decode('utf-8', 'replace')) try:
file_path = '%s.frag.urls' % tmpfilename
os.remove(file_path)
except OSError as ose:
self.report_error("Unable to delete file %s; %s" % (file_path, error_to_compat_str(ose)))
return p.returncode return p.returncode
def _prepare_url(self, info_dict, url): def _prepare_url(self, info_dict, url):
@@ -261,22 +245,15 @@ class Aria2cFD(ExternalFD):
AVAILABLE_OPT = '-v' AVAILABLE_OPT = '-v'
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'frag_urls') SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'frag_urls')
@staticmethod
def supports_manifest(manifest):
UNSUPPORTED_FEATURES = [
r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [1]
# 1. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.2
]
check_results = (not re.search(feature, manifest) for feature in UNSUPPORTED_FEATURES)
return all(check_results)
def _make_cmd(self, tmpfilename, info_dict): def _make_cmd(self, tmpfilename, info_dict):
cmd = [self.exe, '-c', cmd = [self.exe, '-c']
'--console-log-level=warn', '--summary-interval=0', '--download-result=hide', dn = os.path.dirname(tmpfilename)
'--file-allocation=none', '-x16', '-j16', '-s16'] if 'url_list' not in info_dict:
if 'fragments' in info_dict: cmd += ['--out', os.path.basename(tmpfilename)]
cmd += ['--allow-overwrite=true', '--allow-piece-length-change=true'] verbose_level_args = ['--console-log-level=warn', '--summary-interval=0']
cmd += self._configuration_args(['--file-allocation=none', '-x16', '-j16', '-s16'] + verbose_level_args)
if dn:
cmd += ['--dir', dn]
if info_dict.get('http_headers') is not None: if info_dict.get('http_headers') is not None:
for key, val in info_dict['http_headers'].items(): for key, val in info_dict['http_headers'].items():
cmd += ['--header', '%s: %s' % (key, val)] cmd += ['--header', '%s: %s' % (key, val)]
@@ -284,25 +261,19 @@ 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._configuration_args()
dn = os.path.dirname(tmpfilename)
if dn:
cmd += ['--dir', dn]
if 'fragments' not in info_dict:
cmd += ['--out', os.path.basename(tmpfilename)]
cmd += ['--auto-file-renaming=false'] cmd += ['--auto-file-renaming=false']
if 'url_list' in info_dict:
if 'fragments' in info_dict: cmd += verbose_level_args
cmd += ['--file-allocation=none', '--uri-selector=inorder'] cmd += ['--uri-selector', 'inorder', '--download-result=hide']
url_list_file = '%s.frag.urls' % tmpfilename url_list_file = '%s.frag.urls' % tmpfilename
url_list = [] url_list = []
for frag_index, fragment in enumerate(info_dict['fragments']): for [i, url] in enumerate(info_dict['url_list']):
fragment_filename = '%s-Frag%d' % (os.path.basename(tmpfilename), frag_index) tmpsegmentname = '%s_%s.frag' % (os.path.basename(tmpfilename), i)
url_list.append('%s\n\tout=%s' % (fragment['url'], fragment_filename)) url_list.append('%s\n\tout=%s' % (url, tmpsegmentname))
stream, _ = sanitize_open(url_list_file, 'wb') stream, _ = sanitize_open(url_list_file, 'wb')
stream.write('\n'.join(url_list).encode('utf-8')) stream.write('\n'.join(url_list).encode('utf-8'))
stream.close() stream.close()
cmd += ['-i', url_list_file] cmd += ['-i', url_list_file]
else: else:
cmd += ['--', info_dict['url']] cmd += ['--', info_dict['url']]
@@ -311,8 +282,8 @@ class Aria2cFD(ExternalFD):
class HttpieFD(ExternalFD): class HttpieFD(ExternalFD):
@classmethod @classmethod
def available(cls, path=None): def available(cls):
return check_executable(path or 'http', ['--version']) return check_executable('http', ['--version'])
def _make_cmd(self, tmpfilename, info_dict): def _make_cmd(self, tmpfilename, info_dict):
cmd = ['http', '--download', '--output', tmpfilename, info_dict['url']] cmd = ['http', '--download', '--output', tmpfilename, info_dict['url']]
@@ -327,7 +298,7 @@ class FFmpegFD(ExternalFD):
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'm3u8', 'rtsp', 'rtmp', 'mms') SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'm3u8', 'rtsp', 'rtmp', 'mms')
@classmethod @classmethod
def available(cls, path=None): # path is ignored for ffmpeg def available(cls):
return FFmpegPostProcessor().available return FFmpegPostProcessor().available
def _call_downloader(self, tmpfilename, info_dict): def _call_downloader(self, tmpfilename, info_dict):
+75 -154
View File
@@ -7,11 +7,6 @@ try:
can_decrypt_frag = True can_decrypt_frag = True
except ImportError: except ImportError:
can_decrypt_frag = False can_decrypt_frag = False
try:
import concurrent.futures
can_threaded_download = True
except ImportError:
can_threaded_download = False
from ..downloader import _get_real_downloader from ..downloader import _get_real_downloader
from .fragment import FragmentFD from .fragment import FragmentFD
@@ -24,17 +19,12 @@ from ..compat import (
) )
from ..utils import ( from ..utils import (
parse_m3u8_attributes, parse_m3u8_attributes,
sanitize_open,
update_url_query, update_url_query,
) )
class HlsFD(FragmentFD): class HlsFD(FragmentFD):
""" """ A limited implementation that does not require ffmpeg """
Download segments in a m3u8 manifest. External downloaders can take over
the fragment downloads by supporting the 'frag_urls' protocol and
re-defining 'supports_manifest' function
"""
FD_NAME = 'hlsnative' FD_NAME = 'hlsnative'
@@ -63,15 +53,12 @@ class HlsFD(FragmentFD):
UNSUPPORTED_FEATURES += [ UNSUPPORTED_FEATURES += [
r'#EXT-X-KEY:METHOD=(?!NONE|AES-128)', # encrypted streams [1] r'#EXT-X-KEY:METHOD=(?!NONE|AES-128)', # encrypted streams [1]
] ]
check_results = [not re.search(feature, manifest) for feature in UNSUPPORTED_FEATURES]
def check_results(): is_aes128_enc = '#EXT-X-KEY:METHOD=AES-128' in manifest
yield not info_dict.get('is_live') check_results.append(with_crypto or not is_aes128_enc)
is_aes128_enc = '#EXT-X-KEY:METHOD=AES-128' in manifest check_results.append(not (is_aes128_enc and r'#EXT-X-BYTERANGE' in manifest))
yield with_crypto or not is_aes128_enc check_results.append(not info_dict.get('is_live'))
yield not (is_aes128_enc and r'#EXT-X-BYTERANGE' in manifest) return all(check_results)
for feature in UNSUPPORTED_FEATURES:
yield not re.search(feature, manifest)
return all(check_results())
def real_download(self, filename, info_dict): def real_download(self, filename, info_dict):
man_url = info_dict['url'] man_url = info_dict['url']
@@ -83,24 +70,20 @@ class HlsFD(FragmentFD):
if not self.can_download(s, info_dict, self.params.get('allow_unplayable_formats')): if not self.can_download(s, info_dict, self.params.get('allow_unplayable_formats')):
if info_dict.get('extra_param_to_segment_url') or info_dict.get('_decryption_key_url'): if info_dict.get('extra_param_to_segment_url') or info_dict.get('_decryption_key_url'):
self.report_error('pycryptodome not found. Please install') self.report_error('pycryptodome not found. Please install it.')
return False return False
if self.can_download(s, info_dict, with_crypto=True): if self.can_download(s, info_dict, with_crypto=True):
self.report_warning('pycryptodome is needed to download this file natively') self.report_warning('pycryptodome is needed to download this file with hlsnative')
fd = FFmpegFD(self.ydl, self.params)
self.report_warning( self.report_warning(
'%s detected unsupported features; extraction will be delegated to %s' % (self.FD_NAME, fd.get_basename())) 'hlsnative has detected features it does not support, '
'extraction will be delegated to ffmpeg')
fd = FFmpegFD(self.ydl, self.params)
# TODO: Make progress updates work without hooking twice # TODO: Make progress updates work without hooking twice
# for ph in self._progress_hooks: # for ph in self._progress_hooks:
# fd.add_progress_hook(ph) # fd.add_progress_hook(ph)
return fd.real_download(filename, info_dict) return fd.real_download(filename, info_dict)
real_downloader = _get_real_downloader(info_dict, 'frag_urls', self.params, None) real_downloader = _get_real_downloader(info_dict, 'frag_urls', self.params, None)
if real_downloader and not real_downloader.supports_manifest(s):
real_downloader = None
if real_downloader:
self.to_screen(
'[%s] Fragment downloads will be delegated to %s' % (self.FD_NAME, real_downloader.get_basename()))
def is_ad_fragment_start(s): def is_ad_fragment_start(s):
return (s.startswith('#ANVATO-SEGMENT-INFO') and 'type=ad' in s return (s.startswith('#ANVATO-SEGMENT-INFO') and 'type=ad' in s
@@ -110,7 +93,7 @@ class HlsFD(FragmentFD):
return (s.startswith('#ANVATO-SEGMENT-INFO') and 'type=master' in s return (s.startswith('#ANVATO-SEGMENT-INFO') and 'type=master' in s
or s.startswith('#UPLYNK-SEGMENT') and s.endswith(',segment')) or s.startswith('#UPLYNK-SEGMENT') and s.endswith(',segment'))
fragments = [] fragment_urls = []
media_frags = 0 media_frags = 0
ad_frags = 0 ad_frags = 0
@@ -153,12 +136,14 @@ class HlsFD(FragmentFD):
i = 0 i = 0
media_sequence = 0 media_sequence = 0
decrypt_info = {'METHOD': 'NONE'} decrypt_info = {'METHOD': 'NONE'}
key_list = []
byte_range = {} byte_range = {}
discontinuity_count = 0 discontinuity_count = 0
frag_index = 0 frag_index = 0
ad_frag_next = False ad_frag_next = False
for line in s.splitlines(): for line in s.splitlines():
line = line.strip() line = line.strip()
download_frag = False
if line: if line:
if not line.startswith('#'): if not line.startswith('#'):
if format_index and discontinuity_count != format_index: if format_index and discontinuity_count != format_index:
@@ -175,20 +160,17 @@ class HlsFD(FragmentFD):
if extra_query: if extra_query:
frag_url = update_url_query(frag_url, extra_query) frag_url = update_url_query(frag_url, extra_query)
fragments.append({ if real_downloader:
'frag_index': frag_index, fragment_urls.append(frag_url)
'url': frag_url, continue
'decrypt_info': decrypt_info, download_frag = True
'byte_range': byte_range,
'media_sequence': media_sequence,
})
elif line.startswith('#EXT-X-MAP'): elif line.startswith('#EXT-X-MAP'):
if format_index and discontinuity_count != format_index: if format_index and discontinuity_count != format_index:
continue continue
if frag_index > 0: if frag_index > 0:
self.report_error( self.report_error(
'Initialization fragment found after media fragments, unable to download') 'initialization fragment found after media fragments, unable to download')
return False return False
frag_index += 1 frag_index += 1
map_info = parse_m3u8_attributes(line[11:]) map_info = parse_m3u8_attributes(line[11:])
@@ -198,14 +180,9 @@ class HlsFD(FragmentFD):
else compat_urlparse.urljoin(man_url, map_info.get('URI'))) else compat_urlparse.urljoin(man_url, map_info.get('URI')))
if extra_query: if extra_query:
frag_url = update_url_query(frag_url, extra_query) frag_url = update_url_query(frag_url, extra_query)
if real_downloader:
fragments.append({ fragment_urls.append(frag_url)
'frag_index': frag_index, continue
'url': frag_url,
'decrypt_info': decrypt_info,
'byte_range': byte_range,
'media_sequence': media_sequence
})
if map_info.get('BYTERANGE'): if map_info.get('BYTERANGE'):
splitted_byte_range = map_info.get('BYTERANGE').split('@') splitted_byte_range = map_info.get('BYTERANGE').split('@')
@@ -214,6 +191,7 @@ class HlsFD(FragmentFD):
'start': sub_range_start, 'start': sub_range_start,
'end': sub_range_start + int(splitted_byte_range[0]), 'end': sub_range_start + int(splitted_byte_range[0]),
} }
download_frag = True
elif line.startswith('#EXT-X-KEY'): elif line.startswith('#EXT-X-KEY'):
decrypt_url = decrypt_info.get('URI') decrypt_url = decrypt_info.get('URI')
@@ -228,6 +206,9 @@ class HlsFD(FragmentFD):
decrypt_info['URI'] = update_url_query(decrypt_info['URI'], extra_query) decrypt_info['URI'] = update_url_query(decrypt_info['URI'], extra_query)
if decrypt_url != decrypt_info['URI']: if decrypt_url != decrypt_info['URI']:
decrypt_info['KEY'] = None decrypt_info['KEY'] = None
key_data = decrypt_info.copy()
key_data['INDEX'] = frag_index
key_list.append(key_data)
elif line.startswith('#EXT-X-MEDIA-SEQUENCE'): elif line.startswith('#EXT-X-MEDIA-SEQUENCE'):
media_sequence = int(line[22:]) media_sequence = int(line[22:])
@@ -244,16 +225,58 @@ class HlsFD(FragmentFD):
ad_frag_next = False ad_frag_next = False
elif line.startswith('#EXT-X-DISCONTINUITY'): elif line.startswith('#EXT-X-DISCONTINUITY'):
discontinuity_count += 1 discontinuity_count += 1
i += 1
media_sequence += 1
# We only download the first fragment during the test if download_frag:
if test: count = 0
fragments = [fragments[0] if fragments else None] headers = info_dict.get('http_headers', {})
if byte_range:
headers['Range'] = 'bytes=%d-%d' % (byte_range['start'], byte_range['end'] - 1)
while count <= fragment_retries:
try:
success, frag_content = self._download_fragment(
ctx, frag_url, info_dict, headers)
if not success:
return False
break
except compat_urllib_error.HTTPError as err:
# Unavailable (possibly temporary) fragments may be served.
# First we try to retry then either skip or abort.
# See https://github.com/ytdl-org/youtube-dl/issues/10165,
# https://github.com/ytdl-org/youtube-dl/issues/10448).
count += 1
if count <= fragment_retries:
self.report_retry_fragment(err, frag_index, count, fragment_retries)
if count > fragment_retries:
if skip_unavailable_fragments:
i += 1
media_sequence += 1
self.report_skip_fragment(frag_index)
continue
self.report_error(
'giving up after %s fragment retries' % fragment_retries)
return False
if decrypt_info['METHOD'] == 'AES-128':
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', media_sequence)
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
self._prepare_url(info_dict, info_dict.get('_decryption_key_url') or decrypt_info['URI'])).read()
# Don't decrypt the content in tests since the data is explicitly truncated and it's not to a valid block
# size (see https://github.com/ytdl-org/youtube-dl/pull/27660). Tests only care that the correct data downloaded,
# not what it decrypts to.
if not test:
frag_content = AES.new(
decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(frag_content)
self._append_fragment(ctx, frag_content)
# We only download the first fragment during the test
if test:
break
i += 1
media_sequence += 1
if real_downloader: if real_downloader:
info_copy = info_dict.copy() info_copy = info_dict.copy()
info_copy['fragments'] = fragments info_copy['url_list'] = fragment_urls
info_copy['key_list'] = key_list
fd = real_downloader(self.ydl, self.params) fd = real_downloader(self.ydl, self.params)
# TODO: Make progress updates work without hooking twice # TODO: Make progress updates work without hooking twice
# for ph in self._progress_hooks: # for ph in self._progress_hooks:
@@ -262,107 +285,5 @@ class HlsFD(FragmentFD):
if not success: if not success:
return False return False
else: else:
def download_fragment(fragment):
frag_index = fragment['frag_index']
frag_url = fragment['url']
decrypt_info = fragment['decrypt_info']
byte_range = fragment['byte_range']
media_sequence = fragment['media_sequence']
ctx['fragment_index'] = frag_index
count = 0
headers = info_dict.get('http_headers', {})
if byte_range:
headers['Range'] = 'bytes=%d-%d' % (byte_range['start'], byte_range['end'] - 1)
while count <= fragment_retries:
try:
success, frag_content = self._download_fragment(
ctx, frag_url, info_dict, headers)
if not success:
return False, frag_index
break
except compat_urllib_error.HTTPError as err:
# Unavailable (possibly temporary) fragments may be served.
# First we try to retry then either skip or abort.
# See https://github.com/ytdl-org/youtube-dl/issues/10165,
# https://github.com/ytdl-org/youtube-dl/issues/10448).
count += 1
if count <= fragment_retries:
self.report_retry_fragment(err, frag_index, count, fragment_retries)
if count > fragment_retries:
self.report_error('Giving up after %s fragment retries' % fragment_retries)
return False, frag_index
if decrypt_info['METHOD'] == 'AES-128':
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', media_sequence)
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
self._prepare_url(info_dict, info_dict.get('_decryption_key_url') or decrypt_info['URI'])).read()
# Don't decrypt the content in tests since the data is explicitly truncated and it's not to a valid block
# size (see https://github.com/ytdl-org/youtube-dl/pull/27660). Tests only care that the correct data downloaded,
# not what it decrypts to.
if not test:
frag_content = AES.new(
decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(frag_content)
return frag_content, frag_index
def append_fragment(frag_content, frag_index):
if frag_content:
fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], frag_index)
try:
file, frag_sanitized = sanitize_open(fragment_filename, 'rb')
ctx['fragment_filename_sanitized'] = frag_sanitized
file.close()
self._append_fragment(ctx, frag_content)
return True
except FileNotFoundError:
if skip_unavailable_fragments:
self.report_skip_fragment(frag_index)
return True
else:
self.report_error(
'fragment %s not found, unable to continue' % frag_index)
return False
else:
if skip_unavailable_fragments:
self.report_skip_fragment(frag_index)
return True
else:
self.report_error(
'fragment %s not found, unable to continue' % frag_index)
return False
max_workers = self.params.get('concurrent_fragment_downloads', 1)
if can_threaded_download and max_workers > 1:
self.report_warning('The download speed shown is only of one thread. This is a known issue')
with concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
futures = [pool.submit(download_fragment, fragment) for fragment in fragments]
# timeout must be 0 to return instantly
done, not_done = concurrent.futures.wait(futures, timeout=0)
try:
while not_done:
# Check every 1 second for KeyboardInterrupt
freshly_done, not_done = concurrent.futures.wait(not_done, timeout=1)
done |= freshly_done
except KeyboardInterrupt:
for future in not_done:
future.cancel()
# timeout must be none to cancel
concurrent.futures.wait(not_done, timeout=None)
raise KeyboardInterrupt
results = [future.result() for future in futures]
for frag_content, frag_index in results:
result = append_fragment(frag_content, frag_index)
if not result:
return False
else:
for fragment in fragments:
frag_content, frag_index = download_fragment(fragment)
result = append_fragment(frag_content, frag_index)
if not result:
return False
self._finish_frag_download(ctx) self._finish_frag_download(ctx)
return True return True
+1 -1
View File
@@ -117,7 +117,7 @@ class RtmpFD(FileDownloader):
# Check for rtmpdump first # Check for rtmpdump first
if not check_executable('rtmpdump', ['-h']): if not check_executable('rtmpdump', ['-h']):
self.report_error('RTMP download detected but "rtmpdump" could not be run. Please install') self.report_error('RTMP download detected but "rtmpdump" could not be run. Please install it.')
return False return False
# Download using rtmpdump. rtmpdump returns exit code 2 when # Download using rtmpdump. rtmpdump returns exit code 2 when
+1 -1
View File
@@ -24,7 +24,7 @@ class RtspFD(FileDownloader):
args = [ args = [
'mpv', '-really-quiet', '--vo=null', '--stream-dump=' + tmpfilename, url] 'mpv', '-really-quiet', '--vo=null', '--stream-dump=' + tmpfilename, url]
else: else:
self.report_error('MMS or RTSP download detected but neither "mplayer" nor "mpv" could be run. Please install one') self.report_error('MMS or RTSP download detected but neither "mplayer" nor "mpv" could be run. Please install any.')
return False return False
self._debug_cmd(args) self._debug_cmd(args)
+2 -1
View File
@@ -79,7 +79,8 @@ class YoutubeLiveChatReplayFD(FragmentFD):
self._prepare_and_start_frag_download(ctx) self._prepare_and_start_frag_download(ctx)
success, raw_fragment = dl_fragment(info_dict['url']) success, raw_fragment = dl_fragment(
'https://www.youtube.com/watch?v={}'.format(video_id))
if not success: if not success:
return False return False
try: try:
+18 -49
View File
@@ -65,35 +65,15 @@ class AMCNetworksIE(ThePlatformIE):
def _real_extract(self, url): def _real_extract(self, url):
site, display_id = re.match(self._VALID_URL, url).groups() site, display_id = re.match(self._VALID_URL, url).groups()
requestor_id = self._REQUESTOR_ID_MAP[site] requestor_id = self._REQUESTOR_ID_MAP[site]
page_data = self._download_json( properties = self._download_json(
'https://content-delivery-gw.svc.ds.amcn.com/api/v2/content/amcn/%s/url/%s' 'https://content-delivery-gw.svc.ds.amcn.com/api/v2/content/amcn/%s/url/%s' % (requestor_id.lower(), display_id),
% (requestor_id.lower(), display_id), display_id)['data'] display_id)['data']['properties']
properties = page_data.get('properties') or {}
query = { query = {
'mbr': 'true', 'mbr': 'true',
'manifest': 'm3u', 'manifest': 'm3u',
} }
tp_path = 'M_UwQC/media/' + properties['videoPid']
video_player_count = 0 media_url = 'https://link.theplatform.com/s/' + tp_path
try:
for v in page_data['children']:
if v.get('type') == 'video-player':
releasePid = v['properties']['currentVideo']['meta']['releasePid']
tp_path = 'M_UwQC/' + releasePid
media_url = 'https://link.theplatform.com/s/' + tp_path
video_player_count += 1
except KeyError:
pass
if video_player_count > 1:
self.report_warning(
'The JSON data has %d video players. Only one will be extracted' % video_player_count)
# Fall back to videoPid if releasePid not found.
# TODO: Fall back to videoPid if releasePid manifest uses DRM.
if not video_player_count:
tp_path = 'M_UwQC/media/' + properties['videoPid']
media_url = 'https://link.theplatform.com/s/' + tp_path
theplatform_metadata = self._download_theplatform_metadata(tp_path, display_id) theplatform_metadata = self._download_theplatform_metadata(tp_path, display_id)
info = self._parse_theplatform_metadata(theplatform_metadata) info = self._parse_theplatform_metadata(theplatform_metadata)
video_id = theplatform_metadata['pid'] video_id = theplatform_metadata['pid']
@@ -110,41 +90,30 @@ class AMCNetworksIE(ThePlatformIE):
formats, subtitles = self._extract_theplatform_smil( formats, subtitles = self._extract_theplatform_smil(
media_url, video_id) media_url, video_id)
self._sort_formats(formats) self._sort_formats(formats)
thumbnails = []
thumbnail_urls = [properties.get('imageDesktop')]
if 'thumbnail' in info:
thumbnail_urls.append(info.pop('thumbnail'))
for thumbnail_url in thumbnail_urls:
if not thumbnail_url:
continue
mobj = re.search(r'(\d+)x(\d+)', thumbnail_url)
thumbnails.append({
'url': thumbnail_url,
'width': int(mobj.group(1)) if mobj else None,
'height': int(mobj.group(2)) if mobj else None,
})
info.update({ info.update({
'age_limit': parse_age_limit(rating),
'formats': formats,
'id': video_id, 'id': video_id,
'subtitles': subtitles, 'subtitles': subtitles,
'thumbnails': thumbnails, 'formats': formats,
'age_limit': parse_age_limit(parse_age_limit(rating)),
}) })
ns_keys = theplatform_metadata.get('$xmlns', {}).keys() ns_keys = theplatform_metadata.get('$xmlns', {}).keys()
if ns_keys: if ns_keys:
ns = list(ns_keys)[0] ns = list(ns_keys)[0]
episode = theplatform_metadata.get(ns + '$episodeTitle') or None series = theplatform_metadata.get(ns + '$show')
episode_number = int_or_none(
theplatform_metadata.get(ns + '$episode'))
season_number = int_or_none( season_number = int_or_none(
theplatform_metadata.get(ns + '$season')) theplatform_metadata.get(ns + '$season'))
series = theplatform_metadata.get(ns + '$show') or None episode = theplatform_metadata.get(ns + '$episodeTitle')
episode_number = int_or_none(
theplatform_metadata.get(ns + '$episode'))
if season_number:
title = 'Season %d - %s' % (season_number, title)
if series:
title = '%s - %s' % (series, title)
info.update({ info.update({
'title': title,
'series': series,
'season_number': season_number,
'episode': episode, 'episode': episode,
'episode_number': episode_number, 'episode_number': episode_number,
'season_number': season_number,
'series': series,
}) })
return info return info
-1
View File
@@ -42,7 +42,6 @@ class ApplePodcastsIE(InfoExtractor):
ember_data = self._parse_json(self._search_regex( ember_data = self._parse_json(self._search_regex(
r'id="shoebox-ember-data-store"[^>]*>\s*({.+?})\s*<', r'id="shoebox-ember-data-store"[^>]*>\s*({.+?})\s*<',
webpage, 'ember data'), episode_id) webpage, 'ember data'), episode_id)
ember_data = ember_data.get(episode_id) or ember_data
episode = ember_data['data']['attributes'] episode = ember_data['data']['attributes']
description = episode.get('description') or {} description = episode.get('description') or {}
+1 -3
View File
@@ -49,7 +49,6 @@ class BandcampIE(InfoExtractor):
'uploader': 'Ben Prunty', 'uploader': 'Ben Prunty',
'timestamp': 1396508491, 'timestamp': 1396508491,
'upload_date': '20140403', 'upload_date': '20140403',
'release_timestamp': 1396483200,
'release_date': '20140403', 'release_date': '20140403',
'duration': 260.877, 'duration': 260.877,
'track': 'Lanius (Battle)', 'track': 'Lanius (Battle)',
@@ -70,7 +69,6 @@ class BandcampIE(InfoExtractor):
'uploader': 'Mastodon', 'uploader': 'Mastodon',
'timestamp': 1322005399, 'timestamp': 1322005399,
'upload_date': '20111122', 'upload_date': '20111122',
'release_timestamp': 1076112000,
'release_date': '20040207', 'release_date': '20040207',
'duration': 120.79, 'duration': 120.79,
'track': 'Hail to Fire', 'track': 'Hail to Fire',
@@ -199,7 +197,7 @@ class BandcampIE(InfoExtractor):
'thumbnail': thumbnail, 'thumbnail': thumbnail,
'uploader': artist, 'uploader': artist,
'timestamp': timestamp, 'timestamp': timestamp,
'release_timestamp': unified_timestamp(tralbum.get('album_release_date')), 'release_date': unified_strdate(tralbum.get('album_release_date')),
'duration': duration, 'duration': duration,
'track': track, 'track': track,
'track_number': track_number, 'track_number': track_number,
+15 -190
View File
@@ -1,22 +1,17 @@
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
import functools
import itertools import itertools
import json
import re import re
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import ( from ..compat import (
compat_etree_Element, compat_etree_Element,
compat_HTTPError, compat_HTTPError,
compat_parse_qs,
compat_urllib_parse_urlparse,
compat_urlparse, compat_urlparse,
) )
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
OnDemandPagedList,
clean_html, clean_html,
dict_get, dict_get,
float_or_none, float_or_none,
@@ -816,7 +811,7 @@ class BBCIE(BBCCoUkIE):
@classmethod @classmethod
def suitable(cls, url): def suitable(cls, url):
EXCLUDE_IE = (BBCCoUkIE, BBCCoUkArticleIE, BBCCoUkIPlayerEpisodesIE, BBCCoUkIPlayerGroupIE, BBCCoUkPlaylistIE) EXCLUDE_IE = (BBCCoUkIE, BBCCoUkArticleIE, BBCCoUkIPlayerPlaylistIE, BBCCoUkPlaylistIE)
return (False if any(ie.suitable(url) for ie in EXCLUDE_IE) return (False if any(ie.suitable(url) for ie in EXCLUDE_IE)
else super(BBCIE, cls).suitable(url)) else super(BBCIE, cls).suitable(url))
@@ -1343,149 +1338,21 @@ class BBCCoUkPlaylistBaseIE(InfoExtractor):
playlist_id, title, description) playlist_id, title, description)
class BBCCoUkIPlayerPlaylistBaseIE(InfoExtractor): class BBCCoUkIPlayerPlaylistIE(BBCCoUkPlaylistBaseIE):
_VALID_URL_TMPL = r'https?://(?:www\.)?bbc\.co\.uk/iplayer/%%s/(?P<id>%s)' % BBCCoUkIE._ID_REGEX IE_NAME = 'bbc.co.uk:iplayer:playlist'
_VALID_URL = r'https?://(?:www\.)?bbc\.co\.uk/iplayer/(?:episodes|group)/(?P<id>%s)' % BBCCoUkIE._ID_REGEX
@staticmethod _URL_TEMPLATE = 'http://www.bbc.co.uk/iplayer/episode/%s'
def _get_default(episode, key, default_key='default'): _VIDEO_ID_TEMPLATE = r'data-ip-id=["\'](%s)'
return try_get(episode, lambda x: x[key][default_key])
def _get_description(self, data):
synopsis = data.get(self._DESCRIPTION_KEY) or {}
return dict_get(synopsis, ('large', 'medium', 'small'))
def _fetch_page(self, programme_id, per_page, series_id, page):
elements = self._get_elements(self._call_api(
programme_id, per_page, page + 1, series_id))
for element in elements:
episode = self._get_episode(element)
episode_id = episode.get('id')
if not episode_id:
continue
thumbnail = None
image = self._get_episode_image(episode)
if image:
thumbnail = image.replace('{recipe}', 'raw')
category = self._get_default(episode, 'labels', 'category')
yield {
'_type': 'url',
'id': episode_id,
'title': self._get_episode_field(episode, 'subtitle'),
'url': 'https://www.bbc.co.uk/iplayer/episode/' + episode_id,
'thumbnail': thumbnail,
'description': self._get_description(episode),
'categories': [category] if category else None,
'series': self._get_episode_field(episode, 'title'),
'ie_key': BBCCoUkIE.ie_key(),
}
def _real_extract(self, url):
pid = self._match_id(url)
qs = compat_parse_qs(compat_urllib_parse_urlparse(url).query)
series_id = qs.get('seriesId', [None])[0]
page = qs.get('page', [None])[0]
per_page = 36 if page else self._PAGE_SIZE
fetch_page = functools.partial(self._fetch_page, pid, per_page, series_id)
entries = fetch_page(int(page) - 1) if page else OnDemandPagedList(fetch_page, self._PAGE_SIZE)
playlist_data = self._get_playlist_data(self._call_api(pid, 1))
return self.playlist_result(
entries, pid, self._get_playlist_title(playlist_data),
self._get_description(playlist_data))
class BBCCoUkIPlayerEpisodesIE(BBCCoUkIPlayerPlaylistBaseIE):
IE_NAME = 'bbc.co.uk:iplayer:episodes'
_VALID_URL = BBCCoUkIPlayerPlaylistBaseIE._VALID_URL_TMPL % 'episodes'
_TESTS = [{ _TESTS = [{
'url': 'http://www.bbc.co.uk/iplayer/episodes/b05rcz9v', 'url': 'http://www.bbc.co.uk/iplayer/episodes/b05rcz9v',
'info_dict': { 'info_dict': {
'id': 'b05rcz9v', 'id': 'b05rcz9v',
'title': 'The Disappearance', 'title': 'The Disappearance',
'description': 'md5:58eb101aee3116bad4da05f91179c0cb', 'description': 'French thriller serial about a missing teenager.',
}, },
'playlist_mincount': 8, 'playlist_mincount': 6,
'skip': 'This programme is not currently available on BBC iPlayer',
}, { }, {
# all seasons
'url': 'https://www.bbc.co.uk/iplayer/episodes/b094m5t9/doctor-foster',
'info_dict': {
'id': 'b094m5t9',
'title': 'Doctor Foster',
'description': 'md5:5aa9195fad900e8e14b52acd765a9fd6',
},
'playlist_mincount': 10,
}, {
# explicit season
'url': 'https://www.bbc.co.uk/iplayer/episodes/b094m5t9/doctor-foster?seriesId=b094m6nv',
'info_dict': {
'id': 'b094m5t9',
'title': 'Doctor Foster',
'description': 'md5:5aa9195fad900e8e14b52acd765a9fd6',
},
'playlist_mincount': 5,
}, {
# all pages
'url': 'https://www.bbc.co.uk/iplayer/episodes/m0004c4v/beechgrove',
'info_dict': {
'id': 'm0004c4v',
'title': 'Beechgrove',
'description': 'Gardening show that celebrates Scottish horticulture and growing conditions.',
},
'playlist_mincount': 37,
}, {
# explicit page
'url': 'https://www.bbc.co.uk/iplayer/episodes/m0004c4v/beechgrove?page=2',
'info_dict': {
'id': 'm0004c4v',
'title': 'Beechgrove',
'description': 'Gardening show that celebrates Scottish horticulture and growing conditions.',
},
'playlist_mincount': 1,
}]
_PAGE_SIZE = 100
_DESCRIPTION_KEY = 'synopsis'
def _get_episode_image(self, episode):
return self._get_default(episode, 'image')
def _get_episode_field(self, episode, field):
return self._get_default(episode, field)
@staticmethod
def _get_elements(data):
return data['entities']['results']
@staticmethod
def _get_episode(element):
return element.get('episode') or {}
def _call_api(self, pid, per_page, page=1, series_id=None):
variables = {
'id': pid,
'page': page,
'perPage': per_page,
}
if series_id:
variables['sliceId'] = series_id
return self._download_json(
'https://graph.ibl.api.bbc.co.uk/', pid, headers={
'Content-Type': 'application/json'
}, data=json.dumps({
'id': '5692d93d5aac8d796a0305e895e61551',
'variables': variables,
}).encode('utf-8'))['data']['programme']
@staticmethod
def _get_playlist_data(data):
return data
def _get_playlist_title(self, data):
return self._get_default(data, 'title')
class BBCCoUkIPlayerGroupIE(BBCCoUkIPlayerPlaylistBaseIE):
IE_NAME = 'bbc.co.uk:iplayer:group'
_VALID_URL = BBCCoUkIPlayerPlaylistBaseIE._VALID_URL_TMPL % 'group'
_TESTS = [{
# Available for over a year unlike 30 days for most other programmes # Available for over a year unlike 30 days for most other programmes
'url': 'http://www.bbc.co.uk/iplayer/group/p02tcc32', 'url': 'http://www.bbc.co.uk/iplayer/group/p02tcc32',
'info_dict': { 'info_dict': {
@@ -1494,56 +1361,14 @@ class BBCCoUkIPlayerGroupIE(BBCCoUkIPlayerPlaylistBaseIE):
'description': 'md5:683e901041b2fe9ba596f2ab04c4dbe7', 'description': 'md5:683e901041b2fe9ba596f2ab04c4dbe7',
}, },
'playlist_mincount': 10, 'playlist_mincount': 10,
}, {
# all pages
'url': 'https://www.bbc.co.uk/iplayer/group/p081d7j7',
'info_dict': {
'id': 'p081d7j7',
'title': 'Music in Scotland',
'description': 'Perfomances in Scotland and programmes featuring Scottish acts.',
},
'playlist_mincount': 47,
}, {
# explicit page
'url': 'https://www.bbc.co.uk/iplayer/group/p081d7j7?page=2',
'info_dict': {
'id': 'p081d7j7',
'title': 'Music in Scotland',
'description': 'Perfomances in Scotland and programmes featuring Scottish acts.',
},
'playlist_mincount': 11,
}] }]
_PAGE_SIZE = 200
_DESCRIPTION_KEY = 'synopses'
def _get_episode_image(self, episode): def _extract_title_and_description(self, webpage):
return self._get_default(episode, 'images', 'standard') title = self._search_regex(r'<h1>([^<]+)</h1>', webpage, 'title', fatal=False)
description = self._search_regex(
def _get_episode_field(self, episode, field): r'<p[^>]+class=(["\'])subtitle\1[^>]*>(?P<value>[^<]+)</p>',
return episode.get(field) webpage, 'description', fatal=False, group='value')
return title, description
@staticmethod
def _get_elements(data):
return data['elements']
@staticmethod
def _get_episode(element):
return element
def _call_api(self, pid, per_page, page=1, series_id=None):
return self._download_json(
'http://ibl.api.bbc.co.uk/ibl/v1/groups/%s/episodes' % pid,
pid, query={
'page': page,
'per_page': per_page,
})['group_episodes']
@staticmethod
def _get_playlist_data(data):
return data['group']
def _get_playlist_title(self, data):
return data.get('title')
class BBCCoUkPlaylistIE(BBCCoUkPlaylistBaseIE): class BBCCoUkPlaylistIE(BBCCoUkPlaylistBaseIE):
+5 -50
View File
@@ -7,7 +7,6 @@ import re
from .common import InfoExtractor, SearchInfoExtractor from .common import InfoExtractor, SearchInfoExtractor
from ..compat import ( from ..compat import (
compat_str,
compat_parse_qs, compat_parse_qs,
compat_urlparse, compat_urlparse,
) )
@@ -16,7 +15,6 @@ from ..utils import (
int_or_none, int_or_none,
float_or_none, float_or_none,
parse_iso8601, parse_iso8601,
try_get,
smuggle_url, smuggle_url,
str_or_none, str_or_none,
strip_jsonp, strip_jsonp,
@@ -115,13 +113,6 @@ class BiliBiliIE(InfoExtractor):
# new BV video id format # new BV video id format
'url': 'https://www.bilibili.com/video/BV1JE411F741', 'url': 'https://www.bilibili.com/video/BV1JE411F741',
'only_matching': True, 'only_matching': True,
}, {
# Anthology
'url': 'https://www.bilibili.com/video/BV1bK411W797',
'info_dict': {
'id': 'BV1bK411W797',
},
'playlist_count': 17,
}] }]
_APP_KEY = 'iVGUTjsxvpLeuDCf' _APP_KEY = 'iVGUTjsxvpLeuDCf'
@@ -148,19 +139,9 @@ class BiliBiliIE(InfoExtractor):
page_id = mobj.group('page') page_id = mobj.group('page')
webpage = self._download_webpage(url, video_id) webpage = self._download_webpage(url, video_id)
# Bilibili anthologies are similar to playlists but all videos share the same video ID as the anthology itself.
# If the video has no page argument, check to see if it's an anthology
if page_id is None:
if not self._downloader.params.get('noplaylist'):
r = self._extract_anthology_entries(bv_id, video_id, webpage)
if r is not None:
self.to_screen('Downloading anthology %s - add --no-playlist to just download video' % video_id)
return r
self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
if 'anime/' not in url: if 'anime/' not in url:
cid = self._search_regex( cid = self._search_regex(
r'\bcid(?:["\']:|=)(\d+),["\']page(?:["\']:|=)' + compat_str(page_id), webpage, 'cid', r'\bcid(?:["\']:|=)(\d+),["\']page(?:["\']:|=)' + str(page_id), webpage, 'cid',
default=None default=None
) or self._search_regex( ) or self._search_regex(
r'\bcid(?:["\']:|=)(\d+)', webpage, 'cid', r'\bcid(?:["\']:|=)(\d+)', webpage, 'cid',
@@ -189,7 +170,6 @@ class BiliBiliIE(InfoExtractor):
cid = js['result']['cid'] cid = js['result']['cid']
headers = { headers = {
'Accept': 'application/json',
'Referer': url 'Referer': url
} }
headers.update(self.geo_verification_headers()) headers.update(self.geo_verification_headers())
@@ -243,18 +223,7 @@ class BiliBiliIE(InfoExtractor):
title = self._html_search_regex( title = self._html_search_regex(
(r'<h1[^>]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1', (r'<h1[^>]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1',
r'(?s)<h1[^>]*>(?P<title>.+?)</h1>'), webpage, 'title', r'(?s)<h1[^>]*>(?P<title>.+?)</h1>'), webpage, 'title',
group='title') group='title') + ('_p' + str(page_id) if page_id is not None else '')
# Get part title for anthologies
if page_id is not None:
# TODO: The json is already downloaded by _extract_anthology_entries. Don't redownload for each video
part_title = try_get(
self._download_json(
"https://api.bilibili.com/x/player/pagelist?bvid=%s&jsonp=jsonp" % bv_id,
video_id, note='Extracting videos in anthology'),
lambda x: x['data'][int(page_id) - 1]['part'])
title = part_title or title
description = self._html_search_meta('description', webpage) description = self._html_search_meta('description', webpage)
timestamp = unified_timestamp(self._html_search_regex( timestamp = unified_timestamp(self._html_search_regex(
r'<time[^>]+datetime="([^"]+)"', webpage, 'upload time', r'<time[^>]+datetime="([^"]+)"', webpage, 'upload time',
@@ -264,7 +233,7 @@ class BiliBiliIE(InfoExtractor):
# TODO 'view_count' requires deobfuscating Javascript # TODO 'view_count' requires deobfuscating Javascript
info = { info = {
'id': compat_str(video_id) if page_id is None else '%s_p%s' % (video_id, page_id), 'id': str(video_id) if page_id is None else '%s_p%s' % (video_id, page_id),
'cid': cid, 'cid': cid,
'title': title, 'title': title,
'description': description, 'description': description,
@@ -330,7 +299,7 @@ class BiliBiliIE(InfoExtractor):
global_info = { global_info = {
'_type': 'multi_video', '_type': 'multi_video',
'id': compat_str(video_id), 'id': video_id,
'bv_id': bv_id, 'bv_id': bv_id,
'title': title, 'title': title,
'description': description, 'description': description,
@@ -342,20 +311,6 @@ class BiliBiliIE(InfoExtractor):
return global_info return global_info
def _extract_anthology_entries(self, bv_id, video_id, webpage):
title = self._html_search_regex(
(r'<h1[^>]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1',
r'(?s)<h1[^>]*>(?P<title>.+?)</h1>'), webpage, 'title',
group='title')
json_data = self._download_json(
"https://api.bilibili.com/x/player/pagelist?bvid=%s&jsonp=jsonp" % bv_id,
video_id, note='Extracting videos in anthology')
if len(json_data['data']) > 1:
return self.playlist_from_matches(
json_data['data'], bv_id, title, ie=BiliBiliIE.ie_key(),
getter=lambda entry: 'https://www.bilibili.com/video/%s?p=%d' % (bv_id, entry['page']))
def _get_video_id_set(self, id, is_bv): def _get_video_id_set(self, id, is_bv):
query = {'bvid': id} if is_bv else {'aid': id} query = {'bvid': id} if is_bv else {'aid': id}
response = self._download_json( response = self._download_json(
@@ -550,7 +505,7 @@ class BiliBiliSearchIE(SearchInfoExtractor):
videos = data['result'] videos = data['result']
for video in videos: for video in videos:
e = self.url_result(video['arcurl'], 'BiliBili', compat_str(video['aid'])) e = self.url_result(video['arcurl'], 'BiliBili', str(video['aid']))
entries.append(e) entries.append(e)
if(len(entries) >= n or len(videos) >= BiliBiliSearchIE.MAX_NUMBER_OF_RESULTS): if(len(entries) >= n or len(videos) >= BiliBiliSearchIE.MAX_NUMBER_OF_RESULTS):
+4 -7
View File
@@ -27,10 +27,10 @@ class CBSBaseIE(ThePlatformFeedIE):
class CBSIE(CBSBaseIE): class CBSIE(CBSBaseIE):
_VALID_URL = r'(?:cbs:|https?://(?:www\.)?(?:(?:cbs|paramountplus)\.com/shows/[^/]+/video|colbertlateshow\.com/(?:video|podcasts))/)(?P<id>[\w-]+)' _VALID_URL = r'(?:cbs:|https?://(?:www\.)?(?:cbs\.com/shows/[^/]+/video|colbertlateshow\.com/(?:video|podcasts))/)(?P<id>[\w-]+)'
_TESTS = [{ _TESTS = [{
'url': 'https://www.cbs.com/shows/garth-brooks/video/_u7W953k6la293J7EPTd9oHkSPs6Xn6_/connect-chat-feat-garth-brooks/', 'url': 'http://www.cbs.com/shows/garth-brooks/video/_u7W953k6la293J7EPTd9oHkSPs6Xn6_/connect-chat-feat-garth-brooks/',
'info_dict': { 'info_dict': {
'id': '_u7W953k6la293J7EPTd9oHkSPs6Xn6_', 'id': '_u7W953k6la293J7EPTd9oHkSPs6Xn6_',
'ext': 'mp4', 'ext': 'mp4',
@@ -52,19 +52,16 @@ class CBSIE(CBSBaseIE):
}, { }, {
'url': 'http://www.colbertlateshow.com/podcasts/dYSwjqPs_X1tvbV_P2FcPWRa_qT6akTC/in-the-bad-room-with-stephen/', 'url': 'http://www.colbertlateshow.com/podcasts/dYSwjqPs_X1tvbV_P2FcPWRa_qT6akTC/in-the-bad-room-with-stephen/',
'only_matching': True, 'only_matching': True,
}, {
'url': 'https://www.paramountplus.com/shows/all-rise/video/QmR1WhNkh1a_IrdHZrbcRklm176X_rVc/all-rise-space/',
'only_matching': True,
}] }]
def _extract_video_info(self, content_id, site='cbs', mpx_acc=2198311517): def _extract_video_info(self, content_id, site='cbs', mpx_acc=2198311517):
items_data = self._download_xml( items_data = self._download_xml(
'https://can.cbs.com/thunder/player/videoPlayerService.php', 'http://can.cbs.com/thunder/player/videoPlayerService.php',
content_id, query={'partner': site, 'contentId': content_id}) content_id, query={'partner': site, 'contentId': content_id})
video_data = xpath_element(items_data, './/item') video_data = xpath_element(items_data, './/item')
title = xpath_text(video_data, 'videoTitle', 'title') or xpath_text(video_data, 'videotitle', 'title') title = xpath_text(video_data, 'videoTitle', 'title') or xpath_text(video_data, 'videotitle', 'title')
tp_path = 'dJ5BDC/media/guid/%d/%s' % (mpx_acc, content_id) tp_path = 'dJ5BDC/media/guid/%d/%s' % (mpx_acc, content_id)
tp_release_url = 'https://link.theplatform.com/s/' + tp_path tp_release_url = 'http://link.theplatform.com/s/' + tp_path
asset_types = [] asset_types = []
subtitles = {} subtitles = {}
+11 -40
View File
@@ -231,9 +231,8 @@ class InfoExtractor(object):
uploader: Full name of the video uploader. uploader: Full name of the video uploader.
license: License name the video is licensed under. license: License name the video is licensed under.
creator: The creator of the video. creator: The creator of the video.
release_timestamp: UNIX timestamp of the moment the video was released.
release_date: The date (YYYYMMDD) when the video was released. release_date: The date (YYYYMMDD) when the video was released.
timestamp: UNIX timestamp of the moment the video was uploaded timestamp: UNIX timestamp of the moment the video became available.
upload_date: Video upload date (YYYYMMDD). upload_date: Video upload date (YYYYMMDD).
If not explicitly set, calculated from timestamp. If not explicitly set, calculated from timestamp.
uploader_id: Nickname or id of the video uploader. uploader_id: Nickname or id of the video uploader.
@@ -252,8 +251,8 @@ class InfoExtractor(object):
* "data": The subtitles file contents * "data": The subtitles file contents
* "url": A URL pointing to the subtitles file * "url": A URL pointing to the subtitles file
"ext" will be calculated from URL if missing "ext" will be calculated from URL if missing
automatic_captions: Like 'subtitles'; contains automatically generated automatic_captions: Like 'subtitles', used by the YoutubeIE for
captions instead of normal subtitles automatically generated captions
duration: Length of the video in seconds, as an integer or float. duration: Length of the video in seconds, as an integer or float.
view_count: How many users have watched the video on the platform. view_count: How many users have watched the video on the platform.
like_count: Number of positive ratings of the video like_count: Number of positive ratings of the video
@@ -265,7 +264,6 @@ class InfoExtractor(object):
properties (all but one of text or html optional): properties (all but one of text or html optional):
* "author" - human-readable name of the comment author * "author" - human-readable name of the comment author
* "author_id" - user ID of the comment author * "author_id" - user ID of the comment author
* "author_thumbnail" - The thumbnail of the comment author
* "id" - Comment ID * "id" - Comment ID
* "html" - Comment as HTML * "html" - Comment as HTML
* "text" - Plain text of the comment * "text" - Plain text of the comment
@@ -273,12 +271,6 @@ class InfoExtractor(object):
* "parent" - ID of the comment this one is replying to. * "parent" - ID of the comment this one is replying to.
Set to "root" to indicate that this is a Set to "root" to indicate that this is a
comment to the original video. comment to the original video.
* "like_count" - Number of positive ratings of the comment
* "dislike_count" - Number of negative ratings of the comment
* "is_favorited" - Whether the comment is marked as
favorite by the video uploader
* "author_is_uploader" - Whether the comment is made by
the video uploader
age_limit: Age restriction for the video, as an integer (years) age_limit: Age restriction for the video, as an integer (years)
webpage_url: The URL to the video webpage, if given to yt-dlp it webpage_url: The URL to the video webpage, if given to yt-dlp it
should allow to get the same result again. (It will be set should allow to get the same result again. (It will be set
@@ -301,11 +293,7 @@ class InfoExtractor(object):
playable_in_embed: Whether this video is allowed to play in embedded playable_in_embed: Whether this video is allowed to play in embedded
players on other sites. Can be True (=always allowed), players on other sites. Can be True (=always allowed),
False (=never allowed), None (=unknown), or a string False (=never allowed), None (=unknown), or a string
specifying the criteria for embedability (Eg: 'whitelist') specifying the criteria for embedability (Eg: 'whitelist').
availability: Under what condition the video is available. One of
'private', 'premium_only', 'subscriber_only', 'needs_auth',
'unlisted' or 'public'. Use 'InfoExtractor._availability'
to set it
__post_extractor: A function to be called just before the metadata is __post_extractor: A function to be called just before the metadata is
written to either disk, logger or console. The function written to either disk, logger or console. The function
must return a dict which will be added to the info_dict. must return a dict which will be added to the info_dict.
@@ -1421,8 +1409,8 @@ class InfoExtractor(object):
'ie_pref': {'priority': True, 'type': 'extractor'}, 'ie_pref': {'priority': True, 'type': 'extractor'},
'hasvid': {'priority': True, 'field': 'vcodec', 'type': 'boolean', 'not_in_list': ('none',)}, 'hasvid': {'priority': True, 'field': 'vcodec', 'type': 'boolean', 'not_in_list': ('none',)},
'hasaud': {'field': 'acodec', 'type': 'boolean', 'not_in_list': ('none',)}, 'hasaud': {'field': 'acodec', 'type': 'boolean', 'not_in_list': ('none',)},
'lang': {'priority': True, 'convert': 'ignore', 'field': 'language_preference'}, 'lang': {'priority': True, 'convert': 'ignore', 'type': 'extractor', 'field': 'language_preference'},
'quality': {'convert': 'float_none'}, 'quality': {'convert': 'float_none', 'type': 'extractor'},
'filesize': {'convert': 'bytes'}, 'filesize': {'convert': 'bytes'},
'fs_approx': {'convert': 'bytes', 'field': 'filesize_approx'}, 'fs_approx': {'convert': 'bytes', 'field': 'filesize_approx'},
'id': {'convert': 'string', 'field': 'format_id'}, 'id': {'convert': 'string', 'field': 'format_id'},
@@ -1433,7 +1421,7 @@ class InfoExtractor(object):
'vbr': {'convert': 'float_none'}, 'vbr': {'convert': 'float_none'},
'abr': {'convert': 'float_none'}, 'abr': {'convert': 'float_none'},
'asr': {'convert': 'float_none'}, 'asr': {'convert': 'float_none'},
'source': {'convert': 'ignore', 'field': 'source_preference'}, 'source': {'convert': 'ignore', 'type': 'extractor', 'field': 'source_preference'},
'codec': {'type': 'combined', 'field': ('vcodec', 'acodec')}, 'codec': {'type': 'combined', 'field': ('vcodec', 'acodec')},
'br': {'type': 'combined', 'field': ('tbr', 'vbr', 'abr'), 'same_limit': True}, 'br': {'type': 'combined', 'field': ('tbr', 'vbr', 'abr'), 'same_limit': True},
@@ -1861,9 +1849,8 @@ class InfoExtractor(object):
def _extract_m3u8_formats(self, m3u8_url, video_id, ext=None, def _extract_m3u8_formats(self, m3u8_url, video_id, ext=None,
entry_protocol='m3u8', preference=None, quality=None, entry_protocol='m3u8', preference=None, quality=None,
m3u8_id=None, note=None, errnote=None, m3u8_id=None, live=False, note=None, errnote=None,
fatal=True, live=False, data=None, headers={}, fatal=True, data=None, headers={}, query={}):
query={}):
res = self._download_webpage_handle( res = self._download_webpage_handle(
m3u8_url, video_id, m3u8_url, video_id,
note=note or 'Downloading m3u8 information', note=note or 'Downloading m3u8 information',
@@ -2063,11 +2050,11 @@ class InfoExtractor(object):
playlist_formats = _extract_m3u8_playlist_formats(manifest_url, video_id=video_id, playlist_formats = _extract_m3u8_playlist_formats(manifest_url, video_id=video_id,
fatal=fatal, data=data, headers=headers) fatal=fatal, data=data, headers=headers)
for frmt in playlist_formats: for format in playlist_formats:
format_id = [] format_id = []
if m3u8_id: if m3u8_id:
format_id.append(m3u8_id) format_id.append(m3u8_id)
format_index = frmt.get('index') format_index = format.get('index')
stream_name = build_stream_name() stream_name = build_stream_name()
# Bandwidth of live streams may differ over time thus making # Bandwidth of live streams may differ over time thus making
# format_id unpredictable. So it's better to keep provided # format_id unpredictable. So it's better to keep provided
@@ -2122,8 +2109,6 @@ class InfoExtractor(object):
# TODO: update acodec for audio only formats with # TODO: update acodec for audio only formats with
# the same GROUP-ID # the same GROUP-ID
f['acodec'] = 'none' f['acodec'] = 'none'
if not f.get('ext'):
f['ext'] = 'm4a' if f.get('vcodec') == 'none' else 'mp4'
formats.append(f) formats.append(f)
# for DailyMotion # for DailyMotion
@@ -3336,20 +3321,6 @@ class InfoExtractor(object):
def _generic_title(self, url): def _generic_title(self, url):
return compat_urllib_parse_unquote(os.path.splitext(url_basename(url))[0]) return compat_urllib_parse_unquote(os.path.splitext(url_basename(url))[0])
@staticmethod
def _availability(is_private, needs_premium, needs_subscription, needs_auth, is_unlisted):
all_known = all(map(
lambda x: x is not None,
(is_private, needs_premium, needs_subscription, needs_auth, is_unlisted)))
return (
'private' if is_private
else 'premium_only' if needs_premium
else 'subscriber_only' if needs_subscription
else 'needs_auth' if needs_auth
else 'unlisted' if is_unlisted
else 'public' if all_known
else None)
class SearchInfoExtractor(InfoExtractor): class SearchInfoExtractor(InfoExtractor):
""" """
+10 -24
View File
@@ -108,8 +108,7 @@ from .bandcamp import BandcampIE, BandcampAlbumIE, BandcampWeeklyIE
from .bbc import ( from .bbc import (
BBCCoUkIE, BBCCoUkIE,
BBCCoUkArticleIE, BBCCoUkArticleIE,
BBCCoUkIPlayerEpisodesIE, BBCCoUkIPlayerPlaylistIE,
BBCCoUkIPlayerGroupIE,
BBCCoUkPlaylistIE, BBCCoUkPlaylistIE,
BBCIE, BBCIE,
) )
@@ -451,7 +450,10 @@ from .gamestar import GameStarIE
from .gaskrank import GaskrankIE from .gaskrank import GaskrankIE
from .gazeta import GazetaIE from .gazeta import GazetaIE
from .gdcvault import GDCVaultIE from .gdcvault import GDCVaultIE
from .gedidigital import GediDigitalIE from .gedi import (
GediIE,
GediEmbedsIE,
)
from .generic import GenericIE from .generic import GenericIE
from .gfycat import GfycatIE from .gfycat import GfycatIE
from .giantbomb import GiantBombIE from .giantbomb import GiantBombIE
@@ -709,10 +711,7 @@ from .mixcloud import (
MixcloudUserIE, MixcloudUserIE,
MixcloudPlaylistIE, MixcloudPlaylistIE,
) )
from .mlb import ( from .mlb import MLBIE
MLBIE,
MLBVideoIE,
)
from .mnet import MnetIE from .mnet import MnetIE
from .moevideo import MoeVideoIE from .moevideo import MoeVideoIE
from .mofosex import ( from .mofosex import (
@@ -736,8 +735,6 @@ from .mtv import (
MTVServicesEmbeddedIE, MTVServicesEmbeddedIE,
MTVDEIE, MTVDEIE,
MTVJapanIE, MTVJapanIE,
MTVItaliaIE,
MTVItaliaProgrammaIE,
) )
from .muenchentv import MuenchenTVIE from .muenchentv import MuenchenTVIE
from .mwave import MwaveIE, MwaveMeetGreetIE from .mwave import MwaveIE, MwaveMeetGreetIE
@@ -826,7 +823,7 @@ from .nick import (
NickNightIE, NickNightIE,
NickRuIE, NickRuIE,
) )
from .niconico import NiconicoIE, NiconicoPlaylistIE, NiconicoUserIE from .niconico import NiconicoIE, NiconicoPlaylistIE
from .ninecninemedia import NineCNineMediaIE from .ninecninemedia import NineCNineMediaIE
from .ninegag import NineGagIE from .ninegag import NineGagIE
from .ninenow import NineNowIE from .ninenow import NineNowIE
@@ -957,7 +954,6 @@ from .plays import PlaysTVIE
from .playtvak import PlaytvakIE from .playtvak import PlaytvakIE
from .playvid import PlayvidIE from .playvid import PlayvidIE
from .playwire import PlaywireIE from .playwire import PlaywireIE
from .plutotv import PlutoTVIE
from .pluralsight import ( from .pluralsight import (
PluralsightIE, PluralsightIE,
PluralsightCourseIE, PluralsightCourseIE,
@@ -1183,10 +1179,7 @@ from .spike import (
BellatorIE, BellatorIE,
ParamountNetworkIE, ParamountNetworkIE,
) )
from .stitcher import ( from .stitcher import StitcherIE
StitcherIE,
StitcherShowIE,
)
from .sport5 import Sport5IE from .sport5 import Sport5IE
from .sportbox import SportBoxIE from .sportbox import SportBoxIE
from .sportdeutschland import SportDeutschlandIE from .sportdeutschland import SportDeutschlandIE
@@ -1567,7 +1560,6 @@ from .weibo import (
WeiboMobileIE WeiboMobileIE
) )
from .weiqitv import WeiqiTVIE from .weiqitv import WeiqiTVIE
from .wimtv import WimTVIE
from .wistia import ( from .wistia import (
WistiaIE, WistiaIE,
WistiaPlaylistIE, WistiaPlaylistIE,
@@ -1674,14 +1666,8 @@ from .zattoo import (
ZattooLiveIE, ZattooLiveIE,
) )
from .zdf import ZDFIE, ZDFChannelIE from .zdf import ZDFIE, ZDFChannelIE
from .zee5 import (
Zee5IE,
Zee5SeriesIE,
)
from .zhihu import ZhihuIE from .zhihu import ZhihuIE
from .zingmp3 import ( from .zingmp3 import ZingMp3IE
ZingMp3IE, from .zee5 import Zee5IE
ZingMp3AlbumIE,
)
from .zoom import ZoomIE from .zoom import ZoomIE
from .zype import ZypeIE from .zype import ZypeIE
+1 -1
View File
@@ -17,7 +17,7 @@ class FujiTVFODPlus7IE(InfoExtractor):
def _real_extract(self, url): def _real_extract(self, url):
video_id = self._match_id(url) video_id = self._match_id(url)
formats = self._extract_m3u8_formats( formats = self._extract_m3u8_formats(
self._BASE_URL + 'abr/pc_html5/%s.m3u8' % video_id, video_id, 'mp4') self._BASE_URL + 'abr/pc_html5/%s.m3u8' % video_id, video_id)
for f in formats: for f in formats:
wh = self._BITRATE_MAP.get(f.get('tbr')) wh = self._BITRATE_MAP.get(f.get('tbr'))
if wh: if wh:
+266
View File
@@ -0,0 +1,266 @@
# coding: utf-8
from __future__ import unicode_literals
import re
from .common import InfoExtractor
from ..compat import compat_str
from ..utils import (
base_url,
url_basename,
urljoin,
)
class GediBaseIE(InfoExtractor):
@staticmethod
def _clean_audio_fmts(formats):
unique_formats = []
for f in formats:
if 'acodec' in f:
unique_formats.append(f)
formats[:] = unique_formats
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
player_data = re.findall(
r'PlayerFactory\.setParam\(\'(?P<type>.+?)\',\s*\'(?P<name>.+?)\',\s*\'(?P<val>.+?)\'\);',
webpage)
formats = []
audio_fmts = []
hls_fmts = []
http_fmts = []
title = ''
thumb = ''
fmt_reg = r'(?P<t>video|audio)-(?P<p>rrtv|hls)-(?P<h>[\w\d]+)(?:-(?P<br>[\w\d]+))?$'
br_reg = r'video-rrtv-(?P<br>\d+)-'
for t, n, v in player_data:
if t == 'format':
m = re.match(fmt_reg, n)
if m:
# audio formats
if m.group('t') == 'audio':
if m.group('p') == 'hls':
audio_fmts.extend(self._extract_m3u8_formats(
v, video_id, 'm4a', m3u8_id='hls', fatal=False))
elif m.group('p') == 'rrtv':
audio_fmts.append({
'format_id': 'mp3',
'url': v,
'tbr': 128,
'ext': 'mp3',
'vcodec': 'none',
'acodec': 'mp3',
})
# video formats
elif m.group('t') == 'video':
# hls manifest video
if m.group('p') == 'hls':
hls_fmts.extend(self._extract_m3u8_formats(
v, video_id, 'mp4', m3u8_id='hls', fatal=False))
# direct mp4 video
elif m.group('p') == 'rrtv':
if not m.group('br'):
mm = re.search(br_reg, v)
http_fmts.append({
'format_id': 'https-' + m.group('h'),
'protocol': 'https',
'url': v,
'tbr': int(m.group('br')) if m.group('br') else
(int(mm.group('br')) if mm.group('br') else 0),
'height': int(m.group('h'))
})
elif t == 'param':
if n == 'videotitle':
title = v
if n == 'image_full_play':
thumb = v
title = self._og_search_title(webpage) if title == '' else title
# clean weird char
title = compat_str(title).encode('utf8', 'replace').replace(b'\xc3\x82', b'').decode('utf8', 'replace')
if audio_fmts:
self._clean_audio_fmts(audio_fmts)
self._sort_formats(audio_fmts)
if hls_fmts:
self._sort_formats(hls_fmts)
if http_fmts:
self._sort_formats(http_fmts)
formats.extend(audio_fmts)
formats.extend(hls_fmts)
formats.extend(http_fmts)
return {
'id': video_id,
'title': title,
'description': self._html_search_meta('twitter:description', webpage),
'thumbnail': thumb,
'formats': formats,
}
class GediIE(GediBaseIE):
_VALID_URL = r'''(?x)https?://video\.
(?:
(?:espresso\.)?repubblica
|lastampa
|huffingtonpost
|ilsecoloxix
|iltirreno
|messaggeroveneto
|ilpiccolo
|gazzettadimantova
|mattinopadova
|laprovinciapavese
|tribunatreviso
|nuovavenezia
|gazzettadimodena
|lanuovaferrara
|corrierealpi
|lasentinella
)
(?:\.gelocal)?\.it/(?!embed/).+?/(?P<id>[\d/]+)(?:\?|\&|$)'''
_TESTS = [{
'url': 'https://video.lastampa.it/politica/il-paradosso-delle-regionali-la-lega-vince-ma-sembra-aver-perso/121559/121683',
'md5': '84658d7fb9e55a6e57ecc77b73137494',
'info_dict': {
'id': '121559/121683',
'ext': 'mp4',
'title': 'Il paradosso delle Regionali: ecco perché la Lega vince ma sembra aver perso',
'description': 'md5:de7f4d6eaaaf36c153b599b10f8ce7ca',
'thumbnail': r're:^https://www\.repstatic\.it/video/photo/.+?-thumb-social-play\.jpg$',
},
}, {
'url': 'https://video.repubblica.it/motori/record-della-pista-a-spa-francorchamps-la-pagani-huayra-roadster-bc-stupisce/367415/367963',
'md5': 'e763b94b7920799a0e0e23ffefa2d157',
'info_dict': {
'id': '367415/367963',
'ext': 'mp4',
'title': 'Record della pista a Spa Francorchamps, la Pagani Huayra Roadster BC stupisce',
'description': 'md5:5deb503cefe734a3eb3f07ed74303920',
'thumbnail': r're:^https://www\.repstatic\.it/video/photo/.+?-thumb-social-play\.jpg$',
},
}, {
'url': 'https://video.ilsecoloxix.it/sport/cassani-e-i-brividi-azzurri-ai-mondiali-di-imola-qui-mi-sono-innamorato-del-ciclismo-da-ragazzino-incredibile-tornarci-da-ct/66184/66267',
'md5': 'e48108e97b1af137d22a8469f2019057',
'info_dict': {
'id': '66184/66267',
'ext': 'mp4',
'title': 'Cassani e i brividi azzurri ai Mondiali di Imola: \\"Qui mi sono innamorato del ciclismo da ragazzino, incredibile tornarci da ct\\"',
'description': 'md5:fc9c50894f70a2469bb9b54d3d0a3d3b',
'thumbnail': r're:^https://www\.repstatic\.it/video/photo/.+?-thumb-social-play\.jpg$',
},
}, {
'url': 'https://video.iltirreno.gelocal.it/sport/dentro-la-notizia-ferrari-cosa-succede-a-maranello/141059/142723',
'md5': 'a6e39f3bdc1842bbd92abbbbef230817',
'info_dict': {
'id': '141059/142723',
'ext': 'mp4',
'title': 'Dentro la notizia - Ferrari, cosa succede a Maranello',
'description': 'md5:9907d65b53765681fa3a0b3122617c1f',
'thumbnail': r're:^https://www\.repstatic\.it/video/photo/.+?-thumb-social-play\.jpg$',
},
}]
class GediEmbedsIE(GediBaseIE):
_VALID_URL = r'''(?x)https?://video\.
(?:
(?:espresso\.)?repubblica
|lastampa
|huffingtonpost
|ilsecoloxix
|iltirreno
|messaggeroveneto
|ilpiccolo
|gazzettadimantova
|mattinopadova
|laprovinciapavese
|tribunatreviso
|nuovavenezia
|gazzettadimodena
|lanuovaferrara
|corrierealpi
|lasentinella
)
(?:\.gelocal)?\.it/embed/.+?/(?P<id>[\d/]+)(?:\?|\&|$)'''
_TESTS = [{
'url': 'https://video.huffingtonpost.it/embed/politica/cotticelli-non-so-cosa-mi-sia-successo-sto-cercando-di-capire-se-ho-avuto-un-malore/29312/29276?responsive=true&el=video971040871621586700',
'md5': 'f4ac23cadfea7fef89bea536583fa7ed',
'info_dict': {
'id': '29312/29276',
'ext': 'mp4',
'title': 'Cotticelli: \\"Non so cosa mi sia successo. Sto cercando di capire se ho avuto un malore\\"',
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
'thumbnail': r're:^https://www\.repstatic\.it/video/photo/.+?-thumb-social-play\.jpg$',
},
}, {
'url': 'https://video.espresso.repubblica.it/embed/tutti-i-video/01-ted-villa/14772/14870&width=640&height=360',
'md5': '0391c2c83c6506581003aaf0255889c0',
'info_dict': {
'id': '14772/14870',
'ext': 'mp4',
'title': 'Festival EMERGENCY, Villa: «La buona informazione aiuta la salute» (14772-14870)',
'description': 'md5:2bce954d278248f3c950be355b7c2226',
'thumbnail': r're:^https://www\.repstatic\.it/video/photo/.+?-thumb-social-play\.jpg$',
},
}]
@staticmethod
def _sanitize_urls(urls):
# add protocol if missing
for i, e in enumerate(urls):
if e.startswith('//'):
urls[i] = 'https:%s' % e
# clean iframes urls
for i, e in enumerate(urls):
urls[i] = urljoin(base_url(e), url_basename(e))
return urls
@staticmethod
def _extract_urls(webpage):
entries = [
mobj.group('url')
for mobj in re.finditer(r'''(?x)
(?:
data-frame-src=|
<iframe[^\n]+src=
)
(["'])
(?P<url>https?://video\.
(?:
(?:espresso\.)?repubblica
|lastampa
|huffingtonpost
|ilsecoloxix
|iltirreno
|messaggeroveneto
|ilpiccolo
|gazzettadimantova
|mattinopadova
|laprovinciapavese
|tribunatreviso
|nuovavenezia
|gazzettadimodena
|lanuovaferrara
|corrierealpi
|lasentinella
)
(?:\.gelocal)?\.it/embed/.+?)
\1''', webpage)]
return GediEmbedsIE._sanitize_urls(entries)
@staticmethod
def _extract_url(webpage):
urls = GediEmbedsIE._extract_urls(webpage)
return urls[0] if urls else None
-210
View File
@@ -1,210 +0,0 @@
# coding: utf-8
from __future__ import unicode_literals
import re
from .common import InfoExtractor
from ..utils import (
base_url,
determine_ext,
int_or_none,
url_basename,
urljoin,
)
class GediDigitalIE(InfoExtractor):
_VALID_URL = r'''(?x)(?P<url>(?:https?:)//video\.
(?:
(?:
(?:espresso\.)?repubblica
|lastampa
|ilsecoloxix
|huffingtonpost
)|
(?:
iltirreno
|messaggeroveneto
|ilpiccolo
|gazzettadimantova
|mattinopadova
|laprovinciapavese
|tribunatreviso
|nuovavenezia
|gazzettadimodena
|lanuovaferrara
|corrierealpi
|lasentinella
)\.gelocal
)\.it(?:/[^/]+){2,4}/(?P<id>\d+))(?:$|[?&].*)'''
_TESTS = [{
'url': 'https://video.lastampa.it/politica/il-paradosso-delle-regionali-la-lega-vince-ma-sembra-aver-perso/121559/121683',
'md5': '84658d7fb9e55a6e57ecc77b73137494',
'info_dict': {
'id': '121683',
'ext': 'mp4',
'title': 'Il paradosso delle Regionali: ecco perché la Lega vince ma sembra aver perso',
'description': 'md5:de7f4d6eaaaf36c153b599b10f8ce7ca',
'thumbnail': r're:^https://www\.repstatic\.it/video/photo/.+?-thumb-full-.+?\.jpg$',
'duration': 125,
},
}, {
'url': 'https://video.huffingtonpost.it/embed/politica/cotticelli-non-so-cosa-mi-sia-successo-sto-cercando-di-capire-se-ho-avuto-un-malore/29312/29276?responsive=true&el=video971040871621586700',
'only_matching': True,
}, {
'url': 'https://video.espresso.repubblica.it/embed/tutti-i-video/01-ted-villa/14772/14870&width=640&height=360',
'only_matching': True,
}, {
'url': 'https://video.repubblica.it/motori/record-della-pista-a-spa-francorchamps-la-pagani-huayra-roadster-bc-stupisce/367415/367963',
'only_matching': True,
}, {
'url': 'https://video.ilsecoloxix.it/sport/cassani-e-i-brividi-azzurri-ai-mondiali-di-imola-qui-mi-sono-innamorato-del-ciclismo-da-ragazzino-incredibile-tornarci-da-ct/66184/66267',
'only_matching': True,
}, {
'url': 'https://video.iltirreno.gelocal.it/sport/dentro-la-notizia-ferrari-cosa-succede-a-maranello/141059/142723',
'only_matching': True,
}, {
'url': 'https://video.messaggeroveneto.gelocal.it/locale/maria-giovanna-elmi-covid-vaccino/138155/139268',
'only_matching': True,
}, {
'url': 'https://video.ilpiccolo.gelocal.it/dossier/big-john/dinosauro-big-john-al-via-le-visite-guidate-a-trieste/135226/135751',
'only_matching': True,
}, {
'url': 'https://video.gazzettadimantova.gelocal.it/locale/dal-ponte-visconteo-di-valeggio-l-and-8217sos-dei-ristoratori-aprire-anche-a-cena/137310/137818',
'only_matching': True,
}, {
'url': 'https://video.mattinopadova.gelocal.it/dossier/coronavirus-in-veneto/covid-a-vo-un-anno-dopo-un-cuore-tricolore-per-non-dimenticare/138402/138964',
'only_matching': True,
}, {
'url': 'https://video.laprovinciapavese.gelocal.it/locale/mede-zona-rossa-via-alle-vaccinazioni-per-gli-over-80/137545/138120',
'only_matching': True,
}, {
'url': 'https://video.tribunatreviso.gelocal.it/dossier/coronavirus-in-veneto/ecco-le-prima-vaccinazioni-di-massa-nella-marca/134485/135024',
'only_matching': True,
}, {
'url': 'https://video.nuovavenezia.gelocal.it/locale/camion-troppo-alto-per-il-ponte-ferroviario-perde-il-carico/135734/136266',
'only_matching': True,
}, {
'url': 'https://video.gazzettadimodena.gelocal.it/locale/modena-scoperta-la-proteina-che-predice-il-livello-di-gravita-del-covid/139109/139796',
'only_matching': True,
}, {
'url': 'https://video.lanuovaferrara.gelocal.it/locale/due-bombole-di-gpl-aperte-e-abbandonate-i-vigili-bruciano-il-gas/134391/134957',
'only_matching': True,
}, {
'url': 'https://video.corrierealpi.gelocal.it/dossier/cortina-2021-i-mondiali-di-sci-alpino/mondiali-di-sci-il-timelapse-sulla-splendida-olympia/133760/134331',
'only_matching': True,
}, {
'url': 'https://video.lasentinella.gelocal.it/locale/vestigne-centra-un-auto-e-si-ribalta/138931/139466',
'only_matching': True,
}, {
'url': 'https://video.espresso.repubblica.it/tutti-i-video/01-ted-villa/14772',
'only_matching': True,
}]
@staticmethod
def _sanitize_urls(urls):
# add protocol if missing
for i, e in enumerate(urls):
if e.startswith('//'):
urls[i] = 'https:%s' % e
# clean iframes urls
for i, e in enumerate(urls):
urls[i] = urljoin(base_url(e), url_basename(e))
return urls
@staticmethod
def _extract_urls(webpage):
entries = [
mobj.group('eurl')
for mobj in re.finditer(r'''(?x)
(?:
data-frame-src=|
<iframe[^\n]+src=
)
(["'])(?P<eurl>%s)\1''' % GediDigitalIE._VALID_URL, webpage)]
return GediDigitalIE._sanitize_urls(entries)
@staticmethod
def _extract_url(webpage):
urls = GediDigitalIE._extract_urls(webpage)
return urls[0] if urls else None
@staticmethod
def _clean_formats(formats):
format_urls = set()
clean_formats = []
for f in formats:
if f['url'] not in format_urls:
if f.get('audio_ext') != 'none' and not f.get('acodec'):
continue
format_urls.add(f['url'])
clean_formats.append(f)
formats[:] = clean_formats
def _real_extract(self, url):
video_id = self._match_id(url)
url = re.match(self._VALID_URL, url).group('url')
webpage = self._download_webpage(url, video_id)
title = self._html_search_meta(
['twitter:title', 'og:title'], webpage, fatal=True)
player_data = re.findall(
r"PlayerFactory\.setParam\('(?P<type>format|param)',\s*'(?P<name>[^']+)',\s*'(?P<val>[^']+)'\);",
webpage)
formats = []
duration = thumb = None
for t, n, v in player_data:
if t == 'format':
if n in ('video-hds-vod-ec', 'video-hls-vod-ec', 'video-viralize', 'video-youtube-pfp'):
continue
elif n.endswith('-vod-ak'):
formats.extend(self._extract_akamai_formats(
v, video_id, {'http': 'media.gedidigital.it'}))
else:
ext = determine_ext(v)
if ext == 'm3u8':
formats.extend(self._extract_m3u8_formats(
v, video_id, 'mp4', 'm3u8_native', m3u8_id=n, fatal=False))
continue
f = {
'format_id': n,
'url': v,
}
if ext == 'mp3':
abr = int_or_none(self._search_regex(
r'-mp3-audio-(\d+)', v, 'abr', default=None))
f.update({
'abr': abr,
'tbr': abr,
'acodec': ext,
'vcodec': 'none'
})
else:
mobj = re.match(r'^video-rrtv-(\d+)(?:-(\d+))?$', n)
if mobj:
f.update({
'height': int(mobj.group(1)),
'vbr': int_or_none(mobj.group(2)),
})
if not f.get('vbr'):
f['vbr'] = int_or_none(self._search_regex(
r'-video-rrtv-(\d+)', v, 'abr', default=None))
formats.append(f)
elif t == 'param':
if n in ['image_full', 'image']:
thumb = v
elif n == 'videoDuration':
duration = int_or_none(v)
self._clean_formats(formats)
self._sort_formats(formats)
return {
'id': video_id,
'title': title,
'description': self._html_search_meta(
['twitter:description', 'og:description', 'description'], webpage),
'thumbnail': thumb or self._og_search_thumbnail(webpage),
'formats': formats,
'duration': duration,
}
+5 -20
View File
@@ -127,14 +127,13 @@ from .expressen import ExpressenIE
from .zype import ZypeIE from .zype import ZypeIE
from .odnoklassniki import OdnoklassnikiIE from .odnoklassniki import OdnoklassnikiIE
from .kinja import KinjaEmbedIE from .kinja import KinjaEmbedIE
from .gedidigital import GediDigitalIE from .gedi import GediEmbedsIE
from .rcs import RCSEmbedsIE from .rcs import RCSEmbedsIE
from .bitchute import BitChuteIE from .bitchute import BitChuteIE
from .rumble import RumbleEmbedIE from .rumble import RumbleEmbedIE
from .arcpublishing import ArcPublishingIE from .arcpublishing import ArcPublishingIE
from .medialaan import MedialaanIE from .medialaan import MedialaanIE
from .simplecast import SimplecastIE from .simplecast import SimplecastIE
from .wimtv import WimTVIE
class GenericIE(InfoExtractor): class GenericIE(InfoExtractor):
@@ -2251,15 +2250,6 @@ class GenericIE(InfoExtractor):
}, },
'playlist_mincount': 52, 'playlist_mincount': 52,
}, },
{
# WimTv embed player
'url': 'http://www.msmotor.tv/wearefmi-pt-2-2021/',
'info_dict': {
'id': 'wearefmi-pt-2-2021',
'title': '#WEAREFMI PT.2 2021 MsMotorTV',
},
'playlist_count': 1,
},
] ]
def report_following_redirect(self, new_url): def report_following_redirect(self, new_url):
@@ -2965,7 +2955,7 @@ class GenericIE(InfoExtractor):
webpage) webpage)
if not mobj: if not mobj:
mobj = re.search( mobj = re.search(
r'data-video-link=["\'](?P<url>http://m\.mlb\.com/video/[^"\']+)', r'data-video-link=["\'](?P<url>http://m.mlb.com/video/[^"\']+)',
webpage) webpage)
if mobj is not None: if mobj is not None:
return self.url_result(mobj.group('url'), 'MLB') return self.url_result(mobj.group('url'), 'MLB')
@@ -3349,22 +3339,17 @@ class GenericIE(InfoExtractor):
return self.playlist_from_matches( return self.playlist_from_matches(
zype_urls, video_id, video_title, ie=ZypeIE.ie_key()) zype_urls, video_id, video_title, ie=ZypeIE.ie_key())
gedi_urls = GediDigitalIE._extract_urls(webpage) # Look for RCS media group embeds
gedi_urls = GediEmbedsIE._extract_urls(webpage)
if gedi_urls: if gedi_urls:
return self.playlist_from_matches( return self.playlist_from_matches(
gedi_urls, video_id, video_title, ie=GediDigitalIE.ie_key()) gedi_urls, video_id, video_title, ie=GediEmbedsIE.ie_key())
# Look for RCS media group embeds
rcs_urls = RCSEmbedsIE._extract_urls(webpage) rcs_urls = RCSEmbedsIE._extract_urls(webpage)
if rcs_urls: if rcs_urls:
return self.playlist_from_matches( return self.playlist_from_matches(
rcs_urls, video_id, video_title, ie=RCSEmbedsIE.ie_key()) rcs_urls, video_id, video_title, ie=RCSEmbedsIE.ie_key())
wimtv_urls = WimTVIE._extract_urls(webpage)
if wimtv_urls:
return self.playlist_from_matches(
wimtv_urls, video_id, video_title, ie=WimTVIE.ie_key())
bitchute_urls = BitChuteIE._extract_urls(webpage) bitchute_urls = BitChuteIE._extract_urls(webpage)
if bitchute_urls: if bitchute_urls:
return self.playlist_from_matches( return self.playlist_from_matches(
+1 -1
View File
@@ -146,7 +146,7 @@ class IviIE(InfoExtractor):
expected=True) expected=True)
elif not pycryptodomex_found: elif not pycryptodomex_found:
raise ExtractorError( raise ExtractorError(
'pycryptodomex not found. Please install', 'pycryptodomex not found. Please install it.',
expected=True) expected=True)
elif message: elif message:
extractor_msg += ': ' + message extractor_msg += ': ' + message
+14 -53
View File
@@ -6,10 +6,8 @@ import json
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import ( from ..compat import (
compat_parse_qs,
compat_str, compat_str,
compat_urllib_parse_unquote, compat_urllib_parse_unquote,
compat_urllib_parse_urlparse,
) )
from ..utils import ( from ..utils import (
determine_ext, determine_ext,
@@ -23,9 +21,9 @@ from ..utils import (
class LBRYBaseIE(InfoExtractor): class LBRYBaseIE(InfoExtractor):
_BASE_URL_REGEX = r'(?:https?://(?:www\.)?(?:lbry\.tv|odysee\.com)/|lbry://)' _BASE_URL_REGEX = r'https?://(?:www\.)?(?:lbry\.tv|odysee\.com)/'
_CLAIM_ID_REGEX = r'[0-9a-f]{1,40}' _CLAIM_ID_REGEX = r'[0-9a-f]{1,40}'
_OPT_CLAIM_ID = '[^:/?#&]+(?:[:#]%s)?' % _CLAIM_ID_REGEX _OPT_CLAIM_ID = '[^:/?#&]+(?::%s)?' % _CLAIM_ID_REGEX
_SUPPORTED_STREAM_TYPES = ['video', 'audio'] _SUPPORTED_STREAM_TYPES = ['video', 'audio']
def _call_api_proxy(self, method, display_id, params, resource): def _call_api_proxy(self, method, display_id, params, resource):
@@ -43,9 +41,7 @@ class LBRYBaseIE(InfoExtractor):
'resolve', display_id, {'urls': url}, resource)[url] 'resolve', display_id, {'urls': url}, resource)[url]
def _permanent_url(self, url, claim_name, claim_id): def _permanent_url(self, url, claim_name, claim_id):
return urljoin( return urljoin(url, '/%s:%s' % (claim_name, claim_id))
url.replace('lbry://', 'https://lbry.tv/'),
'/%s:%s' % (claim_name, claim_id))
def _parse_stream(self, stream, url): def _parse_stream(self, stream, url):
stream_value = stream.get('value') or {} stream_value = stream.get('value') or {}
@@ -64,7 +60,6 @@ class LBRYBaseIE(InfoExtractor):
'description': stream_value.get('description'), 'description': stream_value.get('description'),
'license': stream_value.get('license'), 'license': stream_value.get('license'),
'timestamp': int_or_none(stream.get('timestamp')), 'timestamp': int_or_none(stream.get('timestamp')),
'release_timestamp': int_or_none(stream_value.get('release_time')),
'tags': stream_value.get('tags'), 'tags': stream_value.get('tags'),
'duration': int_or_none(media.get('duration')), 'duration': int_or_none(media.get('duration')),
'channel': try_get(signing_channel, lambda x: x['value']['title']), 'channel': try_get(signing_channel, lambda x: x['value']['title']),
@@ -97,8 +92,6 @@ class LBRYIE(LBRYBaseIE):
'description': 'md5:f6cb5c704b332d37f5119313c2c98f51', 'description': 'md5:f6cb5c704b332d37f5119313c2c98f51',
'timestamp': 1595694354, 'timestamp': 1595694354,
'upload_date': '20200725', 'upload_date': '20200725',
'release_timestamp': 1595340697,
'release_date': '20200721',
'width': 1280, 'width': 1280,
'height': 720, 'height': 720,
} }
@@ -113,8 +106,6 @@ class LBRYIE(LBRYBaseIE):
'description': 'md5:661ac4f1db09f31728931d7b88807a61', 'description': 'md5:661ac4f1db09f31728931d7b88807a61',
'timestamp': 1591312601, 'timestamp': 1591312601,
'upload_date': '20200604', 'upload_date': '20200604',
'release_timestamp': 1591312421,
'release_date': '20200604',
'tags': list, 'tags': list,
'duration': 2570, 'duration': 2570,
'channel': 'The LBRY Foundation', 'channel': 'The LBRY Foundation',
@@ -146,9 +137,6 @@ class LBRYIE(LBRYBaseIE):
}, { }, {
'url': 'https://lbry.tv/@lacajadepandora:a/TRUMP-EST%C3%81-BIEN-PUESTO-con-Pilar-Baselga,-Carlos-Senra,-Luis-Palacios-(720p_30fps_H264-192kbit_AAC):1', 'url': 'https://lbry.tv/@lacajadepandora:a/TRUMP-EST%C3%81-BIEN-PUESTO-con-Pilar-Baselga,-Carlos-Senra,-Luis-Palacios-(720p_30fps_H264-192kbit_AAC):1',
'only_matching': True, 'only_matching': True,
}, {
'url': 'lbry://@lbry#3f/odysee#7',
'only_matching': True,
}] }]
def _real_extract(self, url): def _real_extract(self, url):
@@ -178,7 +166,7 @@ class LBRYIE(LBRYBaseIE):
class LBRYChannelIE(LBRYBaseIE): class LBRYChannelIE(LBRYBaseIE):
IE_NAME = 'lbry:channel' IE_NAME = 'lbry:channel'
_VALID_URL = LBRYBaseIE._BASE_URL_REGEX + r'(?P<id>@%s)/?(?:[?&]|$)' % LBRYBaseIE._OPT_CLAIM_ID _VALID_URL = LBRYBaseIE._BASE_URL_REGEX + r'(?P<id>@%s)/?(?:[?#&]|$)' % LBRYBaseIE._OPT_CLAIM_ID
_TESTS = [{ _TESTS = [{
'url': 'https://lbry.tv/@LBRYFoundation:0', 'url': 'https://lbry.tv/@LBRYFoundation:0',
'info_dict': { 'info_dict': {
@@ -190,24 +178,20 @@ class LBRYChannelIE(LBRYBaseIE):
}, { }, {
'url': 'https://lbry.tv/@LBRYFoundation', 'url': 'https://lbry.tv/@LBRYFoundation',
'only_matching': True, 'only_matching': True,
}, {
'url': 'lbry://@lbry#3f',
'only_matching': True,
}] }]
_PAGE_SIZE = 50 _PAGE_SIZE = 50
def _fetch_page(self, claim_id, url, params, page): def _fetch_page(self, claim_id, url, page):
page += 1 page += 1
page_params = {
'channel_ids': [claim_id],
'claim_type': 'stream',
'no_totals': True,
'page': page,
'page_size': self._PAGE_SIZE,
}
page_params.update(params)
result = self._call_api_proxy( result = self._call_api_proxy(
'claim_search', claim_id, page_params, 'page %d' % page) 'claim_search', claim_id, {
'channel_ids': [claim_id],
'claim_type': 'stream',
'no_totals': True,
'page': page,
'page_size': self._PAGE_SIZE,
'stream_types': self._SUPPORTED_STREAM_TYPES,
}, 'page %d' % page)
for item in (result.get('items') or []): for item in (result.get('items') or []):
stream_claim_name = item.get('name') stream_claim_name = item.get('name')
stream_claim_id = item.get('claim_id') stream_claim_id = item.get('claim_id')
@@ -228,31 +212,8 @@ class LBRYChannelIE(LBRYBaseIE):
result = self._resolve_url( result = self._resolve_url(
'lbry://' + display_id, display_id, 'channel') 'lbry://' + display_id, display_id, 'channel')
claim_id = result['claim_id'] claim_id = result['claim_id']
qs = compat_parse_qs(compat_urllib_parse_urlparse(url).query)
content = qs.get('content', [None])[0]
params = {
'fee_amount': qs.get('fee_amount', ['>=0'])[0],
'order_by': {
'new': ['release_time'],
'top': ['effective_amount'],
'trending': ['trending_group', 'trending_mixed'],
}[qs.get('order', ['new'])[0]],
'stream_types': [content] if content in ['audio', 'video'] else self._SUPPORTED_STREAM_TYPES,
}
duration = qs.get('duration', [None])[0]
if duration:
params['duration'] = {
'long': '>=1200',
'short': '<=240',
}[duration]
language = qs.get('language', ['all'])[0]
if language != 'all':
languages = [language]
if language == 'en':
languages.append('none')
params['any_languages'] = languages
entries = OnDemandPagedList( entries = OnDemandPagedList(
functools.partial(self._fetch_page, claim_id, url, params), functools.partial(self._fetch_page, claim_id, url),
self._PAGE_SIZE) self._PAGE_SIZE)
result_value = result.get('value') or {} result_value = result.get('value') or {}
return self.playlist_result( return self.playlist_result(
+4 -14
View File
@@ -38,8 +38,8 @@ class LinuxAcademyIE(InfoExtractor):
'ext': 'mp4', 'ext': 'mp4',
'title': 'What Is Data Science', 'title': 'What Is Data Science',
'description': 'md5:c574a3c20607144fb36cb65bdde76c99', 'description': 'md5:c574a3c20607144fb36cb65bdde76c99',
'timestamp': int, # The timestamp and upload date changes 'timestamp': 1607387907,
'upload_date': r're:\d+', 'upload_date': '20201208',
'duration': 304, 'duration': 304,
}, },
'params': { 'params': {
@@ -59,16 +59,6 @@ class LinuxAcademyIE(InfoExtractor):
}, },
'playlist_count': 41, 'playlist_count': 41,
'skip': 'Requires Linux Academy account credentials', 'skip': 'Requires Linux Academy account credentials',
}, {
'url': 'https://linuxacademy.com/cp/modules/view/id/39',
'info_dict': {
'id': '39',
'title': 'Red Hat Certified Systems Administrator - RHCSA (EX200) Exam Prep (legacy)',
'description': 'md5:0f1d3369e90c3fb14a79813b863c902f',
'duration': 89280,
},
'playlist_count': 73,
'skip': 'Requires Linux Academy account credentials',
}] }]
_AUTHORIZE_URL = 'https://login.linuxacademy.com/authorize' _AUTHORIZE_URL = 'https://login.linuxacademy.com/authorize'
@@ -112,7 +102,7 @@ class LinuxAcademyIE(InfoExtractor):
'client_id': self._CLIENT_ID, 'client_id': self._CLIENT_ID,
'redirect_uri': self._ORIGIN_URL, 'redirect_uri': self._ORIGIN_URL,
'tenant': 'lacausers', 'tenant': 'lacausers',
'connection': 'Username-Password-ACG-Proxy', 'connection': 'Username-Password-Authentication',
'username': username, 'username': username,
'password': password, 'password': password,
'sso': 'true', 'sso': 'true',
@@ -172,7 +162,7 @@ class LinuxAcademyIE(InfoExtractor):
if course_id: if course_id:
module = self._parse_json( module = self._parse_json(
self._search_regex( self._search_regex(
r'window\.module\s*=\s*({(?:(?!};)[^"]|"([^"]|\\")*")+})\s*;', webpage, 'module'), r'window\.module\s*=\s*({.+?})\s*;', webpage, 'module'),
item_id) item_id)
entries = [] entries = []
chapter_number = None chapter_number = None
+21 -168
View File
@@ -1,91 +1,15 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import re from .nhl import NHLBaseIE
from .common import InfoExtractor
from ..utils import (
determine_ext,
int_or_none,
parse_duration,
parse_iso8601,
try_get,
)
class MLBBaseIE(InfoExtractor): class MLBIE(NHLBaseIE):
def _real_extract(self, url):
display_id = self._match_id(url)
video = self._download_video_data(display_id)
video_id = video['id']
title = video['title']
feed = self._get_feed(video)
formats = []
for playback in (feed.get('playbacks') or []):
playback_url = playback.get('url')
if not playback_url:
continue
name = playback.get('name')
ext = determine_ext(playback_url)
if ext == 'm3u8':
formats.extend(self._extract_m3u8_formats(
playback_url, video_id, 'mp4',
'm3u8_native', m3u8_id=name, fatal=False))
else:
f = {
'format_id': name,
'url': playback_url,
}
mobj = re.search(r'_(\d+)K_(\d+)X(\d+)', name)
if mobj:
f.update({
'height': int(mobj.group(3)),
'tbr': int(mobj.group(1)),
'width': int(mobj.group(2)),
})
mobj = re.search(r'_(\d+)x(\d+)_(\d+)_(\d+)K\.mp4', playback_url)
if mobj:
f.update({
'fps': int(mobj.group(3)),
'height': int(mobj.group(2)),
'tbr': int(mobj.group(4)),
'width': int(mobj.group(1)),
})
formats.append(f)
self._sort_formats(formats)
thumbnails = []
for cut in (try_get(feed, lambda x: x['image']['cuts'], list) or []):
src = cut.get('src')
if not src:
continue
thumbnails.append({
'height': int_or_none(cut.get('height')),
'url': src,
'width': int_or_none(cut.get('width')),
})
language = (video.get('language') or 'EN').lower()
return {
'id': video_id,
'title': title,
'formats': formats,
'description': video.get('description'),
'duration': parse_duration(feed.get('duration')),
'thumbnails': thumbnails,
'timestamp': parse_iso8601(video.get(self._TIMESTAMP_KEY)),
'subtitles': self._extract_mlb_subtitles(feed, language),
}
class MLBIE(MLBBaseIE):
_VALID_URL = r'''(?x) _VALID_URL = r'''(?x)
https?:// https?://
(?:[\da-z_-]+\.)*mlb\.com/ (?:[\da-z_-]+\.)*(?P<site>mlb)\.com/
(?: (?:
(?: (?:
(?:[^/]+/)*video/[^/]+/c-| (?:[^/]+/)*c-|
(?: (?:
shared/video/embed/(?:embed|m-internal-embed)\.html| shared/video/embed/(?:embed|m-internal-embed)\.html|
(?:[^/]+/)+(?:play|index)\.jsp| (?:[^/]+/)+(?:play|index)\.jsp|
@@ -94,6 +18,7 @@ class MLBIE(MLBBaseIE):
(?P<id>\d+) (?P<id>\d+)
) )
''' '''
_CONTENT_DOMAIN = 'content.mlb.com'
_TESTS = [ _TESTS = [
{ {
'url': 'https://www.mlb.com/mariners/video/ackleys-spectacular-catch/c-34698933', 'url': 'https://www.mlb.com/mariners/video/ackleys-spectacular-catch/c-34698933',
@@ -151,6 +76,18 @@ class MLBIE(MLBBaseIE):
'thumbnail': r're:^https?://.*\.jpg$', 'thumbnail': r're:^https?://.*\.jpg$',
}, },
}, },
{
'url': 'https://www.mlb.com/news/blue-jays-kevin-pillar-goes-spidey-up-the-wall-to-rob-tim-beckham-of-a-homer/c-118550098',
'md5': 'e09e37b552351fddbf4d9e699c924d68',
'info_dict': {
'id': '75609783',
'ext': 'mp4',
'title': 'Must C: Pillar climbs for catch',
'description': '4/15/15: Blue Jays outfielder Kevin Pillar continues his defensive dominance by climbing the wall in left to rob Tim Beckham of a home run',
'timestamp': 1429139220,
'upload_date': '20150415',
}
},
{ {
'url': 'https://www.mlb.com/video/hargrove-homers-off-caldwell/c-1352023483?tid=67793694', 'url': 'https://www.mlb.com/video/hargrove-homers-off-caldwell/c-1352023483?tid=67793694',
'only_matching': True, 'only_matching': True,
@@ -176,92 +113,8 @@ class MLBIE(MLBBaseIE):
'url': 'http://mlb.mlb.com/shared/video/embed/m-internal-embed.html?content_id=75609783&property=mlb&autoplay=true&hashmode=false&siteSection=mlb/multimedia/article_118550098/article_embed&club=mlb', 'url': 'http://mlb.mlb.com/shared/video/embed/m-internal-embed.html?content_id=75609783&property=mlb&autoplay=true&hashmode=false&siteSection=mlb/multimedia/article_118550098/article_embed&club=mlb',
'only_matching': True, 'only_matching': True,
}, },
] {
_TIMESTAMP_KEY = 'date' 'url': 'https://www.mlb.com/cut4/carlos-gomez-borrowed-sunglasses-from-an-as-fan/c-278912842',
'only_matching': True,
@staticmethod
def _get_feed(video):
return video
@staticmethod
def _extract_mlb_subtitles(feed, language):
subtitles = {}
for keyword in (feed.get('keywordsAll') or []):
keyword_type = keyword.get('type')
if keyword_type and keyword_type.startswith('closed_captions_location_'):
cc_location = keyword.get('value')
if cc_location:
subtitles.setdefault(language, []).append({
'url': cc_location,
})
return subtitles
def _download_video_data(self, display_id):
return self._download_json(
'http://content.mlb.com/mlb/item/id/v1/%s/details/web-v1.json' % display_id,
display_id)
class MLBVideoIE(MLBBaseIE):
_VALID_URL = r'https?://(?:www\.)?mlb\.com/(?:[^/]+/)*video/(?P<id>[^/?&#]+)'
_TEST = {
'url': 'https://www.mlb.com/mariners/video/ackley-s-spectacular-catch-c34698933',
'md5': '632358dacfceec06bad823b83d21df2d',
'info_dict': {
'id': 'c04a8863-f569-42e6-9f87-992393657614',
'ext': 'mp4',
'title': "Ackley's spectacular catch",
'description': 'md5:7f5a981eb4f3cbc8daf2aeffa2215bf0',
'duration': 66,
'timestamp': 1405995000,
'upload_date': '20140722',
'thumbnail': r're:^https?://.+',
},
}
_TIMESTAMP_KEY = 'timestamp'
@classmethod
def suitable(cls, url):
return False if MLBIE.suitable(url) else super(MLBVideoIE, cls).suitable(url)
@staticmethod
def _get_feed(video):
return video['feeds'][0]
@staticmethod
def _extract_mlb_subtitles(feed, language):
subtitles = {}
for cc_location in (feed.get('closedCaptions') or []):
subtitles.setdefault(language, []).append({
'url': cc_location,
})
def _download_video_data(self, display_id):
# https://www.mlb.com/data-service/en/videos/[SLUG]
return self._download_json(
'https://fastball-gateway.mlb.com/graphql',
display_id, query={
'query': '''{
mediaPlayback(ids: "%s") {
description
feeds(types: CMS) {
closedCaptions
duration
image {
cuts {
width
height
src
} }
} ]
playbacks {
name
url
}
}
id
timestamp
title
}
}''' % display_id,
})['data']['mediaPlayback'][0]
+1 -170
View File
@@ -14,7 +14,6 @@ from ..utils import (
fix_xml_ampersands, fix_xml_ampersands,
float_or_none, float_or_none,
HEADRequest, HEADRequest,
int_or_none,
RegexNotFoundError, RegexNotFoundError,
sanitized_Request, sanitized_Request,
strip_or_none, strip_or_none,
@@ -177,22 +176,6 @@ class MTVServicesInfoExtractor(InfoExtractor):
raise ExtractorError('Could not find video title') raise ExtractorError('Could not find video title')
title = title.strip() title = title.strip()
series = find_xpath_attr(
itemdoc, './/{http://search.yahoo.com/mrss/}category',
'scheme', 'urn:mtvn:franchise')
season = find_xpath_attr(
itemdoc, './/{http://search.yahoo.com/mrss/}category',
'scheme', 'urn:mtvn:seasonN')
episode = find_xpath_attr(
itemdoc, './/{http://search.yahoo.com/mrss/}category',
'scheme', 'urn:mtvn:episodeN')
series = series.text if series is not None else None
season = season.text if season is not None else None
episode = episode.text if episode is not None else None
if season and episode:
# episode number includes season, so remove it
episode = re.sub(r'^%s' % season, '', episode)
# This a short id that's used in the webpage urls # This a short id that's used in the webpage urls
mtvn_id = None mtvn_id = None
mtvn_id_node = find_xpath_attr(itemdoc, './/{http://search.yahoo.com/mrss/}category', mtvn_id_node = find_xpath_attr(itemdoc, './/{http://search.yahoo.com/mrss/}category',
@@ -218,9 +201,6 @@ class MTVServicesInfoExtractor(InfoExtractor):
'description': description, 'description': description,
'duration': float_or_none(content_el.attrib.get('duration')), 'duration': float_or_none(content_el.attrib.get('duration')),
'timestamp': timestamp, 'timestamp': timestamp,
'series': series,
'season_number': int_or_none(season),
'episode_number': int_or_none(episode),
} }
def _get_feed_query(self, uri): def _get_feed_query(self, uri):
@@ -340,7 +320,7 @@ class MTVServicesEmbeddedIE(MTVServicesInfoExtractor):
@staticmethod @staticmethod
def _extract_url(webpage): def _extract_url(webpage):
mobj = re.search( mobj = re.search(
r'<iframe[^>]+?src=(["\'])(?P<url>(?:https?:)?//media\.mtvnservices\.com/embed/.+?)\1', webpage) r'<iframe[^>]+?src=(["\'])(?P<url>(?:https?:)?//media.mtvnservices.com/embed/.+?)\1', webpage)
if mobj: if mobj:
return mobj.group('url') return mobj.group('url')
@@ -503,152 +483,3 @@ class MTVDEIE(MTVServicesInfoExtractor):
'arcEp': 'mtv.de', 'arcEp': 'mtv.de',
'mgid': uri, 'mgid': uri,
} }
class MTVItaliaIE(MTVServicesInfoExtractor):
IE_NAME = 'mtv.it'
_VALID_URL = r'https?://(?:www\.)?mtv\.it/(?:episodi|video|musica)/(?P<id>[0-9a-z]+)'
_TESTS = [{
'url': 'http://www.mtv.it/episodi/24bqab/mario-una-serie-di-maccio-capatonda-cavoli-amario-episodio-completo-S1-E1',
'info_dict': {
'id': '0f0fc78e-45fc-4cce-8f24-971c25477530',
'ext': 'mp4',
'title': 'Cavoli amario (episodio completo)',
'description': 'md5:4962bccea8fed5b7c03b295ae1340660',
'series': 'Mario - Una Serie Di Maccio Capatonda',
'season_number': 1,
'episode_number': 1,
},
'params': {
'skip_download': True,
},
}]
_GEO_COUNTRIES = ['IT']
_FEED_URL = 'http://feeds.mtvnservices.com/od/feed/intl-mrss-player-feed'
def _get_feed_query(self, uri):
return {
'arcEp': 'mtv.it',
'mgid': uri,
}
class MTVItaliaProgrammaIE(MTVItaliaIE):
IE_NAME = 'mtv.it:programma'
_VALID_URL = r'https?://(?:www\.)?mtv\.it/(?:programmi|playlist)/(?P<id>[0-9a-z]+)'
_TESTS = [{
# program page: general
'url': 'http://www.mtv.it/programmi/s2rppv/mario-una-serie-di-maccio-capatonda',
'info_dict': {
'id': 'a6f155bc-8220-4640-aa43-9b95f64ffa3d',
'title': 'Mario - Una Serie Di Maccio Capatonda',
'description': 'md5:72fbffe1f77ccf4e90757dd4e3216153',
},
'playlist_count': 2,
'params': {
'skip_download': True,
},
}, {
# program page: specific season
'url': 'http://www.mtv.it/programmi/d9ncjf/mario-una-serie-di-maccio-capatonda-S2',
'info_dict': {
'id': '4deeb5d8-f272-490c-bde2-ff8d261c6dd1',
'title': 'Mario - Una Serie Di Maccio Capatonda - Stagione 2',
},
'playlist_count': 34,
'params': {
'skip_download': True,
},
}, {
# playlist page + redirect
'url': 'http://www.mtv.it/playlist/sexy-videos/ilctal',
'info_dict': {
'id': 'dee8f9ee-756d-493b-bf37-16d1d2783359',
'title': 'Sexy Videos',
},
'playlist_mincount': 145,
'params': {
'skip_download': True,
},
}]
_GEO_COUNTRIES = ['IT']
_FEED_URL = 'http://www.mtv.it/feeds/triforce/manifest/v8'
def _get_entries(self, title, url):
while True:
pg = self._search_regex(r'/(\d+)$', url, 'entries', '1')
entries = self._download_json(url, title, 'page %s' % pg)
url = try_get(
entries, lambda x: x['result']['nextPageURL'], compat_str)
entries = try_get(
entries, (
lambda x: x['result']['data']['items'],
lambda x: x['result']['data']['seasons']),
list)
for entry in entries or []:
if entry.get('canonicalURL'):
yield self.url_result(entry['canonicalURL'])
if not url:
break
def _real_extract(self, url):
query = {'url': url}
info_url = update_url_query(self._FEED_URL, query)
video_id = self._match_id(url)
info = self._download_json(info_url, video_id).get('manifest')
redirect = try_get(
info, lambda x: x['newLocation']['url'], compat_str)
if redirect:
return self.url_result(redirect)
title = info.get('title')
video_id = try_get(
info, lambda x: x['reporting']['itemId'], compat_str)
parent_id = try_get(
info, lambda x: x['reporting']['parentId'], compat_str)
playlist_url = current_url = None
for z in (info.get('zones') or {}).values():
if z.get('moduleName') in ('INTL_M304', 'INTL_M209'):
info_url = z.get('feed')
if z.get('moduleName') in ('INTL_M308', 'INTL_M317'):
playlist_url = playlist_url or z.get('feed')
if z.get('moduleName') in ('INTL_M300',):
current_url = current_url or z.get('feed')
if not info_url:
raise ExtractorError('No info found')
if video_id == parent_id:
video_id = self._search_regex(
r'([^\/]+)/[^\/]+$', info_url, 'video_id')
info = self._download_json(info_url, video_id, 'Show infos')
info = try_get(info, lambda x: x['result']['data'], dict)
title = title or try_get(
info, (
lambda x: x['title'],
lambda x: x['headline']),
compat_str)
description = try_get(info, lambda x: x['content'], compat_str)
if current_url:
season = try_get(
self._download_json(playlist_url, video_id, 'Seasons info'),
lambda x: x['result']['data'], dict)
current = try_get(
season, lambda x: x['currentSeason'], compat_str)
seasons = try_get(
season, lambda x: x['seasons'], list) or []
if current in [s.get('eTitle') for s in seasons]:
playlist_url = current_url
title = re.sub(
r'[-|]\s*(?:mtv\s*italia|programma|playlist)',
'', title, flags=re.IGNORECASE).strip()
return self.playlist_result(
self._get_entries(title, playlist_url),
video_id, title, description)
+59 -83
View File
@@ -6,122 +6,98 @@ from .common import InfoExtractor
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
js_to_json, js_to_json,
qualities,
try_get,
url_or_none, url_or_none,
urljoin, urljoin,
) )
VALID_STREAMS = ('dash', )
class MxplayerIE(InfoExtractor): class MxplayerIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?mxplayer\.in/(?:show|movie)/(?:(?P<display_id>[-/a-z0-9]+)-)?(?P<id>[a-z0-9]+)' _VALID_URL = r'https?://(?:www\.)?mxplayer\.in/movie/(?P<slug>[a-z0-9]+(?:-[a-z0-9]+)*)'
_TESTS = [{ _TEST = {
'url': 'https://www.mxplayer.in/movie/watch-knock-knock-hindi-dubbed-movie-online-b9fa28df3bfb8758874735bbd7d2655a?watch=true', 'url': 'https://www.mxplayer.in/movie/watch-knock-knock-hindi-dubbed-movie-online-b9fa28df3bfb8758874735bbd7d2655a?watch=true',
'info_dict': { 'info_dict': {
'id': 'b9fa28df3bfb8758874735bbd7d2655a', 'id': 'b9fa28df3bfb8758874735bbd7d2655a',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Knock Knock (Hindi Dubbed)', 'title': 'Knock Knock Movie | Watch 2015 Knock Knock Full Movie Online- MX Player',
'description': 'md5:b195ba93ff1987309cfa58e2839d2a5b' 'description': 'md5:b195ba93ff1987309cfa58e2839d2a5b'
}, },
'params': { 'params': {
'skip_download': True, 'skip_download': True,
'format': 'bestvideo' 'format': 'bestvideo'
} }
}, { }
'url': 'https://www.mxplayer.in/show/watch-shaitaan/season-1/the-infamous-taxi-gang-of-meerut-online-45055d5bcff169ad48f2ad7552a83d6c',
'info_dict': { def _get_best_stream_url(self, stream):
'id': '45055d5bcff169ad48f2ad7552a83d6c', best_stream = list(filter(None, [v for k, v in stream.items()]))
'ext': 'm3u8', return best_stream.pop(0) if len(best_stream) else None
'title': 'The infamous taxi gang of Meerut',
'description': 'md5:033a0a7e3fd147be4fb7e07a01a3dc28',
'season': 'Season 1',
'series': 'Shaitaan'
},
'params': {
'skip_download': True,
}
}, {
'url': 'https://www.mxplayer.in/show/watch-aashram/chapter-1/duh-swapna-online-d445579792b0135598ba1bc9088a84cb',
'info_dict': {
'id': 'd445579792b0135598ba1bc9088a84cb',
'ext': 'mp4',
'title': 'Duh Swapna',
'description': 'md5:35ff39c4bdac403c53be1e16a04192d8',
'season': 'Chapter 1',
'series': 'Aashram'
},
'expected_warnings': ['Unknown MIME type application/mp4 in DASH manifest'],
'params': {
'skip_download': True,
'format': 'bestvideo'
}
}]
def _get_stream_urls(self, video_dict): def _get_stream_urls(self, video_dict):
stream_provider_dict = try_get( stream_dict = video_dict.get('stream', {'provider': {}})
video_dict, stream_provider = stream_dict.get('provider')
lambda x: x['stream'][x['stream']['provider']])
if not stream_provider_dict:
raise ExtractorError('No stream provider found', expected=True)
for stream_name, stream in stream_provider_dict.items(): if not stream_dict[stream_provider]:
if stream_name in ('hls', 'dash', 'hlsUrl', 'dashUrl'): message = 'No stream provider found'
stream_type = stream_name.replace('Url', '') raise ExtractorError('%s said: %s' % (self.IE_NAME, message), expected=True)
if isinstance(stream, dict):
for quality, stream_url in stream.items(): streams = []
if stream_url: for stream_name, v in stream_dict[stream_provider].items():
yield stream_type, quality, stream_url if stream_name in VALID_STREAMS:
else: stream_url = self._get_best_stream_url(v)
yield stream_type, 'base', stream if stream_url is None:
continue
streams.append((stream_name, stream_url))
return streams
def _real_extract(self, url): def _real_extract(self, url):
display_id, video_id = re.match(self._VALID_URL, url).groups() mobj = re.match(self._VALID_URL, url)
video_slug = mobj.group('slug')
video_id = video_slug.split('-')[-1]
webpage = self._download_webpage(url, video_id) webpage = self._download_webpage(url, video_id)
source = self._parse_json( window_state_json = self._html_search_regex(
js_to_json(self._html_search_regex( r'(?s)<script>window\.state\s*[:=]\s(\{.+\})\n(\w+).*(</script>).*',
r'(?s)<script>window\.state\s*[:=]\s(\{.+\})\n(\w+).*(</script>).*', webpage, 'WindowState')
webpage, 'WindowState')),
video_id) source = self._parse_json(js_to_json(window_state_json), video_id)
if not source: if not source:
raise ExtractorError('Cannot find source', expected=True) raise ExtractorError('Cannot find source', expected=True)
config_dict = source['config'] config_dict = source['config']
video_dict = source['entities'][video_id] video_dict = source['entities'][video_id]
stream_urls = self._get_stream_urls(video_dict)
thumbnails = [] title = self._og_search_title(webpage, fatal=True, default=video_dict['title'])
for i in video_dict.get('imageInfo') or []:
thumbnails.append({
'url': urljoin(config_dict['imageBaseUrl'], i['url']),
'width': i['width'],
'height': i['height'],
})
formats = [] formats = []
get_quality = qualities(['main', 'base', 'high']) headers = {'Referer': url}
for stream_type, quality, stream_url in self._get_stream_urls(video_dict): for stream_name, stream_url in stream_urls:
format_url = url_or_none(urljoin(config_dict['videoCdnBaseUrl'], stream_url)) if stream_name == 'dash':
if not format_url: format_url = url_or_none(urljoin(config_dict['videoCdnBaseUrl'], stream_url))
continue if not format_url:
if stream_type == 'dash': continue
dash_formats = self._extract_mpd_formats( formats.extend(self._extract_mpd_formats(
format_url, video_id, mpd_id='dash-%s' % quality, headers={'Referer': url}) format_url, video_id, mpd_id='dash', headers=headers))
for frmt in dash_formats:
frmt['quality'] = get_quality(quality)
formats.extend(dash_formats)
elif stream_type == 'hls':
formats.extend(self._extract_m3u8_formats(
format_url, video_id, fatal=False,
m3u8_id='hls-%s' % quality, quality=get_quality(quality)))
self._sort_formats(formats) self._sort_formats(formats)
return { info = {
'id': video_id, 'id': video_id,
'display_id': display_id.replace('/', '-'), 'title': title,
'title': video_dict['title'] or self._og_search_title(webpage),
'formats': formats,
'description': video_dict.get('description'), 'description': video_dict.get('description'),
'season': try_get(video_dict, lambda x: x['container']['title']), 'formats': formats
'series': try_get(video_dict, lambda x: x['container']['container']['title']),
'thumbnails': thumbnails,
} }
if video_dict.get('imageInfo'):
info['thumbnails'] = list(map(lambda i: dict(i, **{
'url': urljoin(config_dict['imageBaseUrl'], i['url'])
}), video_dict['imageInfo']))
if video_dict.get('webUrl'):
last_part = video_dict['webUrl'].split("/")[-1]
info['display_id'] = last_part.replace(video_id, "").rstrip("-")
return info
+9 -34
View File
@@ -10,7 +10,6 @@ from .adobepass import AdobePassIE
from ..compat import compat_urllib_parse_unquote from ..compat import compat_urllib_parse_unquote
from ..utils import ( from ..utils import (
int_or_none, int_or_none,
parse_age_limit,
parse_duration, parse_duration,
smuggle_url, smuggle_url,
try_get, try_get,
@@ -19,7 +18,7 @@ from ..utils import (
) )
class NBCIE(ThePlatformIE): class NBCIE(AdobePassIE):
_VALID_URL = r'https?(?P<permalink>://(?:www\.)?nbc\.com/(?:classic-tv/)?[^/]+/video/[^/]+/(?P<id>n?\d+))' _VALID_URL = r'https?(?P<permalink>://(?:www\.)?nbc\.com/(?:classic-tv/)?[^/]+/video/[^/]+/(?P<id>n?\d+))'
_TESTS = [ _TESTS = [
@@ -133,9 +132,7 @@ class NBCIE(ThePlatformIE):
'manifest': 'm3u', 'manifest': 'm3u',
} }
video_id = video_data['mpxGuid'] video_id = video_data['mpxGuid']
tp_path = 'NnzsPC/media/guid/%s/%s' % (video_data.get('mpxAccountId') or '2410887629', video_id) title = video_data['secondaryTitle']
tpm = self._download_theplatform_metadata(tp_path, video_id)
title = tpm.get('title') or video_data.get('secondaryTitle')
if video_data.get('locked'): if video_data.get('locked'):
resource = self._get_mvpd_resource( resource = self._get_mvpd_resource(
video_data.get('resourceId') or 'nbcentertainment', video_data.get('resourceId') or 'nbcentertainment',
@@ -145,40 +142,18 @@ class NBCIE(ThePlatformIE):
theplatform_url = smuggle_url(update_url_query( theplatform_url = smuggle_url(update_url_query(
'http://link.theplatform.com/s/NnzsPC/media/guid/%s/%s' % (video_data.get('mpxAccountId') or '2410887629', video_id), 'http://link.theplatform.com/s/NnzsPC/media/guid/%s/%s' % (video_data.get('mpxAccountId') or '2410887629', video_id),
query), {'force_smil_url': True}) query), {'force_smil_url': True})
# Empty string or 0 can be valid values for these. So the check must be `is None`
description = video_data.get('description')
if description is None:
description = tpm.get('description')
episode_number = int_or_none(video_data.get('episodeNumber'))
if episode_number is None:
episode_number = int_or_none(tpm.get('nbcu$airOrder'))
rating = video_data.get('rating')
if rating is None:
try_get(tpm, lambda x: x['ratings'][0]['rating'])
season_number = int_or_none(video_data.get('seasonNumber'))
if season_number is None:
season_number = int_or_none(tpm.get('nbcu$seasonNumber'))
series = video_data.get('seriesShortTitle')
if series is None:
series = tpm.get('nbcu$seriesShortTitle')
tags = video_data.get('keywords')
if tags is None or len(tags) == 0:
tags = tpm.get('keywords')
return { return {
'_type': 'url_transparent', '_type': 'url_transparent',
'age_limit': parse_age_limit(rating),
'description': description,
'episode': title,
'episode_number': episode_number,
'id': video_id, 'id': video_id,
'ie_key': 'ThePlatform',
'season_number': season_number,
'series': series,
'tags': tags,
'title': title, 'title': title,
'url': theplatform_url, 'url': theplatform_url,
'description': video_data.get('description'),
'tags': video_data.get('keywords'),
'season_number': int_or_none(video_data.get('seasonNumber')),
'episode_number': int_or_none(video_data.get('episodeNumber')),
'episode': title,
'series': video_data.get('seriesShortTitle'),
'ie_key': 'ThePlatform',
} }
+93 -140
View File
@@ -8,7 +8,6 @@ import datetime
from .common import InfoExtractor from .common import InfoExtractor
from ..postprocessor.ffmpeg import FFmpegPostProcessor from ..postprocessor.ffmpeg import FFmpegPostProcessor
from ..compat import ( from ..compat import (
compat_str,
compat_parse_qs, compat_parse_qs,
compat_urllib_parse_urlparse, compat_urllib_parse_urlparse,
) )
@@ -21,7 +20,6 @@ from ..utils import (
parse_duration, parse_duration,
parse_iso8601, parse_iso8601,
PostProcessingError, PostProcessingError,
str_or_none,
remove_start, remove_start,
try_get, try_get,
unified_timestamp, unified_timestamp,
@@ -36,7 +34,7 @@ class NiconicoIE(InfoExtractor):
_TESTS = [{ _TESTS = [{
'url': 'http://www.nicovideo.jp/watch/sm22312215', 'url': 'http://www.nicovideo.jp/watch/sm22312215',
'md5': 'a5bad06f1347452102953f323c69da34s', 'md5': 'd1a75c0823e2f629128c43e1212760f9',
'info_dict': { 'info_dict': {
'id': 'sm22312215', 'id': 'sm22312215',
'ext': 'mp4', 'ext': 'mp4',
@@ -205,7 +203,7 @@ class NiconicoIE(InfoExtractor):
'data-api-data="([^"]+)"', webpage, 'data-api-data="([^"]+)"', webpage,
'API data', default='{}'), video_id) 'API data', default='{}'), video_id)
session_api_data = try_get(api_data, lambda x: x['media']['delivery']['movie']['session']) session_api_data = try_get(api_data, lambda x: x['video']['dmcInfo']['session_api'])
session_api_endpoint = try_get(session_api_data, lambda x: x['urls'][0]) session_api_endpoint = try_get(session_api_data, lambda x: x['urls'][0])
# ping # ping
@@ -222,7 +220,7 @@ class NiconicoIE(InfoExtractor):
yesno = lambda x: 'yes' if x else 'no' yesno = lambda x: 'yes' if x else 'no'
# m3u8 (encryption) # m3u8 (encryption)
if 'encryption' in (try_get(api_data, lambda x: x['media']['delivery']['movie']) or {}): if 'encryption' in try_get(api_data, lambda x: x['video']['dmcInfo']) or {}:
protocol = 'm3u8' protocol = 'm3u8'
session_api_http_parameters = { session_api_http_parameters = {
'parameters': { 'parameters': {
@@ -246,8 +244,8 @@ class NiconicoIE(InfoExtractor):
session_api_http_parameters = { session_api_http_parameters = {
'parameters': { 'parameters': {
'http_output_download_parameters': { 'http_output_download_parameters': {
'use_ssl': yesno(session_api_endpoint['isSsl']), 'use_ssl': yesno(session_api_endpoint['is_ssl']),
'use_well_known_port': yesno(session_api_endpoint['isWellKnownPort']), 'use_well_known_port': yesno(session_api_endpoint['is_well_known_port']),
} }
} }
} }
@@ -260,15 +258,15 @@ class NiconicoIE(InfoExtractor):
data=json.dumps({ data=json.dumps({
'session': { 'session': {
'client_info': { 'client_info': {
'player_id': session_api_data.get('playerId'), 'player_id': session_api_data.get('player_id'),
}, },
'content_auth': { 'content_auth': {
'auth_type': try_get(session_api_data, lambda x: x['authTypes'][session_api_data['protocols'][0]]), 'auth_type': try_get(session_api_data, lambda x: x['auth_types'][session_api_data['protocols'][0]]),
'content_key_timeout': session_api_data.get('contentKeyTimeout'), 'content_key_timeout': session_api_data.get('content_key_timeout'),
'service_id': 'nicovideo', 'service_id': 'nicovideo',
'service_user_id': session_api_data.get('serviceUserId') 'service_user_id': session_api_data.get('service_user_id')
}, },
'content_id': session_api_data.get('contentId'), 'content_id': session_api_data.get('content_id'),
'content_src_id_sets': [{ 'content_src_id_sets': [{
'content_src_ids': [{ 'content_src_ids': [{
'src_id_to_mux': { 'src_id_to_mux': {
@@ -281,7 +279,7 @@ class NiconicoIE(InfoExtractor):
'content_uri': '', 'content_uri': '',
'keep_method': { 'keep_method': {
'heartbeat': { 'heartbeat': {
'lifetime': session_api_data.get('heartbeatLifetime') 'lifetime': session_api_data.get('heartbeat_lifetime')
} }
}, },
'priority': session_api_data.get('priority'), 'priority': session_api_data.get('priority'),
@@ -291,7 +289,7 @@ class NiconicoIE(InfoExtractor):
'http_parameters': session_api_http_parameters 'http_parameters': session_api_http_parameters
} }
}, },
'recipe_id': session_api_data.get('recipeId'), 'recipe_id': session_api_data.get('recipe_id'),
'session_operation_auth': { 'session_operation_auth': {
'session_operation_auth_by_signature': { 'session_operation_auth_by_signature': {
'signature': session_api_data.get('signature'), 'signature': session_api_data.get('signature'),
@@ -310,7 +308,7 @@ class NiconicoIE(InfoExtractor):
'url': session_api_endpoint['url'] + '/' + session_response['data']['session']['id'] + '?_format=json&_method=PUT', 'url': session_api_endpoint['url'] + '/' + session_response['data']['session']['id'] + '?_format=json&_method=PUT',
'data': json.dumps(session_response['data']), 'data': json.dumps(session_response['data']),
# interval, convert milliseconds to seconds, then halve to make a buffer. # interval, convert milliseconds to seconds, then halve to make a buffer.
'interval': float_or_none(session_api_data.get('heartbeatLifetime'), scale=2000), 'interval': float_or_none(session_api_data.get('heartbeat_lifetime'), scale=2000),
} }
return info_dict, heartbeat_info_dict return info_dict, heartbeat_info_dict
@@ -329,17 +327,15 @@ class NiconicoIE(InfoExtractor):
format_id = '-'.join(map(lambda s: remove_start(s['id'], 'archive_'), [video_quality, audio_quality])) format_id = '-'.join(map(lambda s: remove_start(s['id'], 'archive_'), [video_quality, audio_quality]))
vdict = parse_format_id(video_quality['id']) vdict = parse_format_id(video_quality['id'])
adict = parse_format_id(audio_quality['id']) adict = parse_format_id(audio_quality['id'])
resolution = try_get(video_quality, lambda x: x['metadata']['resolution'], dict) or {'height': vdict.get('res')} resolution = video_quality.get('resolution', {'height': vdict.get('res')})
vbr = try_get(video_quality, lambda x: x['metadata']['bitrate'], float)
return { return {
'url': '%s:%s/%s/%s' % (protocol, video_id, video_quality['id'], audio_quality['id']), 'url': '%s:%s/%s/%s' % (protocol, video_id, video_quality['id'], audio_quality['id']),
'format_id': format_id, 'format_id': format_id,
'format_note': 'DMC %s' % try_get(video_quality, lambda x: x['metadata']['label'], compat_str),
'ext': 'mp4', # Session API are used in HTML5, which always serves mp4 'ext': 'mp4', # Session API are used in HTML5, which always serves mp4
'vcodec': vdict.get('codec'), 'vcodec': vdict.get('codec'),
'acodec': adict.get('codec'), 'acodec': adict.get('codec'),
'vbr': float_or_none(vbr, 1000) or float_or_none(vdict.get('br')), 'vbr': float_or_none(video_quality.get('bitrate'), 1000) or float_or_none(vdict.get('br')),
'abr': float_or_none(audio_quality.get('bitrate'), 1000) or float_or_none(adict.get('br')), 'abr': float_or_none(audio_quality.get('bitrate'), 1000) or float_or_none(adict.get('br')),
'height': int_or_none(resolution.get('height', vdict.get('res'))), 'height': int_or_none(resolution.get('height', vdict.get('res'))),
'width': int_or_none(resolution.get('width')), 'width': int_or_none(resolution.get('width')),
@@ -398,93 +394,92 @@ class NiconicoIE(InfoExtractor):
formats = [] formats = []
# Get HTML5 videos info # Get HTML5 videos info
quality_info = try_get(api_data, lambda x: x['media']['delivery']['movie']) try:
if not quality_info: dmc_info = api_data['video']['dmcInfo']
raise ExtractorError('The video can\'t downloaded.', expected=True) except KeyError:
raise ExtractorError('The video can\'t downloaded.',
expected=True)
quality_info = dmc_info.get('quality')
for audio_quality in quality_info.get('audios') or {}: for audio_quality in quality_info.get('audios') or {}:
for video_quality in quality_info.get('videos') or {}: for video_quality in quality_info.get('videos') or {}:
if not audio_quality.get('isAvailable') or not video_quality.get('isAvailable'): if not audio_quality.get('available') or not video_quality.get('available'):
continue continue
formats.append(self._extract_format_for_quality( formats.append(self._extract_format_for_quality(
api_data, video_id, audio_quality, video_quality)) api_data, video_id, audio_quality, video_quality))
# Get flv/swf info # Get flv/swf info
timestamp = None
video_real_url = try_get(api_data, lambda x: x['video']['smileInfo']['url']) video_real_url = try_get(api_data, lambda x: x['video']['smileInfo']['url'])
if not video_real_url: is_economy = video_real_url.endswith('low')
self.report_warning('Unable to obtain smile video information')
else:
is_economy = video_real_url.endswith('low')
if is_economy: if is_economy:
self.report_warning('Site is currently in economy mode! You will only have access to lower quality streams') self.report_warning('Site is currently in economy mode! You will only have access to lower quality streams')
# Invoking ffprobe to determine resolution # Invoking ffprobe to determine resolution
pp = FFmpegPostProcessor(self._downloader) pp = FFmpegPostProcessor(self._downloader)
cookies = self._get_cookies('https://nicovideo.jp').output(header='', sep='; path=/; domain=nicovideo.jp;\n') cookies = self._get_cookies('https://nicovideo.jp').output(header='', sep='; path=/; domain=nicovideo.jp;\n')
self.to_screen('%s: %s' % (video_id, 'Checking smile format with ffprobe')) self.to_screen('%s: %s' % (video_id, 'Checking smile format with ffprobe'))
try: try:
metadata = pp.get_metadata_object(video_real_url, ['-cookies', cookies]) metadata = pp.get_metadata_object(video_real_url, ['-cookies', cookies])
except PostProcessingError as err: except PostProcessingError as err:
raise ExtractorError(err.msg, expected=True) raise ExtractorError(err.msg, expected=True)
v_stream = a_stream = {} v_stream = a_stream = {}
# Some complex swf files doesn't have video stream (e.g. nm4809023) # Some complex swf files doesn't have video stream (e.g. nm4809023)
for stream in metadata['streams']: for stream in metadata['streams']:
if stream['codec_type'] == 'video': if stream['codec_type'] == 'video':
v_stream = stream v_stream = stream
elif stream['codec_type'] == 'audio': elif stream['codec_type'] == 'audio':
a_stream = stream a_stream = stream
# Community restricted videos seem to have issues with the thumb API not returning anything at all # Community restricted videos seem to have issues with the thumb API not returning anything at all
filesize = int( filesize = int(
(get_video_info_xml('size_high') if not is_economy else get_video_info_xml('size_low')) (get_video_info_xml('size_high') if not is_economy else get_video_info_xml('size_low'))
or metadata['format']['size'] or metadata['format']['size']
) )
extension = ( extension = (
get_video_info_xml('movie_type') get_video_info_xml('movie_type')
or 'mp4' if 'mp4' in metadata['format']['format_name'] else metadata['format']['format_name'] or 'mp4' if 'mp4' in metadata['format']['format_name'] else metadata['format']['format_name']
) )
# 'creation_time' tag on video stream of re-encoded SMILEVIDEO mp4 files are '1970-01-01T00:00:00.000000Z'. # 'creation_time' tag on video stream of re-encoded SMILEVIDEO mp4 files are '1970-01-01T00:00:00.000000Z'.
timestamp = ( timestamp = (
parse_iso8601(get_video_info_web('first_retrieve')) parse_iso8601(get_video_info_web('first_retrieve'))
or unified_timestamp(get_video_info_web('postedDateTime')) or unified_timestamp(get_video_info_web('postedDateTime'))
) )
metadata_timestamp = ( metadata_timestamp = (
parse_iso8601(try_get(v_stream, lambda x: x['tags']['creation_time'])) parse_iso8601(try_get(v_stream, lambda x: x['tags']['creation_time']))
or timestamp if extension != 'mp4' else 0 or timestamp if extension != 'mp4' else 0
) )
# According to compconf, smile videos from pre-2017 are always better quality than their DMC counterparts # According to compconf, smile videos from pre-2017 are always better quality than their DMC counterparts
smile_threshold_timestamp = parse_iso8601('2016-12-08T00:00:00+09:00') smile_threshold_timestamp = parse_iso8601('2016-12-08T00:00:00+09:00')
is_source = timestamp < smile_threshold_timestamp or metadata_timestamp > 0 is_source = timestamp < smile_threshold_timestamp or metadata_timestamp > 0
# If movie file size is unstable, old server movie is not source movie. # If movie file size is unstable, old server movie is not source movie.
if filesize > 1: if filesize > 1:
formats.append({ formats.append({
'url': video_real_url, 'url': video_real_url,
'format_id': 'smile' if not is_economy else 'smile_low', 'format_id': 'smile' if not is_economy else 'smile_low',
'format_note': 'SMILEVIDEO source' if not is_economy else 'SMILEVIDEO low quality', 'format_note': 'SMILEVIDEO source' if not is_economy else 'SMILEVIDEO low quality',
'ext': extension, 'ext': extension,
'container': extension, 'container': extension,
'vcodec': v_stream.get('codec_name'), 'vcodec': v_stream.get('codec_name'),
'acodec': a_stream.get('codec_name'), 'acodec': a_stream.get('codec_name'),
# Some complex swf files doesn't have total bit rate metadata (e.g. nm6049209) # Some complex swf files doesn't have total bit rate metadata (e.g. nm6049209)
'tbr': int_or_none(metadata['format'].get('bit_rate'), scale=1000), 'tbr': int_or_none(metadata['format'].get('bit_rate'), scale=1000),
'vbr': int_or_none(v_stream.get('bit_rate'), scale=1000), 'vbr': int_or_none(v_stream.get('bit_rate'), scale=1000),
'abr': int_or_none(a_stream.get('bit_rate'), scale=1000), 'abr': int_or_none(a_stream.get('bit_rate'), scale=1000),
'height': int_or_none(v_stream.get('height')), 'height': int_or_none(v_stream.get('height')),
'width': int_or_none(v_stream.get('width')), 'width': int_or_none(v_stream.get('width')),
'source_preference': 5 if not is_economy else -2, 'source_preference': 5 if not is_economy else -2,
'quality': 5 if is_source and not is_economy else None, 'quality': 5 if is_source and not is_economy else None,
'filesize': filesize 'filesize': filesize
}) })
if len(formats) == 0: if len(formats) == 0:
raise ExtractorError('Unable to find video info.') raise ExtractorError('Unable to find video info.')
@@ -492,12 +487,13 @@ class NiconicoIE(InfoExtractor):
self._sort_formats(formats) self._sort_formats(formats)
# Start extracting information # Start extracting information
title = ( title = get_video_info_web('originalTitle')
get_video_info_web(['originalTitle', 'title']) if not title:
or self._og_search_title(webpage, default=None) title = self._og_search_title(webpage, default=None)
or self._html_search_regex( if not title:
title = self._html_search_regex(
r'<span[^>]+class="videoHeaderTitle"[^>]*>([^<]+)</span>', r'<span[^>]+class="videoHeaderTitle"[^>]*>([^<]+)</span>',
webpage, 'video title')) webpage, 'video title')
watch_api_data_string = self._html_search_regex( watch_api_data_string = self._html_search_regex(
r'<div[^>]+id="watchAPIDataContainer"[^>]+>([^<]+)</div>', r'<div[^>]+id="watchAPIDataContainer"[^>]+>([^<]+)</div>',
@@ -521,7 +517,6 @@ class NiconicoIE(InfoExtractor):
timestamp = parse_iso8601( timestamp = parse_iso8601(
video_detail['postedAt'].replace('/', '-'), video_detail['postedAt'].replace('/', '-'),
delimiter=' ', timezone=datetime.timedelta(hours=9)) delimiter=' ', timezone=datetime.timedelta(hours=9))
timestamp = timestamp or try_get(api_data, lambda x: parse_iso8601(x['video']['registeredAt']))
view_count = int_or_none(get_video_info_web(['view_counter', 'viewCount'])) view_count = int_or_none(get_video_info_web(['view_counter', 'viewCount']))
if not view_count: if not view_count:
@@ -530,16 +525,11 @@ class NiconicoIE(InfoExtractor):
webpage, 'view count', default=None) webpage, 'view count', default=None)
if match: if match:
view_count = int_or_none(match.replace(',', '')) view_count = int_or_none(match.replace(',', ''))
view_count = ( view_count = view_count or video_detail.get('viewCount')
view_count
or video_detail.get('viewCount')
or try_get(api_data, lambda x: x['video']['count']['view']))
comment_count = (
int_or_none(get_video_info_web('comment_num'))
or video_detail.get('commentCount')
or try_get(api_data, lambda x: x['video']['count']['comment']))
comment_count = (int_or_none(get_video_info_web('comment_num'))
or video_detail.get('commentCount')
or try_get(api_data, lambda x: x['thread']['commentCount']))
if not comment_count: if not comment_count:
match = self._html_search_regex( match = self._html_search_regex(
r'>Comments: <strong[^>]*>([^<]+)</strong>', r'>Comments: <strong[^>]*>([^<]+)</strong>',
@@ -569,7 +559,7 @@ class NiconicoIE(InfoExtractor):
# Note: cannot use api_data.get('owner', {}) because owner may be set to "null" # Note: cannot use api_data.get('owner', {}) because owner may be set to "null"
# in the JSON, which will cause None to be returned instead of {}. # in the JSON, which will cause None to be returned instead of {}.
owner = try_get(api_data, lambda x: x.get('owner'), dict) or {} owner = try_get(api_data, lambda x: x.get('owner'), dict) or {}
uploader_id = str_or_none( uploader_id = (
get_video_info_web(['ch_id', 'user_id']) get_video_info_web(['ch_id', 'user_id'])
or owner.get('id') or owner.get('id')
or channel_id or channel_id
@@ -599,7 +589,7 @@ class NiconicoIE(InfoExtractor):
class NiconicoPlaylistIE(InfoExtractor): class NiconicoPlaylistIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?nicovideo\.jp/(?:user/\d+/|my/)?mylist/(?P<id>\d+)' _VALID_URL = r'https?://(?:www\.)?nicovideo\.jp/(?:user/\d+/)?mylist/(?P<id>\d+)'
_TESTS = [{ _TESTS = [{
'url': 'http://www.nicovideo.jp/mylist/27411728', 'url': 'http://www.nicovideo.jp/mylist/27411728',
@@ -657,40 +647,3 @@ class NiconicoPlaylistIE(InfoExtractor):
'uploader_id': uploader_id, 'uploader_id': uploader_id,
'entries': OnDemandPagedList(pagefunc, 25), 'entries': OnDemandPagedList(pagefunc, 25),
} }
class NiconicoUserIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?nicovideo\.jp/user/(?P<id>\d+)/?(?:$|[#?])'
_TEST = {
'url': 'https://www.nicovideo.jp/user/419948',
'info_dict': {
'id': '419948',
},
'playlist_mincount': 101,
}
_API_URL = "https://nvapi.nicovideo.jp/v1/users/%s/videos?sortKey=registeredAt&sortOrder=desc&pageSize=%s&page=%s"
_api_headers = {
'X-Frontend-ID': '6',
'X-Frontend-Version': '0',
'X-Niconico-Language': 'en-us'
}
_PAGE_SIZE = 100
def _entries(self, list_id, ):
total_count = 1
count = page_num = 0
while count < total_count:
json_parsed = self._download_json(
self._API_URL % (list_id, self._PAGE_SIZE, page_num + 1), list_id,
headers=self._api_headers,
note='Downloading JSON metadata%s' % (' page %d' % page_num if page_num else ''))
if not page_num:
total_count = int_or_none(json_parsed['data'].get('totalCount'))
for entry in json_parsed["data"]["items"]:
count += 1
yield self.url_result('https://www.nicovideo.jp/watch/%s' % entry['id'])
page_num += 1
def _real_extract(self, url):
list_id = self._match_id(url)
return self.playlist_result(self._entries(list_id), list_id, ie=NiconicoIE.ie_key())
+2 -5
View File
@@ -599,13 +599,11 @@ class PeerTubeIE(InfoExtractor):
else: else:
age_limit = None age_limit = None
webpage_url = 'https://%s/videos/watch/%s' % (host, video_id)
return { return {
'id': video_id, 'id': video_id,
'title': title, 'title': title,
'description': description, 'description': description,
'thumbnail': urljoin(webpage_url, video.get('thumbnailPath')), 'thumbnail': urljoin(url, video.get('thumbnailPath')),
'timestamp': unified_timestamp(video.get('publishedAt')), 'timestamp': unified_timestamp(video.get('publishedAt')),
'uploader': account_data('displayName', compat_str), 'uploader': account_data('displayName', compat_str),
'uploader_id': str_or_none(account_data('id', int)), 'uploader_id': str_or_none(account_data('id', int)),
@@ -623,6 +621,5 @@ class PeerTubeIE(InfoExtractor):
'tags': try_get(video, lambda x: x['tags'], list), 'tags': try_get(video, lambda x: x['tags'], list),
'categories': categories, 'categories': categories,
'formats': formats, 'formats': formats,
'subtitles': subtitles, 'subtitles': subtitles
'webpage_url': webpage_url,
} }
+1 -3
View File
@@ -31,7 +31,6 @@ class PinterestBaseIE(InfoExtractor):
title = (data.get('title') or data.get('grid_title') or video_id).strip() title = (data.get('title') or data.get('grid_title') or video_id).strip()
urls = []
formats = [] formats = []
duration = None duration = None
if extract_formats: if extract_formats:
@@ -39,9 +38,8 @@ class PinterestBaseIE(InfoExtractor):
if not isinstance(format_dict, dict): if not isinstance(format_dict, dict):
continue continue
format_url = url_or_none(format_dict.get('url')) format_url = url_or_none(format_dict.get('url'))
if not format_url or format_url in urls: if not format_url:
continue continue
urls.append(format_url)
duration = float_or_none(format_dict.get('duration'), scale=1000) duration = float_or_none(format_dict.get('duration'), scale=1000)
ext = determine_ext(format_url) ext = determine_ext(format_url)
if 'hls' in format_id.lower() or ext == 'm3u8': if 'hls' in format_id.lower() or ext == 'm3u8':
-164
View File
@@ -1,164 +0,0 @@
# coding: utf-8
from __future__ import unicode_literals
import re
import uuid
from .common import InfoExtractor
from ..compat import (
compat_str,
compat_urlparse,
)
from ..utils import (
ExtractorError,
float_or_none,
int_or_none,
try_get,
url_or_none,
)
class PlutoTVIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?pluto\.tv/on-demand/(?P<video_type>movies|series)/(?P<slug>.*)/?$'
_INFO_URL = 'https://service-vod.clusters.pluto.tv/v3/vod/slugs/'
_INFO_QUERY_PARAMS = {
'appName': 'web',
'appVersion': 'na',
'clientID': compat_str(uuid.uuid1()),
'clientModelNumber': 'na',
'serverSideAds': 'false',
'deviceMake': 'unknown',
'deviceModel': 'web',
'deviceType': 'web',
'deviceVersion': 'unknown',
'sid': compat_str(uuid.uuid1()),
}
_TESTS = [
{
'url': 'https://pluto.tv/on-demand/series/i-love-money/season/2/episode/its-in-the-cards-2009-2-3',
'md5': 'ebcdd8ed89aaace9df37924f722fd9bd',
'info_dict': {
'id': '5de6c598e9379ae4912df0a8',
'ext': 'mp4',
'title': 'It\'s In The Cards',
'episode': 'It\'s In The Cards',
'description': 'The teams face off against each other in a 3-on-2 soccer showdown. Strategy comes into play, though, as each team gets to select their opposing teams two defenders.',
'series': 'I Love Money',
'season_number': 2,
'episode_number': 3,
'duration': 3600,
}
},
{
'url': 'https://pluto.tv/on-demand/series/i-love-money/season/1/',
'playlist_count': 11,
'info_dict': {
'id': '5de6c582e9379ae4912dedbd',
'title': 'I Love Money - Season 1',
}
},
{
'url': 'https://pluto.tv/on-demand/series/i-love-money/',
'playlist_count': 26,
'info_dict': {
'id': '5de6c582e9379ae4912dedbd',
'title': 'I Love Money',
}
},
{
'url': 'https://pluto.tv/on-demand/movies/arrival-2015-1-1',
'md5': '3cead001d317a018bf856a896dee1762',
'info_dict': {
'id': '5e83ac701fa6a9001bb9df24',
'ext': 'mp4',
'title': 'Arrival',
'description': 'When mysterious spacecraft touch down across the globe, an elite team - led by expert translator Louise Banks (Academy Award® nominee Amy Adams) races against time to decipher their intent.',
'duration': 9000,
}
},
]
def _to_ad_free_formats(self, video_id, formats):
ad_free_formats = []
m3u8_urls = set()
for format in formats:
res = self._download_webpage(
format.get('url'), video_id, note='Downloading m3u8 playlist',
fatal=False)
if not res:
continue
first_segment_url = re.search(
r'^(https?://.*/)0\-(end|[0-9]+)/[^/]+\.ts$', res,
re.MULTILINE)
if not first_segment_url:
continue
m3u8_urls.add(
compat_urlparse.urljoin(first_segment_url.group(1), '0-end/master.m3u8'))
for m3u8_url in m3u8_urls:
ad_free_formats.extend(
self._extract_m3u8_formats(
m3u8_url, video_id, 'mp4', 'm3u8_native',
m3u8_id='hls', fatal=False))
self._sort_formats(ad_free_formats)
return ad_free_formats
def _get_video_info(self, video_json, slug, series_name=None):
video_id = video_json.get('_id', slug)
formats = []
for video_url in try_get(video_json, lambda x: x['stitched']['urls'], list) or []:
if video_url.get('type') != 'hls':
continue
url = url_or_none(video_url.get('url'))
formats.extend(
self._extract_m3u8_formats(
url, video_id, 'mp4', 'm3u8_native',
m3u8_id='hls', fatal=False))
info = {
'id': video_id,
'formats': self._to_ad_free_formats(video_id, formats),
'title': video_json.get('name'),
'description': video_json.get('description'),
'duration': float_or_none(video_json.get('duration'), scale=1000),
}
if series_name:
info.update({
'series': series_name,
'episode': video_json.get('name'),
'season_number': int_or_none(video_json.get('season')),
'episode_number': int_or_none(video_json.get('number')),
})
return info
def _real_extract(self, url):
path = compat_urlparse.urlparse(url).path
path_components = path.split('/')
video_type = path_components[2]
info_slug = path_components[3]
video_json = self._download_json(self._INFO_URL + info_slug, info_slug,
query=self._INFO_QUERY_PARAMS)
if video_type == 'series':
series_name = video_json.get('name', info_slug)
season_number = int_or_none(try_get(path_components, lambda x: x[5]))
episode_slug = try_get(path_components, lambda x: x[7])
videos = []
for season in video_json['seasons']:
if season_number is not None and season_number != int_or_none(season.get('number')):
continue
for episode in season['episodes']:
if episode_slug is not None and episode_slug != episode.get('slug'):
continue
videos.append(self._get_video_info(episode, episode_slug, series_name))
if not videos:
raise ExtractorError('Failed to find any videos to extract')
if episode_slug is not None and len(videos) == 1:
return videos[0]
playlist_title = series_name
if season_number is not None:
playlist_title += ' - Season %d' % season_number
return self.playlist_result(videos,
playlist_id=video_json.get('_id', info_slug),
playlist_title=playlist_title)
return self._get_video_info(video_json, info_slug)
+13 -31
View File
@@ -167,7 +167,6 @@ class PornHubIE(PornHubBaseIE):
'params': { 'params': {
'skip_download': True, 'skip_download': True,
}, },
'skip': 'Video has been flagged for verification in accordance with our trust and safety policy',
}, { }, {
# subtitles # subtitles
'url': 'https://www.pornhub.com/view_video.php?viewkey=ph5af5fef7c2aa7', 'url': 'https://www.pornhub.com/view_video.php?viewkey=ph5af5fef7c2aa7',
@@ -266,8 +265,7 @@ class PornHubIE(PornHubBaseIE):
webpage = dl_webpage('pc') webpage = dl_webpage('pc')
error_msg = self._html_search_regex( error_msg = self._html_search_regex(
(r'(?s)<div[^>]+class=(["\'])(?:(?!\1).)*\b(?:removed|userMessageSection)\b(?:(?!\1).)*\1[^>]*>(?P<error>.+?)</div>', r'(?s)<div[^>]+class=(["\'])(?:(?!\1).)*\b(?:removed|userMessageSection)\b(?:(?!\1).)*\1[^>]*>(?P<error>.+?)</div>',
r'(?s)<section[^>]+class=["\']noVideo["\'][^>]*>(?P<error>.+?)</section>'),
webpage, 'error message', default=None, group='error') webpage, 'error message', default=None, group='error')
if error_msg: if error_msg:
error_msg = re.sub(r'\s+', ' ', error_msg) error_msg = re.sub(r'\s+', ' ', error_msg)
@@ -396,21 +394,6 @@ class PornHubIE(PornHubBaseIE):
upload_date = None upload_date = None
formats = [] formats = []
def add_format(format_url, height=None):
tbr = None
mobj = re.search(r'(?P<height>\d+)[pP]?_(?P<tbr>\d+)[kK]', format_url)
if mobj:
if not height:
height = int(mobj.group('height'))
tbr = int(mobj.group('tbr'))
formats.append({
'url': format_url,
'format_id': '%dp' % height if height else None,
'height': height,
'tbr': tbr,
})
for video_url, height in video_urls: for video_url, height in video_urls:
if not upload_date: if not upload_date:
upload_date = self._search_regex( upload_date = self._search_regex(
@@ -427,19 +410,18 @@ class PornHubIE(PornHubBaseIE):
video_url, video_id, 'mp4', entry_protocol='m3u8_native', video_url, video_id, 'mp4', entry_protocol='m3u8_native',
m3u8_id='hls', fatal=False)) m3u8_id='hls', fatal=False))
continue continue
if '/video/get_media' in video_url: tbr = None
medias = self._download_json(video_url, video_id, fatal=False) mobj = re.search(r'(?P<height>\d+)[pP]?_(?P<tbr>\d+)[kK]', video_url)
if isinstance(medias, list): if mobj:
for media in medias: if not height:
if not isinstance(media, dict): height = int(mobj.group('height'))
continue tbr = int(mobj.group('tbr'))
video_url = url_or_none(media.get('videoUrl')) formats.append({
if not video_url: 'url': video_url,
continue 'format_id': '%dp' % height if height else None,
height = int_or_none(media.get('quality')) 'height': height,
add_format(video_url, height) 'tbr': tbr,
continue })
add_format(video_url)
self._sort_formats(formats) self._sort_formats(formats)
video_uploader = self._html_search_regex( video_uploader = self._html_search_regex(
-12
View File
@@ -158,10 +158,6 @@ class RaiPlayIE(RaiBaseIE):
# subtitles at 'subtitlesArray' key (see #27698) # subtitles at 'subtitlesArray' key (see #27698)
'url': 'https://www.raiplay.it/video/2020/12/Report---04-01-2021-2e90f1de-8eee-4de4-ac0e-78d21db5b600.html', 'url': 'https://www.raiplay.it/video/2020/12/Report---04-01-2021-2e90f1de-8eee-4de4-ac0e-78d21db5b600.html',
'only_matching': True, 'only_matching': True,
}, {
# DRM protected
'url': 'https://www.raiplay.it/video/2020/09/Lo-straordinario-mondo-di-Zoey-S1E1-Lo-straordinario-potere-di-Zoey-ed493918-1d32-44b7-8454-862e473d00ff.html',
'only_matching': True,
}] }]
def _real_extract(self, url): def _real_extract(self, url):
@@ -170,14 +166,6 @@ class RaiPlayIE(RaiBaseIE):
media = self._download_json( media = self._download_json(
base + '.json', video_id, 'Downloading video JSON') base + '.json', video_id, 'Downloading video JSON')
if not self._downloader.params.get('allow_unplayable_formats'):
if try_get(
media,
(lambda x: x['rights_management']['rights']['drm'],
lambda x: x['program_info']['rights_management']['rights']['drm']),
dict):
raise ExtractorError('This video is DRM protected.', expected=True)
title = media['name'] title = media['name']
video = media['video'] video = media['video']
+17 -31
View File
@@ -15,9 +15,6 @@ from ..utils import (
class RCSBaseIE(InfoExtractor): class RCSBaseIE(InfoExtractor):
# based on VideoPlayerLoader.prototype.getVideoSrc
# and VideoPlayerLoader.prototype.transformSrc from
# https://js2.corriereobjects.it/includes2013/LIBS/js/corriere_video.sjs
_ALL_REPLACE = { _ALL_REPLACE = {
'media2vam.corriere.it.edgesuite.net': 'media2vam.corriere.it.edgesuite.net':
'media2vam-corriere-it.akamaized.net', 'media2vam-corriere-it.akamaized.net',
@@ -194,10 +191,10 @@ class RCSBaseIE(InfoExtractor):
urls.get('m3u8'), video_id, 'mp4', entry_protocol='m3u8_native', urls.get('m3u8'), video_id, 'mp4', entry_protocol='m3u8_native',
m3u8_id='hls', fatal=False) m3u8_id='hls', fatal=False)
if urls.get('mp4'): if not formats:
formats.append({ formats.append({
'format_id': 'http-mp4', 'format_id': 'http-mp4',
'url': urls['mp4'] 'url': urls.get('mp4')
}) })
self._sort_formats(formats) self._sort_formats(formats)
return formats return formats
@@ -219,12 +216,10 @@ class RCSBaseIE(InfoExtractor):
video_data = None video_data = None
# look for json video data url # look for json video data url
json = self._search_regex( json = self._search_regex(
r'''(?x)url\s*=\s*(["']) r'''(?x)var url\s*=\s*["']((?:https?:)?
(?P<url> //video\.rcs\.it
(?:https?:)?//video\.rcs\.it /fragment-includes/video-includes/.+?\.json)["'];''',
/fragment-includes/video-includes/.+?\.json page, video_id, default=None)
)\1;''',
page, video_id, group='url', default=None)
if json: if json:
if json.startswith('//'): if json.startswith('//'):
json = 'https:%s' % json json = 'https:%s' % json
@@ -232,16 +227,13 @@ class RCSBaseIE(InfoExtractor):
# if json url not found, look for json video data directly in the page # if json url not found, look for json video data directly in the page
else: else:
# RCS normal pages and most of the embeds
json = self._search_regex( json = self._search_regex(
r'[\s;]video\s*=\s*({[\s\S]+?})(?:;|,playlist=)', r'[\s;]video\s*=\s*({[\s\S]+?})(?:;|,playlist=)',
page, video_id, default=None) page, video_id, default=None)
if not json and 'video-embed' in url: if json:
page = self._download_webpage(url.replace('video-embed', 'video-json'), video_id) video_data = self._parse_json(
json = self._search_regex( json, video_id, transform_source=js_to_json)
r'##start-video##({[\s\S]+?})##end-video##', else:
page, video_id, default=None)
if not json:
# if no video data found try search for iframes # if no video data found try search for iframes
emb = RCSEmbedsIE._extract_url(page) emb = RCSEmbedsIE._extract_url(page)
if emb: if emb:
@@ -250,9 +242,6 @@ class RCSBaseIE(InfoExtractor):
'url': emb, 'url': emb,
'ie_key': RCSEmbedsIE.ie_key() 'ie_key': RCSEmbedsIE.ie_key()
} }
if json:
video_data = self._parse_json(
json, video_id, transform_source=js_to_json)
if not video_data: if not video_data:
raise ExtractorError('Video data not found in the page') raise ExtractorError('Video data not found in the page')
@@ -261,8 +250,7 @@ class RCSBaseIE(InfoExtractor):
self._get_video_src(video_data), video_id) self._get_video_src(video_data), video_id)
description = (video_data.get('description') description = (video_data.get('description')
or clean_html(video_data.get('htmlDescription')) or clean_html(video_data.get('htmlDescription')))
or self._html_search_meta('description', page))
uploader = video_data.get('provider') or mobj.group('cdn') uploader = video_data.get('provider') or mobj.group('cdn')
return { return {
@@ -295,7 +283,6 @@ class RCSEmbedsIE(RCSBaseIE):
'uploader': 'rcs.it', 'uploader': 'rcs.it',
} }
}, { }, {
# redownload the page changing 'video-embed' in 'video-json'
'url': 'https://video.gazzanet.gazzetta.it/video-embed/gazzanet-mo05-0000260789', 'url': 'https://video.gazzanet.gazzetta.it/video-embed/gazzanet-mo05-0000260789',
'md5': 'a043e3fecbe4d9ed7fc5d888652a5440', 'md5': 'a043e3fecbe4d9ed7fc5d888652a5440',
'info_dict': { 'info_dict': {
@@ -372,7 +359,6 @@ class RCSIE(RCSBaseIE):
'uploader': 'Corriere Tv', 'uploader': 'Corriere Tv',
} }
}, { }, {
# video data inside iframe
'url': 'https://viaggi.corriere.it/video/norvegia-il-nuovo-ponte-spettacolare-sopra-la-cascata-di-voringsfossen/', 'url': 'https://viaggi.corriere.it/video/norvegia-il-nuovo-ponte-spettacolare-sopra-la-cascata-di-voringsfossen/',
'md5': 'da378e4918d2afbf7d61c35abb948d4c', 'md5': 'da378e4918d2afbf7d61c35abb948d4c',
'info_dict': { 'info_dict': {
@@ -403,15 +389,15 @@ class RCSVariousIE(RCSBaseIE):
(?P<cdn> (?P<cdn>
leitv\.it| leitv\.it|
youreporter\.it youreporter\.it
)/(?:[^/]+/)?(?P<id>[^/]+?)(?:$|\?|/)''' )/(?:video/)?(?P<id>[^/]+?)(?:$|\?|/)'''
_TESTS = [{ _TESTS = [{
'url': 'https://www.leitv.it/benessere/mal-di-testa-come-combatterlo-ed-evitarne-la-comparsa/', 'url': 'https://www.leitv.it/video/marmellata-di-ciliegie-fatta-in-casa/',
'md5': '92b4e63667b8f95acb0a04da25ae28a1', 'md5': '618aaabac32152199c1af86784d4d554',
'info_dict': { 'info_dict': {
'id': 'mal-di-testa-come-combatterlo-ed-evitarne-la-comparsa', 'id': 'marmellata-di-ciliegie-fatta-in-casa',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Cervicalgia e mal di testa, il video con i suggerimenti dell\'esperto', 'title': 'Marmellata di ciliegie fatta in casa',
'description': 'md5:ae21418f34cee0b8d02a487f55bcabb5', 'description': 'md5:89133864d6aad456dbcf6e7a29f86263',
'uploader': 'leitv.it', 'uploader': 'leitv.it',
} }
}, { }, {
+128 -104
View File
@@ -2,9 +2,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import base64 import base64
import io
import re import re
import sys import time
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import ( from ..compat import (
@@ -15,13 +14,56 @@ from ..utils import (
determine_ext, determine_ext,
ExtractorError, ExtractorError,
float_or_none, float_or_none,
qualities,
remove_end, remove_end,
remove_start, remove_start,
sanitized_Request,
std_headers, std_headers,
) )
_bytes_to_chr = (lambda x: x) if sys.version_info[0] == 2 else (lambda x: map(chr, x))
def _decrypt_url(png):
encrypted_data = compat_b64decode(png)
text_index = encrypted_data.find(b'tEXt')
text_chunk = encrypted_data[text_index - 4:]
length = compat_struct_unpack('!I', text_chunk[:4])[0]
# Use bytearray to get integers when iterating in both python 2.x and 3.x
data = bytearray(text_chunk[8:8 + length])
data = [chr(b) for b in data if b != 0]
hash_index = data.index('#')
alphabet_data = data[:hash_index]
url_data = data[hash_index + 1:]
if url_data[0] == 'H' and url_data[3] == '%':
# remove useless HQ%% at the start
url_data = url_data[4:]
alphabet = []
e = 0
d = 0
for l in alphabet_data:
if d == 0:
alphabet.append(l)
d = e = (e + 1) % 4
else:
d -= 1
url = ''
f = 0
e = 3
b = 1
for letter in url_data:
if f == 0:
l = int(letter) * 10
f = 1
else:
if e == 0:
l += int(letter)
url += alphabet[l]
e = (b + 3) % 4
f = 0
b += 1
else:
e -= 1
return url
class RTVEALaCartaIE(InfoExtractor): class RTVEALaCartaIE(InfoExtractor):
@@ -37,31 +79,28 @@ class RTVEALaCartaIE(InfoExtractor):
'ext': 'mp4', 'ext': 'mp4',
'title': 'Balonmano - Swiss Cup masculina. Final: España-Suecia', 'title': 'Balonmano - Swiss Cup masculina. Final: España-Suecia',
'duration': 5024.566, 'duration': 5024.566,
'series': 'Balonmano',
}, },
'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
}, { }, {
'note': 'Live stream', 'note': 'Live stream',
'url': 'http://www.rtve.es/alacarta/videos/television/24h-live/1694255/', 'url': 'http://www.rtve.es/alacarta/videos/television/24h-live/1694255/',
'info_dict': { 'info_dict': {
'id': '1694255', 'id': '1694255',
'ext': 'mp4', 'ext': 'flv',
'title': 're:^24H LIVE [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$', 'title': 'TODO',
'is_live': True,
},
'params': {
'skip_download': 'live stream',
}, },
'skip': 'The f4m manifest can\'t be used yet',
}, { }, {
'url': 'http://www.rtve.es/alacarta/videos/servir-y-proteger/servir-proteger-capitulo-104/4236788/', 'url': 'http://www.rtve.es/alacarta/videos/servir-y-proteger/servir-proteger-capitulo-104/4236788/',
'md5': 'd850f3c8731ea53952ebab489cf81cbf', 'md5': 'e55e162379ad587e9640eda4f7353c0f',
'info_dict': { 'info_dict': {
'id': '4236788', 'id': '4236788',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Servir y proteger - Capítulo 104', 'title': 'Servir y proteger - Capítulo 104 ',
'duration': 3222.0, 'duration': 3222.0,
}, },
'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'], 'params': {
'skip_download': True, # requires ffmpeg
},
}, { }, {
'url': 'http://www.rtve.es/m/alacarta/videos/cuentame-como-paso/cuentame-como-paso-t16-ultimo-minuto-nuestra-vida-capitulo-276/2969138/?media=tve', 'url': 'http://www.rtve.es/m/alacarta/videos/cuentame-como-paso/cuentame-como-paso-t16-ultimo-minuto-nuestra-vida-capitulo-276/2969138/?media=tve',
'only_matching': True, 'only_matching': True,
@@ -72,102 +111,58 @@ class RTVEALaCartaIE(InfoExtractor):
def _real_initialize(self): def _real_initialize(self):
user_agent_b64 = base64.b64encode(std_headers['User-Agent'].encode('utf-8')).decode('utf-8') user_agent_b64 = base64.b64encode(std_headers['User-Agent'].encode('utf-8')).decode('utf-8')
self._manager = self._download_json( manager_info = self._download_json(
'http://www.rtve.es/odin/loki/' + user_agent_b64, 'http://www.rtve.es/odin/loki/' + user_agent_b64,
None, 'Fetching manager info')['manager'] None, 'Fetching manager info')
self._manager = manager_info['manager']
@staticmethod
def _decrypt_url(png):
encrypted_data = io.BytesIO(compat_b64decode(png)[8:])
while True:
length = compat_struct_unpack('!I', encrypted_data.read(4))[0]
chunk_type = encrypted_data.read(4)
if chunk_type == b'IEND':
break
data = encrypted_data.read(length)
if chunk_type == b'tEXt':
alphabet_data, text = data.split(b'\0')
quality, url_data = text.split(b'%%')
alphabet = []
e = 0
d = 0
for l in _bytes_to_chr(alphabet_data):
if d == 0:
alphabet.append(l)
d = e = (e + 1) % 4
else:
d -= 1
url = ''
f = 0
e = 3
b = 1
for letter in _bytes_to_chr(url_data):
if f == 0:
l = int(letter) * 10
f = 1
else:
if e == 0:
l += int(letter)
url += alphabet[l]
e = (b + 3) % 4
f = 0
b += 1
else:
e -= 1
yield quality.decode(), url
encrypted_data.read(4) # CRC
def _extract_png_formats(self, video_id):
png = self._download_webpage(
'http://www.rtve.es/ztnr/movil/thumbnail/%s/videos/%s.png' % (self._manager, video_id),
video_id, 'Downloading url information', query={'q': 'v2'})
q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
formats = []
for quality, video_url in self._decrypt_url(png):
ext = determine_ext(video_url)
if ext == 'm3u8':
formats.extend(self._extract_m3u8_formats(
video_url, video_id, 'mp4', 'm3u8_native',
m3u8_id='hls', fatal=False))
elif ext == 'mpd':
formats.extend(self._extract_mpd_formats(
video_url, video_id, 'dash', fatal=False))
else:
formats.append({
'format_id': quality,
'quality': q(quality),
'url': video_url,
})
self._sort_formats(formats)
return formats
def _real_extract(self, url): def _real_extract(self, url):
video_id = self._match_id(url) mobj = re.match(self._VALID_URL, url)
video_id = mobj.group('id')
info = self._download_json( info = self._download_json(
'http://www.rtve.es/api/videos/%s/config/alacarta_videos.json' % video_id, 'http://www.rtve.es/api/videos/%s/config/alacarta_videos.json' % video_id,
video_id)['page']['items'][0] video_id)['page']['items'][0]
if info['state'] == 'DESPU': if info['state'] == 'DESPU':
raise ExtractorError('The video is no longer available', expected=True) raise ExtractorError('The video is no longer available', expected=True)
title = info['title'].strip() title = info['title']
formats = self._extract_png_formats(video_id) png_url = 'http://www.rtve.es/ztnr/movil/thumbnail/%s/videos/%s.png' % (self._manager, video_id)
png_request = sanitized_Request(png_url)
png_request.add_header('Referer', url)
png = self._download_webpage(png_request, video_id, 'Downloading url information')
video_url = _decrypt_url(png)
ext = determine_ext(video_url)
formats = []
if not video_url.endswith('.f4m') and ext != 'm3u8':
if '?' not in video_url:
video_url = video_url.replace('resources/', 'auth/resources/')
video_url = video_url.replace('.net.rtve', '.multimedia.cdn.rtve')
if ext == 'm3u8':
formats.extend(self._extract_m3u8_formats(
video_url, video_id, ext='mp4', entry_protocol='m3u8_native',
m3u8_id='hls', fatal=False))
elif ext == 'f4m':
formats.extend(self._extract_f4m_formats(
video_url, video_id, f4m_id='hds', fatal=False))
else:
formats.append({
'url': video_url,
})
self._sort_formats(formats)
subtitles = None subtitles = None
sbt_file = info.get('sbtFile') if info.get('sbtFile') is not None:
if sbt_file: subtitles = self.extract_subtitles(video_id, info['sbtFile'])
subtitles = self.extract_subtitles(video_id, sbt_file)
is_live = info.get('live') is True
return { return {
'id': video_id, 'id': video_id,
'title': self._live_title(title) if is_live else title, 'title': title,
'formats': formats, 'formats': formats,
'thumbnail': info.get('image'), 'thumbnail': info.get('image'),
'page_url': url,
'subtitles': subtitles, 'subtitles': subtitles,
'duration': float_or_none(info.get('duration'), 1000), 'duration': float_or_none(info.get('duration'), scale=1000),
'is_live': is_live,
'series': info.get('programTitle'),
} }
def _get_subtitles(self, video_id, sub_file): def _get_subtitles(self, video_id, sub_file):
@@ -179,26 +174,48 @@ class RTVEALaCartaIE(InfoExtractor):
for s in subs) for s in subs)
class RTVEInfantilIE(RTVEALaCartaIE): class RTVEInfantilIE(InfoExtractor):
IE_NAME = 'rtve.es:infantil' IE_NAME = 'rtve.es:infantil'
IE_DESC = 'RTVE infantil' IE_DESC = 'RTVE infantil'
_VALID_URL = r'https?://(?:www\.)?rtve\.es/infantil/serie/[^/]+/video/[^/]+/(?P<id>[0-9]+)/' _VALID_URL = r'https?://(?:www\.)?rtve\.es/infantil/serie/(?P<show>[^/]*)/video/(?P<short_title>[^/]*)/(?P<id>[0-9]+)/'
_TESTS = [{ _TESTS = [{
'url': 'http://www.rtve.es/infantil/serie/cleo/video/maneras-vivir/3040283/', 'url': 'http://www.rtve.es/infantil/serie/cleo/video/maneras-vivir/3040283/',
'md5': '5747454717aedf9f9fdf212d1bcfc48d', 'md5': '915319587b33720b8e0357caaa6617e6',
'info_dict': { 'info_dict': {
'id': '3040283', 'id': '3040283',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Maneras de vivir', 'title': 'Maneras de vivir',
'thumbnail': r're:https?://.+/1426182947956\.JPG', 'thumbnail': 'http://www.rtve.es/resources/jpg/6/5/1426182947956.JPG',
'duration': 357.958, 'duration': 357.958,
}, },
'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
}] }]
def _real_extract(self, url):
video_id = self._match_id(url)
info = self._download_json(
'http://www.rtve.es/api/videos/%s/config/alacarta_videos.json' % video_id,
video_id)['page']['items'][0]
class RTVELiveIE(RTVEALaCartaIE): webpage = self._download_webpage(url, video_id)
vidplayer_id = self._search_regex(
r' id="vidplayer([0-9]+)"', webpage, 'internal video ID')
png_url = 'http://www.rtve.es/ztnr/movil/thumbnail/default/videos/%s.png' % vidplayer_id
png = self._download_webpage(png_url, video_id, 'Downloading url information')
video_url = _decrypt_url(png)
return {
'id': video_id,
'ext': 'mp4',
'title': info['title'],
'url': video_url,
'thumbnail': info.get('image'),
'duration': float_or_none(info.get('duration'), scale=1000),
}
class RTVELiveIE(InfoExtractor):
IE_NAME = 'rtve.es:live' IE_NAME = 'rtve.es:live'
IE_DESC = 'RTVE.es live streams' IE_DESC = 'RTVE.es live streams'
_VALID_URL = r'https?://(?:www\.)?rtve\.es/directo/(?P<id>[a-zA-Z0-9-]+)' _VALID_URL = r'https?://(?:www\.)?rtve\.es/directo/(?P<id>[a-zA-Z0-9-]+)'
@@ -208,7 +225,7 @@ class RTVELiveIE(RTVEALaCartaIE):
'info_dict': { 'info_dict': {
'id': 'la-1', 'id': 'la-1',
'ext': 'mp4', 'ext': 'mp4',
'title': 're:^La 1 [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$', 'title': 're:^La 1 [0-9]{4}-[0-9]{2}-[0-9]{2}Z[0-9]{6}$',
}, },
'params': { 'params': {
'skip_download': 'live stream', 'skip_download': 'live stream',
@@ -217,22 +234,29 @@ class RTVELiveIE(RTVEALaCartaIE):
def _real_extract(self, url): def _real_extract(self, url):
mobj = re.match(self._VALID_URL, url) mobj = re.match(self._VALID_URL, url)
start_time = time.gmtime()
video_id = mobj.group('id') video_id = mobj.group('id')
webpage = self._download_webpage(url, video_id) webpage = self._download_webpage(url, video_id)
title = remove_end(self._og_search_title(webpage), ' en directo en RTVE.es') title = remove_end(self._og_search_title(webpage), ' en directo en RTVE.es')
title = remove_start(title, 'Estoy viendo ') title = remove_start(title, 'Estoy viendo ')
title += ' ' + time.strftime('%Y-%m-%dZ%H%M%S', start_time)
vidplayer_id = self._search_regex( vidplayer_id = self._search_regex(
(r'playerId=player([0-9]+)', (r'playerId=player([0-9]+)',
r'class=["\'].*?\blive_mod\b.*?["\'][^>]+data-assetid=["\'](\d+)', r'class=["\'].*?\blive_mod\b.*?["\'][^>]+data-assetid=["\'](\d+)',
r'data-id=["\'](\d+)'), r'data-id=["\'](\d+)'),
webpage, 'internal video ID') webpage, 'internal video ID')
png_url = 'http://www.rtve.es/ztnr/movil/thumbnail/amonet/videos/%s.png' % vidplayer_id
png = self._download_webpage(png_url, video_id, 'Downloading url information')
m3u8_url = _decrypt_url(png)
formats = self._extract_m3u8_formats(m3u8_url, video_id, ext='mp4')
self._sort_formats(formats)
return { return {
'id': video_id, 'id': video_id,
'title': self._live_title(title), 'title': title,
'formats': self._extract_png_formats(vidplayer_id), 'formats': formats,
'is_live': True, 'is_live': True,
} }
+7 -13
View File
@@ -51,16 +51,13 @@ class ShahidIE(ShahidBaseIE):
_NETRC_MACHINE = 'shahid' _NETRC_MACHINE = 'shahid'
_VALID_URL = r'https?://shahid\.mbc\.net/ar/(?:serie|show|movie)s/[^/]+/(?P<type>episode|clip|movie)-(?P<id>\d+)' _VALID_URL = r'https?://shahid\.mbc\.net/ar/(?:serie|show|movie)s/[^/]+/(?P<type>episode|clip|movie)-(?P<id>\d+)'
_TESTS = [{ _TESTS = [{
'url': 'https://shahid.mbc.net/ar/shows/%D9%85%D8%AA%D8%AD%D9%81-%D8%A7%D9%84%D8%AF%D8%AD%D9%8A%D8%AD-%D8%A7%D9%84%D9%85%D9%88%D8%B3%D9%85-1-%D9%83%D9%84%D9%8A%D8%A8-1/clip-816924', 'url': 'https://shahid.mbc.net/ar/shows/%D9%85%D8%AC%D9%84%D8%B3-%D8%A7%D9%84%D8%B4%D8%A8%D8%A7%D8%A8-%D8%A7%D9%84%D9%85%D9%88%D8%B3%D9%85-1-%D9%83%D9%84%D9%8A%D8%A8-1/clip-275286',
'info_dict': { 'info_dict': {
'id': '816924', 'id': '275286',
'ext': 'mp4', 'ext': 'mp4',
'title': 'متحف الدحيح الموسم 1 كليب 1', 'title': 'مجلس الشباب الموسم 1 كليب 1',
'timestamp': 1602806400, 'timestamp': 1506988800,
'upload_date': '20201016', 'upload_date': '20171003',
'description': 'برومو',
'duration': 22,
'categories': ['كوميديا'],
}, },
'params': { 'params': {
# m3u8 download # m3u8 download
@@ -112,15 +109,12 @@ class ShahidIE(ShahidBaseIE):
page_type = 'episode' page_type = 'episode'
playout = self._call_api( playout = self._call_api(
'playout/new/url/' + video_id, video_id)['playout'] 'playout/url/' + video_id, video_id)['playout']
if not self._downloader.params.get('allow_unplayable_formats') and playout.get('drm'): if not self._downloader.params.get('allow_unplayable_formats') and playout.get('drm'):
raise ExtractorError('This video is DRM protected.', expected=True) raise ExtractorError('This video is DRM protected.', expected=True)
formats = self._extract_m3u8_formats(re.sub( formats = self._extract_m3u8_formats(playout['url'], video_id, 'mp4')
# https://docs.aws.amazon.com/mediapackage/latest/ug/manifest-filtering.html
r'aws\.manifestfilter=[\w:;,-]+&?',
'', playout['url']), video_id, 'mp4')
self._sort_formats(formats) self._sort_formats(formats)
# video = self._call_api( # video = self._call_api(
+2 -14
View File
@@ -6,9 +6,9 @@ from .mtv import MTVServicesInfoExtractor
class SouthParkIE(MTVServicesInfoExtractor): class SouthParkIE(MTVServicesInfoExtractor):
IE_NAME = 'southpark.cc.com' IE_NAME = 'southpark.cc.com'
_VALID_URL = r'https?://(?:www\.)?(?P<url>southpark(?:\.cc|studios)\.com/(?:clips|(?:full-)?episodes|collections)/(?P<id>.+?)(\?|#|$))' _VALID_URL = r'https?://(?:www\.)?(?P<url>southpark\.cc\.com/(?:clips|(?:full-)?episodes|collections)/(?P<id>.+?)(\?|#|$))'
_FEED_URL = 'http://feeds.mtvnservices.com/od/feed/intl-mrss-player-feed' _FEED_URL = 'http://www.southparkstudios.com/feeds/video-player/mrss'
_TESTS = [{ _TESTS = [{
'url': 'http://southpark.cc.com/clips/104437/bat-daded#tab=featured', 'url': 'http://southpark.cc.com/clips/104437/bat-daded#tab=featured',
@@ -23,20 +23,8 @@ class SouthParkIE(MTVServicesInfoExtractor):
}, { }, {
'url': 'http://southpark.cc.com/collections/7758/fan-favorites/1', 'url': 'http://southpark.cc.com/collections/7758/fan-favorites/1',
'only_matching': True, 'only_matching': True,
}, {
'url': 'https://www.southparkstudios.com/episodes/h4o269/south-park-stunning-and-brave-season-19-ep-1',
'only_matching': True,
}] }]
def _get_feed_query(self, uri):
return {
'accountOverride': 'intl.mtvi.com',
'arcEp': 'shared.southpark.global',
'ep': '90877963',
'imageEp': 'shared.southpark.global',
'mgid': uri,
}
class SouthParkEsIE(SouthParkIE): class SouthParkEsIE(SouthParkIE):
IE_NAME = 'southpark.cc.com:español' IE_NAME = 'southpark.cc.com:español'
+62 -85
View File
@@ -1,105 +1,82 @@
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
import re
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import (
compat_parse_qs,
compat_urllib_parse_urlparse,
)
from ..utils import ( from ..utils import (
clean_html,
float_or_none,
int_or_none,
parse_iso8601, parse_iso8601,
strip_or_none, sanitized_Request,
try_get,
) )
class SportDeutschlandIE(InfoExtractor): class SportDeutschlandIE(InfoExtractor):
_VALID_URL = r'https?://sportdeutschland\.tv/(?P<id>(?:[^/]+/)?[^?#/&]+)' _VALID_URL = r'https?://sportdeutschland\.tv/(?P<sport>[^/?#]+)/(?P<id>[^?#/]+)(?:$|[?#])'
_TESTS = [{ _TESTS = [{
'url': 'https://sportdeutschland.tv/badminton/re-live-deutsche-meisterschaften-2020-halbfinals?playlistId=0', 'url': 'https://sportdeutschland.tv/badminton/re-live-deutsche-meisterschaften-2020-halbfinals?playlistId=0',
'info_dict': { 'info_dict': {
'id': '5318cac0275701382770543d7edaf0a0', 'id': 're-live-deutsche-meisterschaften-2020-halbfinals',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Re-live: Deutsche Meisterschaften 2020 - Halbfinals - Teil 1', 'title': 're:Re-live: Deutsche Meisterschaften 2020.*Halbfinals',
'duration': 16106.36, 'categories': ['Badminton-Deutschland'],
'view_count': int,
'thumbnail': r're:^https?://.*\.(?:jpg|png)$',
'timestamp': int,
'upload_date': '20200201',
'description': 're:.*', # meaningless description for THIS video
}, },
'params': {
'noplaylist': True,
# m3u8 download
'skip_download': True,
},
}, {
'url': 'https://sportdeutschland.tv/badminton/re-live-deutsche-meisterschaften-2020-halbfinals?playlistId=0',
'info_dict': {
'id': 'c6e2fdd01f63013854c47054d2ab776f',
'title': 'Re-live: Deutsche Meisterschaften 2020 - Halbfinals',
'description': 'md5:5263ff4c31c04bb780c9f91130b48530',
'duration': 31397,
},
'playlist_count': 2,
}, {
'url': 'https://sportdeutschland.tv/freeride-world-tour-2021-fieberbrunn-oesterreich',
'only_matching': True,
}] }]
def _real_extract(self, url): def _real_extract(self, url):
display_id = self._match_id(url) mobj = re.match(self._VALID_URL, url)
data = self._download_json( video_id = mobj.group('id')
'https://backend.sportdeutschland.tv/api/permalinks/' + display_id, sport_id = mobj.group('sport')
display_id, query={'access_token': 'true'})
asset = data['asset']
title = (asset.get('title') or asset['label']).strip()
asset_id = asset.get('id') or asset.get('uuid')
info = {
'id': asset_id,
'title': title,
'description': clean_html(asset.get('body') or asset.get('description')) or asset.get('teaser'),
'duration': int_or_none(asset.get('seconds')),
}
videos = asset.get('videos') or []
if len(videos) > 1:
playlist_id = compat_parse_qs(compat_urllib_parse_urlparse(url).query).get('playlistId', [None])[0]
if playlist_id:
if self._downloader.params.get('noplaylist'):
videos = [videos[int(playlist_id)]]
self.to_screen('Downloading just a single video because of --no-playlist')
else:
self.to_screen('Downloading playlist %s - add --no-playlist to just download video' % asset_id)
def entries(): api_url = 'https://proxy.vidibusdynamic.net/ssl/backend.sportdeutschland.tv/api/permalinks/%s/%s?access_token=true' % (
for i, video in enumerate(videos, 1): sport_id, video_id)
video_id = video.get('uuid') req = sanitized_Request(api_url, headers={
video_url = video.get('url') 'Accept': 'application/vnd.vidibus.v2.html+json',
if not (video_id and video_url): 'Referer': url,
continue })
formats = self._extract_m3u8_formats( data = self._download_json(req, video_id)
video_url.replace('.smil', '.m3u8'), video_id, 'mp4', fatal=False)
if not formats: asset = data['asset']
continue categories = [data['section']['title']]
yield {
'id': video_id, formats = []
'formats': formats, smil_url = asset['video']
'title': title + ' - ' + (video.get('label') or 'Teil %d' % i), if '.smil' in smil_url:
'duration': float_or_none(video.get('duration')), m3u8_url = smil_url.replace('.smil', '.m3u8')
} formats.extend(
info.update({ self._extract_m3u8_formats(m3u8_url, video_id, ext='mp4'))
'_type': 'multi_video',
'entries': entries(), smil_doc = self._download_xml(
}) smil_url, video_id, note='Downloading SMIL metadata')
base_url_el = smil_doc.find('./head/meta')
if base_url_el:
base_url = base_url_el.attrib['base']
formats.extend([{
'format_id': 'rmtp',
'url': base_url if base_url_el else n.attrib['src'],
'play_path': n.attrib['src'],
'ext': 'flv',
'preference': -100,
'format_note': 'Seems to fail at example stream',
} for n in smil_doc.findall('./body/video')])
else: else:
formats = self._extract_m3u8_formats( formats.append({'url': smil_url})
videos[0]['url'].replace('.smil', '.m3u8'), asset_id, 'mp4')
section_title = strip_or_none(try_get(data, lambda x: x['section']['title'])) self._sort_formats(formats)
info.update({
'formats': formats, return {
'display_id': asset.get('permalink'), 'id': video_id,
'thumbnail': try_get(asset, lambda x: x['images'][0]), 'formats': formats,
'categories': [section_title] if section_title else None, 'title': asset['title'],
'view_count': int_or_none(asset.get('views')), 'thumbnail': asset.get('image'),
'is_live': asset.get('is_live') is True, 'description': asset.get('teaser'),
'timestamp': parse_iso8601(asset.get('date') or asset.get('published_at')), 'duration': asset.get('duration'),
}) 'categories': categories,
return info 'view_count': asset.get('views'),
'rtmp_live': asset.get('live'),
'timestamp': parse_iso8601(asset.get('date')),
}
+32 -89
View File
@@ -1,61 +1,19 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import re
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import compat_str
from ..utils import ( from ..utils import (
clean_html, clean_html,
clean_podcast_url,
ExtractorError, ExtractorError,
int_or_none, int_or_none,
str_or_none, str_or_none,
try_get, try_get,
url_or_none,
) )
class StitcherBaseIE(InfoExtractor): class StitcherIE(InfoExtractor):
_VALID_URL_BASE = r'https?://(?:www\.)?stitcher\.com/(?:podcast|show)/' _VALID_URL = r'https?://(?:www\.)?stitcher\.com/(?:podcast|show)/(?:[^/]+/)+e(?:pisode)?/(?:(?P<display_id>[^/#?&]+?)-)?(?P<id>\d+)(?:[/#?&]|$)'
def _call_api(self, path, video_id, query):
resp = self._download_json(
'https://api.prod.stitcher.com/' + path,
video_id, query=query)
error_massage = try_get(resp, lambda x: x['errors'][0]['message'])
if error_massage:
raise ExtractorError(error_massage, expected=True)
return resp['data']
def _extract_description(self, data):
return clean_html(data.get('html_description') or data.get('description'))
def _extract_audio_url(self, episode):
return url_or_none(episode.get('audio_url') or episode.get('guid'))
def _extract_show_info(self, show):
return {
'thumbnail': show.get('image_base_url'),
'series': show.get('title'),
}
def _extract_episode(self, episode, audio_url, show_info):
info = {
'id': compat_str(episode['id']),
'display_id': episode.get('slug'),
'title': episode['title'].strip(),
'description': self._extract_description(episode),
'duration': int_or_none(episode.get('duration')),
'url': clean_podcast_url(audio_url),
'vcodec': 'none',
'timestamp': int_or_none(episode.get('date_published')),
'season_number': int_or_none(episode.get('season')),
'season_id': str_or_none(episode.get('season_id')),
}
info.update(show_info)
return info
class StitcherIE(StitcherBaseIE):
_VALID_URL = StitcherBaseIE._VALID_URL_BASE + r'(?:[^/]+/)+e(?:pisode)?/(?:[^/#?&]+-)?(?P<id>\d+)'
_TESTS = [{ _TESTS = [{
'url': 'http://www.stitcher.com/podcast/the-talking-machines/e/40789481?autoplay=true', 'url': 'http://www.stitcher.com/podcast/the-talking-machines/e/40789481?autoplay=true',
'md5': 'e9635098e0da10b21a0e2b85585530f6', 'md5': 'e9635098e0da10b21a0e2b85585530f6',
@@ -66,9 +24,8 @@ class StitcherIE(StitcherBaseIE):
'description': 'md5:547adb4081864be114ae3831b4c2b42f', 'description': 'md5:547adb4081864be114ae3831b4c2b42f',
'duration': 1604, 'duration': 1604,
'thumbnail': r're:^https?://.*\.jpg', 'thumbnail': r're:^https?://.*\.jpg',
'upload_date': '20151008', 'upload_date': '20180126',
'timestamp': 1444285800, 'timestamp': 1516989316,
'series': 'Talking Machines',
}, },
}, { }, {
'url': 'http://www.stitcher.com/podcast/panoply/vulture-tv/e/the-rare-hourlong-comedy-plus-40846275?autoplay=true', 'url': 'http://www.stitcher.com/podcast/panoply/vulture-tv/e/the-rare-hourlong-comedy-plus-40846275?autoplay=true',
@@ -98,47 +55,33 @@ class StitcherIE(StitcherBaseIE):
}] }]
def _real_extract(self, url): def _real_extract(self, url):
audio_id = self._match_id(url) display_id, audio_id = re.match(self._VALID_URL, url).groups()
data = self._call_api(
'shows/episodes', audio_id, {'episode_ids': audio_id})
episode = data['episodes'][0]
audio_url = self._extract_audio_url(episode)
if not audio_url:
self.raise_login_required()
show = try_get(data, lambda x: x['shows'][0], dict) or {}
return self._extract_episode(
episode, audio_url, self._extract_show_info(show))
resp = self._download_json(
'https://api.prod.stitcher.com/episode/' + audio_id,
display_id or audio_id)
episode = try_get(resp, lambda x: x['data']['episodes'][0], dict)
if not episode:
raise ExtractorError(resp['errors'][0]['message'], expected=True)
class StitcherShowIE(StitcherBaseIE): title = episode['title'].strip()
_VALID_URL = StitcherBaseIE._VALID_URL_BASE + r'(?P<id>[^/#?&]+)/?(?:[?#&]|$)' audio_url = episode['audio_url']
_TESTS = [{
'url': 'http://www.stitcher.com/podcast/the-talking-machines',
'info_dict': {
'id': 'the-talking-machines',
'title': 'Talking Machines',
'description': 'md5:831f0995e40f26c10231af39cf1ebf0b',
},
'playlist_mincount': 106,
}, {
'url': 'https://www.stitcher.com/show/the-talking-machines',
'only_matching': True,
}]
def _real_extract(self, url): thumbnail = None
show_slug = self._match_id(url) show_id = episode.get('show_id')
data = self._call_api( if show_id and episode.get('classic_id') != -1:
'search/show/%s/allEpisodes' % show_slug, show_slug, {'count': 10000}) thumbnail = 'https://stitcher-classic.imgix.net/feedimages/%s.jpg' % show_id
show = try_get(data, lambda x: x['shows'][0], dict) or {}
show_info = self._extract_show_info(show)
entries = [] return {
for episode in (data.get('episodes') or []): 'id': audio_id,
audio_url = self._extract_audio_url(episode) 'display_id': display_id,
if not audio_url: 'title': title,
continue 'description': clean_html(episode.get('html_description') or episode.get('description')),
entries.append(self._extract_episode(episode, audio_url, show_info)) 'duration': int_or_none(episode.get('duration')),
'thumbnail': thumbnail,
return self.playlist_result( 'url': audio_url,
entries, show_slug, show.get('title'), 'vcodec': 'none',
self._extract_description(show)) 'timestamp': int_or_none(episode.get('date_created')),
'season_number': int_or_none(episode.get('season')),
'season_id': str_or_none(episode.get('season_id')),
}
+7 -13
View File
@@ -146,19 +146,18 @@ class SVTPlayIE(SVTPlayBaseIE):
) )
(?P<svt_id>[^/?#&]+)| (?P<svt_id>[^/?#&]+)|
https?://(?:www\.)?(?:svtplay|oppetarkiv)\.se/(?:video|klipp|kanaler)/(?P<id>[^/?#&]+) https?://(?:www\.)?(?:svtplay|oppetarkiv)\.se/(?:video|klipp|kanaler)/(?P<id>[^/?#&]+)
(?:.*?modalId=(?P<modal_id>[\da-zA-Z-]+))?
) )
''' '''
_TESTS = [{ _TESTS = [{
'url': 'https://www.svtplay.se/video/30479064', 'url': 'https://www.svtplay.se/video/26194546/det-har-ar-himlen',
'md5': '2382036fd6f8c994856c323fe51c426e', 'md5': '2382036fd6f8c994856c323fe51c426e',
'info_dict': { 'info_dict': {
'id': '8zVbDPA', 'id': 'jNwpV9P',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Designdrömmar i Stenungsund', 'title': 'Det här är himlen',
'timestamp': 1615770000, 'timestamp': 1586044800,
'upload_date': '20210315', 'upload_date': '20200405',
'duration': 3519, 'duration': 3515,
'thumbnail': r're:^https?://(?:.*[\.-]jpg|www.svtstatic.se/image/.*)$', 'thumbnail': r're:^https?://(?:.*[\.-]jpg|www.svtstatic.se/image/.*)$',
'age_limit': 0, 'age_limit': 0,
'subtitles': { 'subtitles': {
@@ -174,9 +173,6 @@ class SVTPlayIE(SVTPlayBaseIE):
# AssertionError: Expected test_SVTPlay_jNwpV9P.mp4 to be at least 9.77KiB, but it's only 864.00B # AssertionError: Expected test_SVTPlay_jNwpV9P.mp4 to be at least 9.77KiB, but it's only 864.00B
'skip_download': True, 'skip_download': True,
}, },
}, {
'url': 'https://www.svtplay.se/video/30479064/husdrommar/husdrommar-sasong-8-designdrommar-i-stenungsund?modalId=8zVbDPA',
'only_matching': True,
}, { }, {
# geo restricted to Sweden # geo restricted to Sweden
'url': 'http://www.oppetarkiv.se/video/5219710/trollflojten', 'url': 'http://www.oppetarkiv.se/video/5219710/trollflojten',
@@ -223,8 +219,7 @@ class SVTPlayIE(SVTPlayBaseIE):
def _real_extract(self, url): def _real_extract(self, url):
mobj = re.match(self._VALID_URL, url) mobj = re.match(self._VALID_URL, url)
video_id = mobj.group('id') video_id, svt_id = mobj.group('id', 'svt_id')
svt_id = mobj.group('svt_id') or mobj.group('modal_id')
if svt_id: if svt_id:
return self._extract_by_video_id(svt_id) return self._extract_by_video_id(svt_id)
@@ -259,7 +254,6 @@ class SVTPlayIE(SVTPlayBaseIE):
if not svt_id: if not svt_id:
svt_id = self._search_regex( svt_id = self._search_regex(
(r'<video[^>]+data-video-id=["\']([\da-zA-Z-]+)', (r'<video[^>]+data-video-id=["\']([\da-zA-Z-]+)',
r'<[^>]+\bdata-rt=["\']top-area-play-button["\'][^>]+\bhref=["\'][^"\']*video/%s/[^"\']*\bmodalId=([\da-zA-Z-]+)' % re.escape(video_id),
r'["\']videoSvtId["\']\s*:\s*["\']([\da-zA-Z-]+)', r'["\']videoSvtId["\']\s*:\s*["\']([\da-zA-Z-]+)',
r'["\']videoSvtId\\?["\']\s*:\s*\\?["\']([\da-zA-Z-]+)', r'["\']videoSvtId\\?["\']\s*:\s*\\?["\']([\da-zA-Z-]+)',
r'"content"\s*:\s*{.*?"id"\s*:\s*"([\da-zA-Z-]+)"', r'"content"\s*:\s*{.*?"id"\s*:\s*"([\da-zA-Z-]+)"',
+1 -4
View File
@@ -143,10 +143,7 @@ class TikTokIE(TikTokBaseIE):
props_data = try_get(json_data, lambda x: x['props'], expected_type=dict) props_data = try_get(json_data, lambda x: x['props'], expected_type=dict)
# Chech statusCode for success # Chech statusCode for success
status = props_data.get('pageProps').get('statusCode') if props_data.get('pageProps').get('statusCode') == 0:
if status == 0:
return self._extract_aweme(props_data, webpage, url) return self._extract_aweme(props_data, webpage, url)
elif status == 10216:
raise ExtractorError('This video is private', expected=True)
raise ExtractorError('Video not available', video_id=video_id) raise ExtractorError('Video not available', video_id=video_id)
-3
View File
@@ -14,7 +14,6 @@ from ..utils import (
class TrovoBaseIE(InfoExtractor): class TrovoBaseIE(InfoExtractor):
_VALID_URL_BASE = r'https?://(?:www\.)?trovo\.live/' _VALID_URL_BASE = r'https?://(?:www\.)?trovo\.live/'
_HEADERS = {'Origin': 'https://trovo.live'}
def _extract_streamer_info(self, data): def _extract_streamer_info(self, data):
streamer_info = data.get('streamerInfo') or {} streamer_info = data.get('streamerInfo') or {}
@@ -69,7 +68,6 @@ class TrovoIE(TrovoBaseIE):
'format_id': format_id, 'format_id': format_id,
'height': int_or_none(format_id[:-1]) if format_id else None, 'height': int_or_none(format_id[:-1]) if format_id else None,
'url': play_url, 'url': play_url,
'http_headers': self._HEADERS,
}) })
self._sort_formats(formats) self._sort_formats(formats)
@@ -155,7 +153,6 @@ class TrovoVodIE(TrovoBaseIE):
'protocol': 'm3u8_native', 'protocol': 'm3u8_native',
'tbr': int_or_none(play_info.get('bitrate')), 'tbr': int_or_none(play_info.get('bitrate')),
'url': play_url, 'url': play_url,
'http_headers': self._HEADERS,
}) })
self._sort_formats(formats) self._sort_formats(formats)
+1 -10
View File
@@ -9,7 +9,6 @@ from ..utils import (
int_or_none, int_or_none,
remove_start, remove_start,
smuggle_url, smuggle_url,
strip_or_none,
try_get, try_get,
) )
@@ -26,10 +25,6 @@ class TVerIE(InfoExtractor):
}, { }, {
'url': 'https://tver.jp/episode/79622438', 'url': 'https://tver.jp/episode/79622438',
'only_matching': True, 'only_matching': True,
}, {
# subtitle = ' '
'url': 'https://tver.jp/corner/f0068870',
'only_matching': True,
}] }]
_TOKEN = None _TOKEN = None
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s' BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s'
@@ -52,12 +47,8 @@ class TVerIE(InfoExtractor):
} }
if service == 'cx': if service == 'cx':
title = main['title']
subtitle = strip_or_none(main.get('subtitle'))
if subtitle:
title += ' - ' + subtitle
info.update({ info.update({
'title': title, 'title': main.get('subtitle') or main['title'],
'url': 'https://i.fod.fujitv.co.jp/plus7/web/%s/%s.html' % (p_id[:4], p_id), 'url': 'https://i.fod.fujitv.co.jp/plus7/web/%s/%s.html' % (p_id[:4], p_id),
'ie_key': 'FujiTVFODPlus7', 'ie_key': 'FujiTVFODPlus7',
}) })
-6
View File
@@ -23,8 +23,6 @@ class VGTVIE(XstreamIE):
'fvn.no/fvntv': 'fvntv', 'fvn.no/fvntv': 'fvntv',
'aftenposten.no/webtv': 'aptv', 'aftenposten.no/webtv': 'aptv',
'ap.vgtv.no/webtv': 'aptv', 'ap.vgtv.no/webtv': 'aptv',
'tv.aftonbladet.se': 'abtv',
# obsolete URL schemas, kept in order to save one HTTP redirect
'tv.aftonbladet.se/abtv': 'abtv', 'tv.aftonbladet.se/abtv': 'abtv',
'www.aftonbladet.se/tv': 'abtv', 'www.aftonbladet.se/tv': 'abtv',
} }
@@ -142,10 +140,6 @@ class VGTVIE(XstreamIE):
'url': 'http://www.vgtv.no/#!/video/127205/inside-the-mind-of-favela-funk', 'url': 'http://www.vgtv.no/#!/video/127205/inside-the-mind-of-favela-funk',
'only_matching': True, 'only_matching': True,
}, },
{
'url': 'https://tv.aftonbladet.se/video/36015/vulkanutbrott-i-rymden-nu-slapper-nasa-bilderna',
'only_matching': True,
},
{ {
'url': 'http://tv.aftonbladet.se/abtv/articles/36015', 'url': 'http://tv.aftonbladet.se/abtv/articles/36015',
'only_matching': True, 'only_matching': True,
+5 -66
View File
@@ -498,24 +498,6 @@ class VimeoIE(VimeoBaseInfoExtractor):
'url': 'https://vimeo.com/album/2632481/video/79010983', 'url': 'https://vimeo.com/album/2632481/video/79010983',
'only_matching': True, 'only_matching': True,
}, },
{
'url': 'https://vimeo.com/showcase/3253534/video/119195465',
'note': 'A video in a password protected album (showcase)',
'info_dict': {
'id': '119195465',
'ext': 'mp4',
'title': 'youtube-dl test video \'ä"BaW_jenozKc',
'uploader': 'Philipp Hagemeister',
'uploader_id': 'user20132939',
'description': 'md5:fa7b6c6d8db0bdc353893df2f111855b',
'upload_date': '20150209',
'timestamp': 1423518307,
},
'params': {
'format': 'best[protocol=https]',
'videopassword': 'youtube-dl',
},
},
{ {
# source file returns 403: Forbidden # source file returns 403: Forbidden
'url': 'https://vimeo.com/7809605', 'url': 'https://vimeo.com/7809605',
@@ -582,44 +564,6 @@ class VimeoIE(VimeoBaseInfoExtractor):
def _real_initialize(self): def _real_initialize(self):
self._login() self._login()
def _try_album_password(self, url):
album_id = self._search_regex(
r'vimeo\.com/(?:album|showcase)/([^/]+)', url, 'album id', default=None)
if not album_id:
return
viewer = self._download_json(
'https://vimeo.com/_rv/viewer', album_id, fatal=False)
if not viewer:
webpage = self._download_webpage(url, album_id)
viewer = self._parse_json(self._search_regex(
r'bootstrap_data\s*=\s*({.+?})</script>',
webpage, 'bootstrap data'), album_id)['viewer']
jwt = viewer['jwt']
album = self._download_json(
'https://api.vimeo.com/albums/' + album_id,
album_id, headers={'Authorization': 'jwt ' + jwt},
query={'fields': 'description,name,privacy'})
if try_get(album, lambda x: x['privacy']['view']) == 'password':
password = self._downloader.params.get('videopassword')
if not password:
raise ExtractorError(
'This album is protected by a password, use the --video-password option',
expected=True)
self._set_vimeo_cookie('vuid', viewer['vuid'])
try:
self._download_json(
'https://vimeo.com/showcase/%s/auth' % album_id,
album_id, 'Verifying the password', data=urlencode_postdata({
'password': password,
'token': viewer['xsrft'],
}), headers={
'X-Requested-With': 'XMLHttpRequest',
})
except ExtractorError as e:
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
raise ExtractorError('Wrong password', expected=True)
raise
def _real_extract(self, url): def _real_extract(self, url):
url, data = unsmuggle_url(url, {}) url, data = unsmuggle_url(url, {})
headers = std_headers.copy() headers = std_headers.copy()
@@ -647,7 +591,6 @@ class VimeoIE(VimeoBaseInfoExtractor):
elif any(p in url for p in ('play_redirect_hls', 'moogaloop.swf')): elif any(p in url for p in ('play_redirect_hls', 'moogaloop.swf')):
url = 'https://vimeo.com/' + video_id url = 'https://vimeo.com/' + video_id
self._try_album_password(url)
try: try:
# Retrieve video webpage to extract further information # Retrieve video webpage to extract further information
webpage, urlh = self._download_webpage_handle( webpage, urlh = self._download_webpage_handle(
@@ -991,15 +934,11 @@ class VimeoAlbumIE(VimeoBaseInfoExtractor):
} }
if hashed_pass: if hashed_pass:
query['_hashed_pass'] = hashed_pass query['_hashed_pass'] = hashed_pass
try: videos = self._download_json(
videos = self._download_json( 'https://api.vimeo.com/albums/%s/videos' % album_id,
'https://api.vimeo.com/albums/%s/videos' % album_id, album_id, 'Downloading page %d' % api_page, query=query, headers={
album_id, 'Downloading page %d' % api_page, query=query, headers={ 'Authorization': 'jwt ' + authorization,
'Authorization': 'jwt ' + authorization, })['data']
})['data']
except ExtractorError as e:
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
return
for video in videos: for video in videos:
link = video.get('link') link = video.get('link')
if not link: if not link:
+8 -18
View File
@@ -7,8 +7,6 @@ from ..compat import compat_urllib_parse_unquote
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
int_or_none, int_or_none,
try_get,
unified_timestamp,
) )
@@ -21,17 +19,14 @@ class VoxMediaVolumeIE(OnceIE):
setup = self._parse_json(self._search_regex( setup = self._parse_json(self._search_regex(
r'setup\s*=\s*({.+});', webpage, 'setup'), video_id) r'setup\s*=\s*({.+});', webpage, 'setup'), video_id)
player_setup = setup.get('player_setup') or setup video_data = setup.get('video') or {}
video_data = player_setup.get('video') or {}
formatted_metadata = video_data.get('formatted_metadata') or {}
info = { info = {
'id': video_id, 'id': video_id,
'title': player_setup.get('title') or video_data.get('title_short'), 'title': video_data.get('title_short'),
'description': video_data.get('description_long') or video_data.get('description_short'), 'description': video_data.get('description_long') or video_data.get('description_short'),
'thumbnail': formatted_metadata.get('thumbnail') or video_data.get('brightcove_thumbnail'), 'thumbnail': video_data.get('brightcove_thumbnail')
'timestamp': unified_timestamp(formatted_metadata.get('video_publish_date')),
} }
asset = try_get(setup, lambda x: x['embed_assets']['chorus'], dict) or {} asset = setup.get('asset') or setup.get('params') or {}
formats = [] formats = []
hls_url = asset.get('hls_url') hls_url = asset.get('hls_url')
@@ -52,7 +47,6 @@ class VoxMediaVolumeIE(OnceIE):
if formats: if formats:
self._sort_formats(formats) self._sort_formats(formats)
info['formats'] = formats info['formats'] = formats
info['duration'] = int_or_none(asset.get('duration'))
return info return info
for provider_video_type in ('ooyala', 'youtube', 'brightcove'): for provider_video_type in ('ooyala', 'youtube', 'brightcove'):
@@ -90,7 +84,7 @@ class VoxMediaIE(InfoExtractor):
}, { }, {
# Volume embed, Youtube # Volume embed, Youtube
'url': 'http://www.theverge.com/2014/10/21/7025853/google-nexus-6-hands-on-photos-video-android-phablet', 'url': 'http://www.theverge.com/2014/10/21/7025853/google-nexus-6-hands-on-photos-video-android-phablet',
'md5': 'fd19aa0cf3a0eea515d4fd5c8c0e9d68', 'md5': '4c8f4a0937752b437c3ebc0ed24802b5',
'info_dict': { 'info_dict': {
'id': 'Gy8Md3Eky38', 'id': 'Gy8Md3Eky38',
'ext': 'mp4', 'ext': 'mp4',
@@ -99,7 +93,6 @@ class VoxMediaIE(InfoExtractor):
'uploader_id': 'TheVerge', 'uploader_id': 'TheVerge',
'upload_date': '20141021', 'upload_date': '20141021',
'uploader': 'The Verge', 'uploader': 'The Verge',
'timestamp': 1413907200,
}, },
'add_ie': ['Youtube'], 'add_ie': ['Youtube'],
'skip': 'similar to the previous test', 'skip': 'similar to the previous test',
@@ -107,13 +100,13 @@ class VoxMediaIE(InfoExtractor):
# Volume embed, Youtube # Volume embed, Youtube
'url': 'http://www.vox.com/2016/3/31/11336640/mississippi-lgbt-religious-freedom-bill', 'url': 'http://www.vox.com/2016/3/31/11336640/mississippi-lgbt-religious-freedom-bill',
'info_dict': { 'info_dict': {
'id': '22986359b', 'id': 'YCjDnX-Xzhg',
'ext': 'mp4', 'ext': 'mp4',
'title': "Mississippi's laws are so bad that its anti-LGBTQ law isn't needed to allow discrimination", 'title': "Mississippi's laws are so bad that its anti-LGBTQ law isn't needed to allow discrimination",
'description': 'md5:fc1317922057de31cd74bce91eb1c66c', 'description': 'md5:fc1317922057de31cd74bce91eb1c66c',
'uploader_id': 'voxdotcom',
'upload_date': '20150915', 'upload_date': '20150915',
'timestamp': 1442332800, 'uploader': 'Vox',
'duration': 285,
}, },
'add_ie': ['Youtube'], 'add_ie': ['Youtube'],
'skip': 'similar to the previous test', 'skip': 'similar to the previous test',
@@ -167,9 +160,6 @@ class VoxMediaIE(InfoExtractor):
'ext': 'mp4', 'ext': 'mp4',
'title': 'Post-Post-PC CEO: The Full Code Conference Video of Microsoft\'s Satya Nadella', 'title': 'Post-Post-PC CEO: The Full Code Conference Video of Microsoft\'s Satya Nadella',
'description': 'The longtime veteran was chosen earlier this year as the software giant\'s third leader in its history.', 'description': 'The longtime veteran was chosen earlier this year as the software giant\'s third leader in its history.',
'timestamp': 1402938000,
'upload_date': '20140616',
'duration': 4114,
}, },
'add_ie': ['VoxMediaVolume'], 'add_ie': ['VoxMediaVolume'],
}] }]
+11 -14
View File
@@ -182,20 +182,17 @@ class VVVVIDIE(InfoExtractor):
if not embed_code: if not embed_code:
continue continue
embed_code = ds(embed_code) embed_code = ds(embed_code)
if video_type == 'video/kenc': if video_type in ('video/rcs', 'video/kenc'):
embed_code = re.sub(r'https?(://[^/]+)/z/', r'https\1/i/', embed_code).replace('/manifest.f4m', '/master.m3u8') if video_type == 'video/kenc':
kenc = self._download_json( kenc = self._download_json(
'https://www.vvvvid.it/kenc', video_id, query={ 'https://www.vvvvid.it/kenc', video_id, query={
'action': 'kt', 'action': 'kt',
'conn_id': self._conn_id, 'conn_id': self._conn_id,
'url': embed_code, 'url': embed_code,
}, fatal=False) or {} }, fatal=False) or {}
kenc_message = kenc.get('message') kenc_message = kenc.get('message')
if kenc_message: if kenc_message:
embed_code += '?' + ds(kenc_message) embed_code += '?' + ds(kenc_message)
formats.extend(self._extract_m3u8_formats(
embed_code, video_id, 'mp4', m3u8_id='hls', fatal=False))
elif video_type == 'video/rcs':
formats.extend(self._extract_akamai_formats(embed_code, video_id)) formats.extend(self._extract_akamai_formats(embed_code, video_id))
elif video_type == 'video/youtube': elif video_type == 'video/youtube':
info.update({ info.update({
-163
View File
@@ -1,163 +0,0 @@
# coding: utf-8
from __future__ import unicode_literals
import re
from .common import InfoExtractor
from ..utils import (
determine_ext,
parse_duration,
urlencode_postdata,
ExtractorError,
)
class WimTVIE(InfoExtractor):
_player = None
_UUID_RE = r'[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}'
_VALID_URL = r'''(?x)
https?://platform.wim.tv/
(?:
(?:embed/)?\?
|\#/webtv/.+?/
)
(?P<type>vod|live|cast)[=/]
(?P<id>%s).*?''' % _UUID_RE
_TESTS = [{
# vod stream
'url': 'https://platform.wim.tv/embed/?vod=db29fb32-bade-47b6-a3a6-cb69fe80267a',
'md5': 'db29fb32-bade-47b6-a3a6-cb69fe80267a',
'info_dict': {
'id': 'db29fb32-bade-47b6-a3a6-cb69fe80267a',
'ext': 'mp4',
'title': 'AMA SUPERCROSS 2020 - R2 ST. LOUIS',
'duration': 6481,
'thumbnail': r're:https?://.+?/thumbnail/.+?/720$'
},
'params': {
'skip_download': True,
},
}, {
# live stream
'url': 'https://platform.wim.tv/embed/?live=28e22c22-49db-40f3-8c37-8cbb0ff44556&autostart=true',
'info_dict': {
'id': '28e22c22-49db-40f3-8c37-8cbb0ff44556',
'ext': 'mp4',
'title': 'Streaming MSmotorTV',
'is_live': True,
},
'params': {
'skip_download': True,
},
}, {
'url': 'https://platform.wim.tv/#/webtv/automotornews/vod/422492b6-539e-474d-9c6b-68c9d5893365',
'only_matching': True,
}, {
'url': 'https://platform.wim.tv/#/webtv/renzoarborechannel/cast/f47e0d15-5b45-455e-bf0d-dba8ffa96365',
'only_matching': True,
}]
@staticmethod
def _extract_urls(webpage):
return [
mobj.group('url')
for mobj in re.finditer(
r'<iframe[^>]+src=["\'](?P<url>%s)' % WimTVIE._VALID_URL,
webpage)]
def _real_initialize(self):
if not self._player:
self._get_player_data()
def _get_player_data(self):
msg_id = 'Player data'
self._player = {}
datas = [{
'url': 'https://platform.wim.tv/common/libs/player/wimtv/wim-rest.js',
'vars': [{
'regex': r'appAuth = "(.+?)"',
'variable': 'app_auth',
}]
}, {
'url': 'https://platform.wim.tv/common/config/endpointconfig.js',
'vars': [{
'regex': r'PRODUCTION_HOSTNAME_THUMB = "(.+?)"',
'variable': 'thumb_server',
}, {
'regex': r'PRODUCTION_HOSTNAME_THUMB\s*\+\s*"(.+?)"',
'variable': 'thumb_server_path',
}]
}]
for data in datas:
temp = self._download_webpage(data['url'], msg_id)
for var in data['vars']:
val = self._search_regex(var['regex'], temp, msg_id)
if not val:
raise ExtractorError('%s not found' % var['variable'])
self._player[var['variable']] = val
def _generate_token(self):
json = self._download_json(
'https://platform.wim.tv/wimtv-server/oauth/token', 'Token generation',
headers={'Authorization': 'Basic %s' % self._player['app_auth']},
data=urlencode_postdata({'grant_type': 'client_credentials'}))
token = json.get('access_token')
if not token:
raise ExtractorError('access token not generated')
return token
def _generate_thumbnail(self, thumb_id, width='720'):
if not thumb_id or not self._player.get('thumb_server'):
return None
if not self._player.get('thumb_server_path'):
self._player['thumb_server_path'] = ''
return '%s%s/asset/thumbnail/%s/%s' % (
self._player['thumb_server'],
self._player['thumb_server_path'],
thumb_id, width)
def _real_extract(self, url):
urlc = re.match(self._VALID_URL, url).groupdict()
video_id = urlc['id']
stream_type = is_live = None
if urlc['type'] in {'live', 'cast'}:
stream_type = urlc['type'] + '/channel'
is_live = True
else:
stream_type = 'vod'
is_live = False
token = self._generate_token()
json = self._download_json(
'https://platform.wim.tv/wimtv-server/api/public/%s/%s/play' % (
stream_type, video_id), video_id,
headers={'Authorization': 'Bearer %s' % token,
'Content-Type': 'application/json'},
data=bytes('{}', 'utf-8'))
formats = []
for src in json.get('srcs') or []:
if src.get('mimeType') == 'application/x-mpegurl':
formats.extend(
self._extract_m3u8_formats(
src.get('uniqueStreamer'), video_id, 'mp4'))
if src.get('mimeType') == 'video/flash':
formats.append({
'format_id': 'rtmp',
'url': src.get('uniqueStreamer'),
'ext': determine_ext(src.get('uniqueStreamer'), 'flv'),
'rtmp_live': is_live,
})
json = json.get('resource')
thumb = self._generate_thumbnail(json.get('thumbnailId'))
self._sort_formats(formats)
return {
'id': video_id,
'title': json.get('title') or json.get('name'),
'duration': parse_duration(json.get('duration')),
'formats': formats,
'thumbnail': thumb,
'is_live': is_live,
}
+257 -454
View File
@@ -25,9 +25,7 @@ from ..compat import (
) )
from ..jsinterp import JSInterpreter from ..jsinterp import JSInterpreter
from ..utils import ( from ..utils import (
bool_or_none,
clean_html, clean_html,
dict_get,
ExtractorError, ExtractorError,
format_field, format_field,
float_or_none, float_or_none,
@@ -61,9 +59,9 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
_TFA_URL = 'https://accounts.google.com/_/signin/challenge?hl=en&TL={0}' _TFA_URL = 'https://accounts.google.com/_/signin/challenge?hl=en&TL={0}'
_RESERVED_NAMES = ( _RESERVED_NAMES = (
r'channel|c|user|playlist|watch|w|v|embed|e|watch_popup|' r'embed|e|watch_popup|channel|c|user|playlist|watch|w|v|movies|results|shared|hashtag|'
r'movies|results|shared|hashtag|trending|feed|feeds|' r'storefront|oops|index|account|reporthistory|t/terms|about|upload|signin|logout|'
r'storefront|oops|index|account|reporthistory|t/terms|about|upload|signin|logout') r'feed/(?:watch_later|history|subscriptions|library|trending|recommended)')
_NETRC_MACHINE = 'youtube' _NETRC_MACHINE = 'youtube'
# If True it will raise an error if no login info is provided # If True it will raise an error if no login info is provided
@@ -273,21 +271,15 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
if not self._login(): if not self._login():
return return
_YT_WEB_CLIENT_VERSION = '2.20210301.08.00'
_DEFAULT_API_DATA = { _DEFAULT_API_DATA = {
'context': { 'context': {
'client': { 'client': {
'clientName': 'WEB', 'clientName': 'WEB',
'clientVersion': _YT_WEB_CLIENT_VERSION, 'clientVersion': '2.20210301.08.00',
} }
}, },
} }
_DEFAULT_BASIC_API_HEADERS = {
'X-YouTube-Client-Name': '1',
'X-YouTube-Client-Version': _YT_WEB_CLIENT_VERSION
}
_YT_INITIAL_DATA_RE = r'(?:window\s*\[\s*["\']ytInitialData["\']\s*\]|ytInitialData)\s*=\s*({.+?})\s*;' _YT_INITIAL_DATA_RE = r'(?:window\s*\[\s*["\']ytInitialData["\']\s*\]|ytInitialData)\s*=\s*({.+?})\s*;'
_YT_INITIAL_PLAYER_RESPONSE_RE = r'ytInitialPlayerResponse\s*=\s*({.+?})\s*;' _YT_INITIAL_PLAYER_RESPONSE_RE = r'ytInitialPlayerResponse\s*=\s*({.+?})\s*;'
_YT_INITIAL_BOUNDARY_RE = r'(?:var\s+meta|</script|\n)' _YT_INITIAL_BOUNDARY_RE = r'(?:var\s+meta|</script|\n)'
@@ -309,6 +301,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
auth = self._generate_sapisidhash_header() auth = self._generate_sapisidhash_header()
if auth is not None: if auth is not None:
headers.update({'Authorization': auth, 'X-Origin': 'https://www.youtube.com'}) headers.update({'Authorization': auth, 'X-Origin': 'https://www.youtube.com'})
return self._download_json( return self._download_json(
'https://www.youtube.com/youtubei/v1/%s' % ep, 'https://www.youtube.com/youtubei/v1/%s' % ep,
video_id=video_id, fatal=fatal, note=note, errnote=errnote, video_id=video_id, fatal=fatal, note=note, errnote=errnote,
@@ -322,27 +315,6 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
self._YT_INITIAL_DATA_RE), webpage, 'yt initial data'), self._YT_INITIAL_DATA_RE), webpage, 'yt initial data'),
video_id) video_id)
def _extract_identity_token(self, webpage, item_id):
ytcfg = self._extract_ytcfg(item_id, webpage)
if ytcfg:
token = try_get(ytcfg, lambda x: x['ID_TOKEN'], compat_str)
if token:
return token
return self._search_regex(
r'\bID_TOKEN["\']\s*:\s*["\'](.+?)["\']', webpage,
'identity token', default=None)
@staticmethod
def _extract_account_syncid(data):
"""Extract syncId required to download private playlists of secondary channels"""
sync_ids = (
try_get(data, lambda x: x['responseContext']['mainAppWebResponseContext']['datasyncId'], compat_str)
or '').split("||")
if len(sync_ids) >= 2 and sync_ids[1]:
# datasyncid is of the form "channel_syncid||user_syncid" for secondary channel
# and just "user_syncid||" for primary channel. We only want the channel_syncid
return sync_ids[0]
def _extract_ytcfg(self, video_id, webpage): def _extract_ytcfg(self, video_id, webpage):
return self._parse_json( return self._parse_json(
self._search_regex( self._search_regex(
@@ -1490,270 +1462,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
(r'%s\s*%s' % (regex, self._YT_INITIAL_BOUNDARY_RE), (r'%s\s*%s' % (regex, self._YT_INITIAL_BOUNDARY_RE),
regex), webpage, name, default='{}'), video_id, fatal=False) regex), webpage, name, default='{}'), video_id, fatal=False)
@staticmethod
def _join_text_entries(runs):
text = None
for run in runs:
if not isinstance(run, dict):
continue
sub_text = try_get(run, lambda x: x['text'], compat_str)
if sub_text:
if not text:
text = sub_text
continue
text += sub_text
return text
def _extract_comment(self, comment_renderer, parent=None):
comment_id = comment_renderer.get('commentId')
if not comment_id:
return
comment_text_runs = try_get(comment_renderer, lambda x: x['contentText']['runs']) or []
text = self._join_text_entries(comment_text_runs) or ''
comment_time_text = try_get(comment_renderer, lambda x: x['publishedTimeText']['runs']) or []
time_text = self._join_text_entries(comment_time_text)
author = try_get(comment_renderer, lambda x: x['authorText']['simpleText'], compat_str)
author_id = try_get(comment_renderer,
lambda x: x['authorEndpoint']['browseEndpoint']['browseId'], compat_str)
votes = str_to_int(try_get(comment_renderer, (lambda x: x['voteCount']['simpleText'],
lambda x: x['likeCount']), compat_str)) or 0
author_thumbnail = try_get(comment_renderer,
lambda x: x['authorThumbnail']['thumbnails'][-1]['url'], compat_str)
author_is_uploader = try_get(comment_renderer, lambda x: x['authorIsChannelOwner'], bool)
is_liked = try_get(comment_renderer, lambda x: x['isLiked'], bool)
return {
'id': comment_id,
'text': text,
# TODO: This should be parsed to timestamp
'time_text': time_text,
'like_count': votes,
'is_favorited': is_liked,
'author': author,
'author_id': author_id,
'author_thumbnail': author_thumbnail,
'author_is_uploader': author_is_uploader,
'parent': parent or 'root'
}
def _comment_entries(self, root_continuation_data, identity_token, account_syncid,
session_token_list, parent=None, comment_counts=None):
def extract_thread(parent_renderer):
contents = try_get(parent_renderer, lambda x: x['contents'], list) or []
if not parent:
comment_counts[2] = 0
for content in contents:
comment_thread_renderer = try_get(content, lambda x: x['commentThreadRenderer'])
comment_renderer = try_get(
comment_thread_renderer, (lambda x: x['comment']['commentRenderer'], dict)) or try_get(
content, (lambda x: x['commentRenderer'], dict))
if not comment_renderer:
continue
comment = self._extract_comment(comment_renderer, parent)
if not comment:
continue
comment_counts[0] += 1
yield comment
# Attempt to get the replies
comment_replies_renderer = try_get(
comment_thread_renderer, lambda x: x['replies']['commentRepliesRenderer'], dict)
if comment_replies_renderer:
comment_counts[2] += 1
comment_entries_iter = self._comment_entries(
comment_replies_renderer, identity_token, account_syncid,
parent=comment.get('id'), session_token_list=session_token_list,
comment_counts=comment_counts)
for reply_comment in comment_entries_iter:
yield reply_comment
if not comment_counts:
# comment so far, est. total comments, current comment thread #
comment_counts = [0, 0, 0]
headers = self._DEFAULT_BASIC_API_HEADERS.copy()
# TODO: Generalize the download code with TabIE
if identity_token:
headers['x-youtube-identity-token'] = identity_token
if account_syncid:
headers['X-Goog-PageId'] = account_syncid
headers['X-Goog-AuthUser'] = 0
continuation = YoutubeTabIE._extract_continuation(root_continuation_data) # TODO
first_continuation = False
if parent is None:
first_continuation = True
for page_num in itertools.count(0):
if not continuation:
break
retries = self._downloader.params.get('extractor_retries', 3)
count = -1
last_error = None
while count < retries:
count += 1
if last_error:
self.report_warning('%s. Retrying ...' % last_error)
try:
query = {
'ctoken': continuation['ctoken'],
'pbj': 1,
'type': 'next',
}
if parent:
query['action_get_comment_replies'] = 1
else:
query['action_get_comments'] = 1
comment_prog_str = '(%d/%d)' % (comment_counts[0], comment_counts[1])
if page_num == 0:
if first_continuation:
note_prefix = "Downloading initial comment continuation page"
else:
note_prefix = " Downloading comment reply thread %d %s" % (comment_counts[2], comment_prog_str)
else:
note_prefix = "%sDownloading comment%s page %d %s" % (
" " if parent else "",
' replies' if parent else '',
page_num,
comment_prog_str)
browse = self._download_json(
'https://www.youtube.com/comment_service_ajax', None,
'%s %s' % (note_prefix, '(retry #%d)' % count if count else ''),
headers=headers, query=query,
data=urlencode_postdata({
'session_token': session_token_list[0]
}))
except ExtractorError as e:
if isinstance(e.cause, compat_HTTPError) and e.cause.code in (500, 503, 404, 413):
if e.cause.code == 413:
self.report_warning("Assumed end of comments (received HTTP Error 413)")
return
# Downloading page may result in intermittent 5xx HTTP error
# Sometimes a 404 is also recieved. See: https://github.com/ytdl-org/youtube-dl/issues/28289
last_error = 'HTTP Error %s' % e.cause.code
if e.cause.code == 404:
last_error = last_error + " (this API is probably deprecated)"
if count < retries:
continue
raise
else:
session_token = try_get(browse, lambda x: x['xsrf_token'], compat_str)
if session_token:
session_token_list[0] = session_token
response = try_get(browse,
(lambda x: x['response'],
lambda x: x[1]['response'])) or {}
if response.get('continuationContents'):
break
# YouTube sometimes gives reload: now json if something went wrong (e.g. bad auth)
if browse.get('reload'):
raise ExtractorError("Invalid or missing params in continuation request", expected=False)
# TODO: not tested, merged from old extractor
err_msg = browse.get('externalErrorMessage')
if err_msg:
raise ExtractorError('YouTube said: %s' % err_msg, expected=False)
# Youtube sometimes sends incomplete data
# See: https://github.com/ytdl-org/youtube-dl/issues/28194
last_error = 'Incomplete data received'
if count >= retries:
self._downloader.report_error(last_error)
if not response:
break
known_continuation_renderers = {
'itemSectionContinuation': extract_thread,
'commentRepliesContinuation': extract_thread
}
# extract next root continuation from the results
continuation_contents = try_get(
response, lambda x: x['continuationContents'], dict) or {}
for key, value in continuation_contents.items():
if key not in known_continuation_renderers:
continue
continuation_renderer = value
if first_continuation:
first_continuation = False
expected_comment_count = try_get(
continuation_renderer,
(lambda x: x['header']['commentsHeaderRenderer']['countText']['runs'][0]['text'],
lambda x: x['header']['commentsHeaderRenderer']['commentsCount']['runs'][0]['text']),
compat_str)
if expected_comment_count:
comment_counts[1] = str_to_int(expected_comment_count)
self.to_screen("Downloading ~%d comments" % str_to_int(expected_comment_count))
yield comment_counts[1]
# TODO: cli arg.
# 1/True for newest, 0/False for popular (default)
comment_sort_index = int(True)
sort_continuation_renderer = try_get(
continuation_renderer,
lambda x: x['header']['commentsHeaderRenderer']['sortMenu']['sortFilterSubMenuRenderer']['subMenuItems']
[comment_sort_index]['continuation']['reloadContinuationData'], dict)
# If this fails, the initial continuation page
# starts off with popular anyways.
if sort_continuation_renderer:
continuation = YoutubeTabIE._build_continuation_query(
continuation=sort_continuation_renderer.get('continuation'),
ctp=sort_continuation_renderer.get('clickTrackingParams'))
self.to_screen("Sorting comments by %s" % ('popular' if comment_sort_index == 0 else 'newest'))
break
for entry in known_continuation_renderers[key](continuation_renderer):
yield entry
continuation = YoutubeTabIE._extract_continuation(continuation_renderer) # TODO
break
def _extract_comments(self, ytcfg, video_id, contents, webpage, xsrf_token):
"""Entry for comment extraction"""
comments = []
known_entry_comment_renderers = (
'itemSectionRenderer',
)
estimated_total = 0
for entry in contents:
for key, renderer in entry.items():
if key not in known_entry_comment_renderers:
continue
comment_iter = self._comment_entries(
renderer,
identity_token=self._extract_identity_token(webpage, item_id=video_id),
account_syncid=self._extract_account_syncid(ytcfg),
session_token_list=[xsrf_token])
for comment in comment_iter:
if isinstance(comment, int):
estimated_total = comment
continue
comments.append(comment)
break
self.to_screen("Downloaded %d/%d comments" % (len(comments), estimated_total))
return {
'comments': comments,
'comment_count': len(comments),
}
def _real_extract(self, url): def _real_extract(self, url):
url, smuggled_data = unsmuggle_url(url, {}) url, smuggled_data = unsmuggle_url(url, {})
video_id = self._match_id(url) video_id = self._match_id(url)
@@ -1947,7 +1655,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
f['format_id'] = itag f['format_id'] = itag
formats.append(f) formats.append(f)
if self._downloader.params.get('youtube_include_dash_manifest', True): if self._downloader.params.get('youtube_include_dash_manifest'):
dash_manifest_url = streaming_data.get('dashManifestUrl') dash_manifest_url = streaming_data.get('dashManifestUrl')
if dash_manifest_url: if dash_manifest_url:
for f in self._extract_mpd_formats( for f in self._extract_mpd_formats(
@@ -2067,7 +1775,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
'tags': keywords, 'tags': keywords,
'is_live': is_live, 'is_live': is_live,
'playable_in_embed': playability_status.get('playableInEmbed'), 'playable_in_embed': playability_status.get('playableInEmbed'),
'was_live': video_details.get('isLiveContent'), 'was_live': video_details.get('isLiveContent')
} }
pctr = try_get( pctr = try_get(
@@ -2150,7 +1858,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
# This will error if there is no livechat # This will error if there is no livechat
initial_data['contents']['twoColumnWatchNextResults']['conversationBar']['liveChatRenderer']['continuations'][0]['reloadContinuationData']['continuation'] initial_data['contents']['twoColumnWatchNextResults']['conversationBar']['liveChatRenderer']['continuations'][0]['reloadContinuationData']['continuation']
info['subtitles']['live_chat'] = [{ info['subtitles']['live_chat'] = [{
'url': 'https://www.youtube.com/watch?v=%s' % video_id, # url is needed to set cookies
'video_id': video_id, 'video_id': video_id,
'ext': 'json', 'ext': 'json',
'protocol': 'youtube_live_chat_replay', 'protocol': 'youtube_live_chat_replay',
@@ -2285,30 +1992,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
if v: if v:
info[d_k] = v info[d_k] = v
is_private = bool_or_none(video_details.get('isPrivate'))
is_unlisted = bool_or_none(microformat.get('isUnlisted'))
is_membersonly = None
if initial_data and is_private is not None:
is_membersonly = False
contents = try_get(initial_data, lambda x: x['contents']['twoColumnWatchNextResults']['results']['results']['contents'], list)
for content in contents or []:
badges = try_get(content, lambda x: x['videoPrimaryInfoRenderer']['badges'], list)
for badge in badges or []:
label = try_get(badge, lambda x: x['metadataBadgeRenderer']['label']) or ''
if label.lower() == 'members only':
is_membersonly = True
break
if is_membersonly:
break
# TODO: Add this for playlists
info['availability'] = self._availability(
is_private=is_private,
needs_premium=False, # Youtube no longer have premium-only videos?
needs_subscription=is_membersonly,
needs_auth=info['age_limit'] >= 18,
is_unlisted=None if is_private is None else is_unlisted)
# get xsrf for annotations or comments # get xsrf for annotations or comments
get_annotations = self._downloader.params.get('writeannotations', False) get_annotations = self._downloader.params.get('writeannotations', False)
get_comments = self._downloader.params.get('getcomments', False) get_comments = self._downloader.params.get('getcomments', False)
@@ -2341,8 +2024,156 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
errnote='Unable to download video annotations', fatal=False, errnote='Unable to download video annotations', fatal=False,
data=urlencode_postdata({xsrf_field_name: xsrf_token})) data=urlencode_postdata({xsrf_field_name: xsrf_token}))
# Get comments
# TODO: Refactor and move to seperate function
def extract_comments():
expected_video_comment_count = 0
video_comments = []
comment_xsrf = xsrf_token
def find_value(html, key, num_chars=2, separator='"'):
pos_begin = html.find(key) + len(key) + num_chars
pos_end = html.find(separator, pos_begin)
return html[pos_begin: pos_end]
def search_dict(partial, key):
if isinstance(partial, dict):
for k, v in partial.items():
if k == key:
yield v
else:
for o in search_dict(v, key):
yield o
elif isinstance(partial, list):
for i in partial:
for o in search_dict(i, key):
yield o
continuations = []
if initial_data:
try:
ncd = next(search_dict(initial_data, 'nextContinuationData'))
continuations = [ncd['continuation']]
# Handle videos where comments have been disabled entirely
except StopIteration:
pass
def get_continuation(continuation, session_token, replies=False):
query = {
'pbj': 1,
'ctoken': continuation,
}
if replies:
query['action_get_comment_replies'] = 1
else:
query['action_get_comments'] = 1
while True:
content, handle = self._download_webpage_handle(
'https://www.youtube.com/comment_service_ajax',
video_id,
note=False,
expected_status=[413],
data=urlencode_postdata({
'session_token': session_token
}),
query=query,
headers={
'Accept': '*/*',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0',
'X-YouTube-Client-Name': '1',
'X-YouTube-Client-Version': '2.20201202.06.01'
}
)
response_code = handle.getcode()
if (response_code == 200):
return self._parse_json(content, video_id)
if (response_code == 413):
return None
raise ExtractorError('Unexpected HTTP error code: %s' % response_code)
first_continuation = True
chain_msg = ''
self.to_screen('Downloading comments')
while continuations:
continuation = continuations.pop()
comment_response = get_continuation(continuation, comment_xsrf)
if not comment_response:
continue
if list(search_dict(comment_response, 'externalErrorMessage')):
raise ExtractorError('Error returned from server: ' + next(search_dict(comment_response, 'externalErrorMessage')))
if 'continuationContents' not in comment_response['response']:
# Something is wrong here. Youtube won't accept this continuation token for some reason and responds with a user satisfaction dialog (error?)
continue
# not sure if this actually helps
if 'xsrf_token' in comment_response:
comment_xsrf = comment_response['xsrf_token']
item_section = comment_response['response']['continuationContents']['itemSectionContinuation']
if first_continuation:
expected_video_comment_count = int(item_section['header']['commentsHeaderRenderer']['countText']['runs'][0]['text'].replace(' Comments', '').replace('1 Comment', '1').replace(',', ''))
first_continuation = False
if 'contents' not in item_section:
# continuation returned no comments?
# set an empty array as to not break the for loop
item_section['contents'] = []
for meta_comment in item_section['contents']:
comment = meta_comment['commentThreadRenderer']['comment']['commentRenderer']
video_comments.append({
'id': comment['commentId'],
'text': ''.join([c['text'] for c in try_get(comment, lambda x: x['contentText']['runs'], list) or []]),
'time_text': ''.join([c['text'] for c in comment['publishedTimeText']['runs']]),
'author': comment.get('authorText', {}).get('simpleText', ''),
'votes': comment.get('voteCount', {}).get('simpleText', '0'),
'author_thumbnail': comment['authorThumbnail']['thumbnails'][-1]['url'],
'parent': 'root'
})
if 'replies' not in meta_comment['commentThreadRenderer']:
continue
reply_continuations = [rcn['nextContinuationData']['continuation'] for rcn in meta_comment['commentThreadRenderer']['replies']['commentRepliesRenderer']['continuations']]
while reply_continuations:
time.sleep(1)
continuation = reply_continuations.pop()
replies_data = get_continuation(continuation, comment_xsrf, True)
if not replies_data or 'continuationContents' not in replies_data[1]['response']:
continue
if self._downloader.params.get('verbose', False):
chain_msg = ' (chain %s)' % comment['commentId']
self.to_screen('Comments downloaded: %d of ~%d%s' % (len(video_comments), expected_video_comment_count, chain_msg))
reply_comment_meta = replies_data[1]['response']['continuationContents']['commentRepliesContinuation']
for reply_meta in reply_comment_meta.get('contents', {}):
reply_comment = reply_meta['commentRenderer']
video_comments.append({
'id': reply_comment['commentId'],
'text': ''.join([c['text'] for c in reply_comment['contentText']['runs']]),
'time_text': ''.join([c['text'] for c in reply_comment['publishedTimeText']['runs']]),
'author': reply_comment.get('authorText', {}).get('simpleText', ''),
'votes': reply_comment.get('voteCount', {}).get('simpleText', '0'),
'author_thumbnail': reply_comment['authorThumbnail']['thumbnails'][-1]['url'],
'parent': comment['commentId']
})
if 'continuations' not in reply_comment_meta or len(reply_comment_meta['continuations']) == 0:
continue
reply_continuations += [rcn['nextContinuationData']['continuation'] for rcn in reply_comment_meta['continuations']]
self.to_screen('Comments downloaded: %d of ~%d' % (len(video_comments), expected_video_comment_count))
if 'continuations' in item_section:
continuations += [ncd['nextContinuationData']['continuation'] for ncd in item_section['continuations']]
time.sleep(1)
self.to_screen('Total comments downloaded: %d of ~%d' % (len(video_comments), expected_video_comment_count))
return {
'comments': video_comments,
'comment_count': expected_video_comment_count
}
if get_comments: if get_comments:
info['__post_extractor'] = lambda: self._extract_comments(ytcfg, video_id, contents, webpage, xsrf_token) info['__post_extractor'] = extract_comments
self.mark_watched(video_id, player_response) self.mark_watched(video_id, player_response)
@@ -2689,22 +2520,17 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
channel_url, 'channel id') channel_url, 'channel id')
@staticmethod @staticmethod
def _extract_basic_item_renderer(item): def _extract_grid_item_renderer(item):
# Modified from _extract_grid_item_renderer for item_kind in ('Playlist', 'Video', 'Channel'):
known_renderers = ( renderer = item.get('grid%sRenderer' % item_kind)
'playlistRenderer', 'videoRenderer', 'channelRenderer', if renderer:
'gridPlaylistRenderer', 'gridVideoRenderer', 'gridChannelRenderer' return renderer
)
for key, renderer in item.items():
if key not in known_renderers:
continue
return renderer
def _grid_entries(self, grid_renderer): def _grid_entries(self, grid_renderer):
for item in grid_renderer['items']: for item in grid_renderer['items']:
if not isinstance(item, dict): if not isinstance(item, dict):
continue continue
renderer = self._extract_basic_item_renderer(item) renderer = self._extract_grid_item_renderer(item)
if not isinstance(renderer, dict): if not isinstance(renderer, dict):
continue continue
title = try_get( title = try_get(
@@ -2733,7 +2559,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
content = shelf_renderer.get('content') content = shelf_renderer.get('content')
if not isinstance(content, dict): if not isinstance(content, dict):
return return
renderer = content.get('gridRenderer') or content.get('expandedShelfContentsRenderer') renderer = content.get('gridRenderer')
if renderer: if renderer:
# TODO: add support for nested playlists so each shelf is processed # TODO: add support for nested playlists so each shelf is processed
# as separate playlist # as separate playlist
@@ -2775,6 +2601,20 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
continue continue
yield self._extract_video(renderer) yield self._extract_video(renderer)
r""" # Not needed in the new implementation
def _itemSection_entries(self, item_sect_renderer):
for content in item_sect_renderer['contents']:
if not isinstance(content, dict):
continue
renderer = content.get('videoRenderer', {})
if not isinstance(renderer, dict):
continue
video_id = renderer.get('videoId')
if not video_id:
continue
yield self._extract_video(renderer)
"""
def _rich_entries(self, rich_grid_renderer): def _rich_entries(self, rich_grid_renderer):
renderer = try_get( renderer = try_get(
rich_grid_renderer, lambda x: x['content']['videoRenderer'], dict) or {} rich_grid_renderer, lambda x: x['content']['videoRenderer'], dict) or {}
@@ -2873,7 +2713,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
ctp = continuation_ep.get('clickTrackingParams') ctp = continuation_ep.get('clickTrackingParams')
return YoutubeTabIE._build_continuation_query(continuation, ctp) return YoutubeTabIE._build_continuation_query(continuation, ctp)
def _entries(self, tab, item_id, identity_token, account_syncid): def _entries(self, tab, identity_token, item_id):
def extract_entries(parent_renderer): # this needs to called again for continuation to work with feeds def extract_entries(parent_renderer): # this needs to called again for continuation to work with feeds
contents = try_get(parent_renderer, lambda x: x['contents'], list) or [] contents = try_get(parent_renderer, lambda x: x['contents'], list) or []
@@ -2933,10 +2773,6 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
if identity_token: if identity_token:
headers['x-youtube-identity-token'] = identity_token headers['x-youtube-identity-token'] = identity_token
if account_syncid:
headers['X-Goog-PageId'] = account_syncid
headers['X-Goog-AuthUser'] = 0
for page_num in itertools.count(1): for page_num in itertools.count(1):
if not continuation: if not continuation:
break break
@@ -2967,14 +2803,9 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
else: else:
# Youtube sometimes sends incomplete data # Youtube sometimes sends incomplete data
# See: https://github.com/ytdl-org/youtube-dl/issues/28194 # See: https://github.com/ytdl-org/youtube-dl/issues/28194
if dict_get(response, if response.get('continuationContents') or response.get('onResponseReceivedActions'):
('continuationContents', 'onResponseReceivedActions', 'onResponseReceivedEndpoints')):
break break
last_error = 'Incomplete data recieved'
# Youtube may send alerts if there was an issue with the continuation page
self._extract_alerts(response, expected=False)
last_error = 'Incomplete data received'
if count >= retries: if count >= retries:
self._downloader.report_error(last_error) self._downloader.report_error(last_error)
@@ -3006,13 +2837,11 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
'gridPlaylistRenderer': (self._grid_entries, 'items'), 'gridPlaylistRenderer': (self._grid_entries, 'items'),
'gridVideoRenderer': (self._grid_entries, 'items'), 'gridVideoRenderer': (self._grid_entries, 'items'),
'playlistVideoRenderer': (self._playlist_entries, 'contents'), 'playlistVideoRenderer': (self._playlist_entries, 'contents'),
'itemSectionRenderer': (extract_entries, 'contents'), # for feeds 'itemSectionRenderer': (self._playlist_entries, 'contents'),
'richItemRenderer': (extract_entries, 'contents'), # for hashtag 'richItemRenderer': (extract_entries, 'contents'), # for hashtag
'backstagePostThreadRenderer': (self._post_thread_continuation_entries, 'contents')
} }
continuation_items = try_get( continuation_items = try_get(
response, response, lambda x: x['onResponseReceivedActions'][0]['appendContinuationItemsAction']['continuationItems'], list)
lambda x: dict_get(x, ('onResponseReceivedActions', 'onResponseReceivedEndpoints'))[0]['appendContinuationItemsAction']['continuationItems'], list)
continuation_item = try_get(continuation_items, lambda x: x[0], dict) or {} continuation_item = try_get(continuation_items, lambda x: x[0], dict) or {}
video_items_renderer = None video_items_renderer = None
for key, value in continuation_item.items(): for key, value in continuation_item.items():
@@ -3059,7 +2888,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
try_get(owner, lambda x: x['navigationEndpoint']['browseEndpoint']['canonicalBaseUrl'], compat_str)) try_get(owner, lambda x: x['navigationEndpoint']['browseEndpoint']['canonicalBaseUrl'], compat_str))
return {k: v for k, v in uploader.items() if v is not None} return {k: v for k, v in uploader.items() if v is not None}
def _extract_from_tabs(self, item_id, webpage, data, tabs): def _extract_from_tabs(self, item_id, webpage, data, tabs, identity_token):
playlist_id = title = description = channel_url = channel_name = channel_id = None playlist_id = title = description = channel_url = channel_name = channel_id = None
thumbnails_list = tags = [] thumbnails_list = tags = []
@@ -3123,41 +2952,16 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
'channel_id': metadata['uploader_id'], 'channel_id': metadata['uploader_id'],
'channel_url': metadata['uploader_url']}) 'channel_url': metadata['uploader_url']})
return self.playlist_result( return self.playlist_result(
self._entries( self._entries(selected_tab, identity_token, playlist_id),
selected_tab, playlist_id,
self._extract_identity_token(webpage, item_id),
self._extract_account_syncid(data)),
**metadata) **metadata)
def _extract_mix_playlist(self, playlist, playlist_id):
first_id = last_id = None
for page_num in itertools.count(1):
videos = list(self._playlist_entries(playlist))
if not videos:
return
start = next((i for i, v in enumerate(videos) if v['id'] == last_id), -1) + 1
if start >= len(videos):
return
for video in videos[start:]:
if video['id'] == first_id:
self.to_screen('First video %s found again; Assuming end of Mix' % first_id)
return
yield video
first_id = first_id or videos[0]['id']
last_id = videos[-1]['id']
_, data = self._extract_webpage(
'https://www.youtube.com/watch?list=%s&v=%s' % (playlist_id, last_id),
'%s page %d' % (playlist_id, page_num))
playlist = try_get(
data, lambda x: x['contents']['twoColumnWatchNextResults']['playlist']['playlist'], dict)
def _extract_from_playlist(self, item_id, url, data, playlist): def _extract_from_playlist(self, item_id, url, data, playlist):
title = playlist.get('title') or try_get( title = playlist.get('title') or try_get(
data, lambda x: x['titleText']['simpleText'], compat_str) data, lambda x: x['titleText']['simpleText'], compat_str)
playlist_id = playlist.get('playlistId') or item_id playlist_id = playlist.get('playlistId') or item_id
# Inline playlist rendition continuation does not always work
# Delegating everything except mix playlists to regular tab-based playlist URL # at Youtube side, so delegating regular tab-based playlist URL
# processing whenever possible.
playlist_url = urljoin(url, try_get( playlist_url = urljoin(url, try_get(
playlist, lambda x: x['endpoint']['commandMetadata']['webCommandMetadata']['url'], playlist, lambda x: x['endpoint']['commandMetadata']['webCommandMetadata']['url'],
compat_str)) compat_str))
@@ -3165,42 +2969,67 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
return self.url_result( return self.url_result(
playlist_url, ie=YoutubeTabIE.ie_key(), video_id=playlist_id, playlist_url, ie=YoutubeTabIE.ie_key(), video_id=playlist_id,
video_title=title) video_title=title)
return self.playlist_result( return self.playlist_result(
self._extract_mix_playlist(playlist, playlist_id), self._playlist_entries(playlist), playlist_id=playlist_id,
playlist_id=playlist_id, playlist_title=title) playlist_title=title)
def _extract_alerts(self, data, expected=False): @staticmethod
def _extract_alerts(data):
def _real_extract_alerts(): for alert_dict in try_get(data, lambda x: x['alerts'], list) or []:
for alert_dict in try_get(data, lambda x: x['alerts'], list) or []: if not isinstance(alert_dict, dict):
if not isinstance(alert_dict, dict): continue
for renderer in alert_dict:
alert = alert_dict[renderer]
alert_type = alert.get('type')
if not alert_type:
continue continue
for alert in alert_dict.values(): message = try_get(alert, lambda x: x['text']['simpleText'], compat_str)
alert_type = alert.get('type') if message:
if not alert_type: yield alert_type, message
continue for run in try_get(alert, lambda x: x['text']['runs'], list) or []:
message = try_get(alert, lambda x: x['text']['simpleText'], compat_str) message = try_get(run, lambda x: x['text'], compat_str)
if message: if message:
yield alert_type, message yield alert_type, message
for run in try_get(alert, lambda x: x['text']['runs'], list) or []:
message = try_get(run, lambda x: x['text'], compat_str)
if message:
yield alert_type, message
err_msg = None def _extract_identity_token(self, webpage, item_id):
for alert_type, alert_message in _real_extract_alerts(): ytcfg = self._extract_ytcfg(item_id, webpage)
if alert_type.lower() == 'error': if ytcfg:
if err_msg: token = try_get(ytcfg, lambda x: x['ID_TOKEN'], compat_str)
self._downloader.report_warning('YouTube said: %s - %s' % ('ERROR', err_msg)) if token:
err_msg = alert_message return token
return self._search_regex(
r'\bID_TOKEN["\']\s*:\s*["\'](.+?)["\']', webpage,
'identity token', default=None)
def _real_extract(self, url):
item_id = self._match_id(url)
url = compat_urlparse.urlunparse(
compat_urlparse.urlparse(url)._replace(netloc='www.youtube.com'))
is_home = re.match(r'(?P<pre>%s)(?P<post>/?(?![^#?]).*$)' % self._VALID_URL, url)
if is_home is not None and is_home.group('not_channel') is None and item_id != 'feed':
self._downloader.report_warning(
'A channel/user page was given. All the channel\'s videos will be downloaded. '
'To download only the videos in the home page, add a "/featured" to the URL')
url = '%s/videos%s' % (is_home.group('pre'), is_home.group('post') or '')
# Handle both video/playlist URLs
qs = compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
video_id = qs.get('v', [None])[0]
playlist_id = qs.get('list', [None])[0]
if is_home is not None and is_home.group('not_channel') is not None and is_home.group('not_channel').startswith('watch') and not video_id:
if playlist_id:
self._downloader.report_warning('%s is not a valid Youtube URL. Trying to download playlist %s' % (url, playlist_id))
url = 'https://www.youtube.com/playlist?list=%s' % playlist_id
# return self.url_result(playlist_id, ie=YoutubePlaylistIE.ie_key())
else: else:
self._downloader.report_warning('YouTube said: %s - %s' % (alert_type, alert_message)) raise ExtractorError('Unable to recognize tab page')
if video_id and playlist_id:
if self._downloader.params.get('noplaylist'):
self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
return self.url_result(video_id, ie=YoutubeIE.ie_key(), video_id=video_id)
self.to_screen('Downloading playlist %s - add --no-playlist to just download video %s' % (playlist_id, video_id))
if err_msg:
raise ExtractorError('YouTube said: %s' % err_msg, expected=expected)
def _extract_webpage(self, url, item_id):
retries = self._downloader.params.get('extractor_retries', 3) retries = self._downloader.params.get('extractor_retries', 3)
count = -1 count = -1
last_error = 'Incomplete yt initial data recieved' last_error = 'Incomplete yt initial data recieved'
@@ -3212,67 +3041,40 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
self.report_warning('%s. Retrying ...' % last_error) self.report_warning('%s. Retrying ...' % last_error)
webpage = self._download_webpage( webpage = self._download_webpage(
url, item_id, url, item_id,
'Downloading webpage%s' % (' (retry #%d)' % count if count else '')) 'Downloading webpage%s' % ' (retry #%d)' % count if count else '')
identity_token = self._extract_identity_token(webpage, item_id)
data = self._extract_yt_initial_data(item_id, webpage) data = self._extract_yt_initial_data(item_id, webpage)
self._extract_alerts(data, expected=True) err_msg = None
for alert_type, alert_message in self._extract_alerts(data):
if alert_type.lower() == 'error':
if err_msg:
self._downloader.report_warning('YouTube said: %s - %s' % ('ERROR', err_msg))
err_msg = alert_message
else:
self._downloader.report_warning('YouTube said: %s - %s' % (alert_type, alert_message))
if err_msg:
raise ExtractorError('YouTube said: %s' % err_msg, expected=True)
if data.get('contents') or data.get('currentVideoEndpoint'): if data.get('contents') or data.get('currentVideoEndpoint'):
break break
if count >= retries: if count >= retries:
self._downloader.report_error(last_error) self._downloader.report_error(last_error)
return webpage, data
def _real_extract(self, url):
item_id = self._match_id(url)
url = compat_urlparse.urlunparse(
compat_urlparse.urlparse(url)._replace(netloc='www.youtube.com'))
# This is not matched in a channel page with a tab selected
mobj = re.match(r'(?P<pre>%s)(?P<post>/?(?![^#?]).*$)' % self._VALID_URL, url)
mobj = mobj.groupdict() if mobj else {}
if mobj and not mobj.get('not_channel'):
self._downloader.report_warning(
'A channel/user page was given. All the channel\'s videos will be downloaded. '
'To download only the videos in the home page, add a "/featured" to the URL')
url = '%s/videos%s' % (mobj.get('pre'), mobj.get('post') or '')
# Handle both video/playlist URLs
qs = compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
video_id = qs.get('v', [None])[0]
playlist_id = qs.get('list', [None])[0]
if not video_id and (mobj.get('not_channel') or '').startswith('watch'):
if not playlist_id:
# If there is neither video or playlist ids,
# youtube redirects to home page, which is undesirable
raise ExtractorError('Unable to recognize tab page')
self._downloader.report_warning('A video URL was given without video ID. Trying to download playlist %s' % playlist_id)
url = 'https://www.youtube.com/playlist?list=%s' % playlist_id
if video_id and playlist_id:
if self._downloader.params.get('noplaylist'):
self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
return self.url_result(video_id, ie=YoutubeIE.ie_key(), video_id=video_id)
self.to_screen('Downloading playlist %s; add --no-playlist to just download video %s' % (playlist_id, video_id))
webpage, data = self._extract_webpage(url, item_id)
tabs = try_get( tabs = try_get(
data, lambda x: x['contents']['twoColumnBrowseResultsRenderer']['tabs'], list) data, lambda x: x['contents']['twoColumnBrowseResultsRenderer']['tabs'], list)
if tabs: if tabs:
return self._extract_from_tabs(item_id, webpage, data, tabs) return self._extract_from_tabs(item_id, webpage, data, tabs, identity_token)
playlist = try_get( playlist = try_get(
data, lambda x: x['contents']['twoColumnWatchNextResults']['playlist']['playlist'], dict) data, lambda x: x['contents']['twoColumnWatchNextResults']['playlist']['playlist'], dict)
if playlist: if playlist:
return self._extract_from_playlist(item_id, url, data, playlist) return self._extract_from_playlist(item_id, url, data, playlist)
# Fallback to video extraction if no playlist alike page is recognized.
# First check for the current video then try the v attribute of URL query.
video_id = try_get( video_id = try_get(
data, lambda x: x['currentVideoEndpoint']['watchEndpoint']['videoId'], data, lambda x: x['currentVideoEndpoint']['watchEndpoint']['videoId'],
compat_str) or video_id compat_str) or video_id
if video_id: if video_id:
self._downloader.report_warning('Unable to recognize playlist. Downloading just video %s' % video_id)
return self.url_result(video_id, ie=YoutubeIE.ie_key(), video_id=video_id) return self.url_result(video_id, ie=YoutubeIE.ie_key(), video_id=video_id)
# Failed to recognize
raise ExtractorError('Unable to recognize tab page') raise ExtractorError('Unable to recognize tab page')
@@ -3536,6 +3338,7 @@ class YoutubeFeedsInfoExtractor(YoutubeTabIE):
Subclasses must define the _FEED_NAME property. Subclasses must define the _FEED_NAME property.
""" """
_LOGIN_REQUIRED = True _LOGIN_REQUIRED = True
# _MAX_PAGES = 5
_TESTS = [] _TESTS = []
@property @property
+34 -126
View File
@@ -4,7 +4,6 @@ from __future__ import unicode_literals
import re import re
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import compat_str
from ..utils import ( from ..utils import (
int_or_none, int_or_none,
parse_age_limit, parse_age_limit,
@@ -17,34 +16,24 @@ from ..utils import (
class Zee5IE(InfoExtractor): class Zee5IE(InfoExtractor):
_VALID_URL = r'''(?x) _VALID_URL = r'https?://(?:www\.)?zee5\.com/[^#?]*/(?P<display_id>[-\w]+)/(?P<id>[-\d]+)'
(?:
zee5:|
(?:https?://)(?:www\.)?zee5\.com/(?:[^#?]+/)?
(?:
(?:tvshows|kids|zee5originals)(?:/[^#/?]+){3}
|movies/[^#/?]+
)/(?P<display_id>[^#/?]+)/
)
(?P<id>[^#/?]+)/?(?:$|[?#])
'''
_TESTS = [{ _TESTS = [{
'url': 'https://www.zee5.com/movies/details/krishna-the-birth/0-0-63098', 'url': 'https://www.zee5.com/movies/details/krishna-the-birth/0-0-63098',
'info_dict': { 'info_dict': {
'id': '0-0-63098', "id": "0-0-63098",
'ext': 'mp4', "ext": "m3u8",
'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, "average_rating": 4,
'description': str, "description": 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": "https://akamaividz.zee5.com/resources/0-0-63098/list/270x152/0063098_list_80888170.jpg",
'tags': list "tags": list
}, },
'params': { 'params': {
'format': 'bv', 'format': 'bv',
@@ -52,43 +41,37 @@ class Zee5IE(InfoExtractor):
}, { }, {
'url': 'https://zee5.com/tvshows/details/krishna-balram/0-6-1871/episode-1-the-test-of-bramha/0-1-233402', 'url': 'https://zee5.com/tvshows/details/krishna-balram/0-6-1871/episode-1-the-test-of-bramha/0-1-233402',
'info_dict': { 'info_dict': {
'id': '0-1-233402', "id": "0-1-233402",
'ext': 'mp4', 'ext': 'm3u8',
'display_id': 'episode-1-the-test-of-bramha', "display_id": "episode-1-the-test-of-bramha",
'title': 'Episode 1 - The Test Of Bramha', "title": "Episode 1 - The Test Of Bramha",
'duration': 1336, "duration": 1336,
'average_rating': 4, "average_rating": 4,
'description': str, "description": str,
'alt_title': 'Episode 1 - The Test Of Bramha', "alt_title": "Episode 1 - The Test Of Bramha",
'uploader': 'Green Gold', "uploader": "Green Gold",
'release_date': '20090101', "release_date": "20090101",
'upload_date': '20090101', "upload_date": "20090101",
'timestamp': 1230768000, "timestamp": 1230768000,
'thumbnail': 'https://akamaividz.zee5.com/resources/0-1-233402/list/270x152/01233402_list.jpg', "thumbnail": "https://akamaividz.zee5.com/resources/0-1-233402/list/270x152/01233402_list.jpg",
'series': 'Krishna Balram', "series": "Krishna Balram",
'season_number': 1, "season_number": 1,
'episode_number': 1, "episode_number": 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',
'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',
'only_matching': True
}] }]
def _real_extract(self, url): def _real_extract(self, url):
video_id, display_id = re.match(self._VALID_URL, url).group('id', 'display_id') video_id, display_id = re.match(self._VALID_URL, url).group('id', 'display_id')
access_token_request = self._download_json( access_token_request = self._download_json(
'https://useraction.zee5.com/token/platform_tokens.php?platform_name=web_app', 'https://useraction.zee5.com/token/platform_tokens.php?platform_name=web_app',
video_id, note='Downloading access token') video_id, note="Downloading access token")
token_request = self._download_json( token_request = self._download_json(
'https://useraction.zee5.com/tokennd', 'https://useraction.zee5.com/tokennd',
video_id, note='Downloading video token') video_id, note="Downloading video token")
json_data = self._download_json( json_data = self._download_json(
'https://gwapi.zee5.com/content/details/{}?translation=en&country=IN'.format(video_id), 'https://gwapi.zee5.com/content/details/{}?translation=en&country=IN'.format(video_id),
video_id, headers={'X-Access-Token': access_token_request['token']}) video_id, headers={'X-Access-Token': access_token_request['token']})
@@ -128,78 +111,3 @@ class Zee5IE(InfoExtractor):
'episode_number': int_or_none(try_get(json_data, lambda x: x['index'])), 'episode_number': int_or_none(try_get(json_data, lambda x: x['index'])),
'tags': try_get(json_data, lambda x: x['tags'], list) 'tags': try_get(json_data, lambda x: x['tags'], list)
} }
class Zee5SeriesIE(InfoExtractor):
IE_NAME = 'zee5:series'
_VALID_URL = r'''(?x)
(?:
zee5:series:|
(?:https?://)(?:www\.)?zee5\.com/(?:[^#?]+/)?
(?:tvshows|kids|zee5originals)(?:/[^#/?]+){2}/
)
(?P<id>[^#/?]+)/?(?:$|[?#])
'''
_TESTS = [{
'url': 'https://www.zee5.com/kids/kids-shows/krishna-balram/0-6-1871',
'playlist_mincount': 43,
'info_dict': {
'id': '0-6-1871',
},
}, {
'url': 'https://www.zee5.com/tvshows/details/bhabi-ji-ghar-par-hai/0-6-199',
'playlist_mincount': 1500,
'info_dict': {
'id': '0-6-199',
},
}, {
'url': 'https://www.zee5.com/tvshows/details/agent-raghav-crime-branch/0-6-965',
'playlist_mincount': 25,
'info_dict': {
'id': '0-6-965',
},
}, {
'url': 'https://www.zee5.com/ta/tvshows/details/nagabhairavi/0-6-3201',
'playlist_mincount': 3,
'info_dict': {
'id': '0-6-3201',
},
}, {
'url': 'https://www.zee5.com/global/hi/tvshows/details/khwaabon-ki-zamin-par/0-6-270',
'playlist_mincount': 150,
'info_dict': {
'id': '0-6-270',
},
}
]
def _entries(self, show_id):
access_token_request = self._download_json(
'https://useraction.zee5.com/token/platform_tokens.php?platform_name=web_app',
show_id, note='Downloading access token')
headers = {
'X-Access-Token': access_token_request['token'],
'Referer': 'https://www.zee5.com/',
}
show_url = 'https://gwapi.zee5.com/content/tvshow/{}?translation=en&country=IN'.format(show_id)
page_num = 0
show_json = self._download_json(show_url, video_id=show_id, headers=headers)
for season in show_json.get('seasons') or []:
season_id = try_get(season, lambda x: x['id'], compat_str)
next_url = 'https://gwapi.zee5.com/content/tvshow/?season_id={}&type=episode&translation=en&country=IN&on_air=false&asset_subtype=tvshow&page=1&limit=100'.format(season_id)
while next_url:
page_num += 1
episodes_json = self._download_json(
next_url, video_id=show_id, headers=headers,
note='Downloading JSON metadata page %d' % page_num)
for episode in try_get(episodes_json, lambda x: x['episode'], list) or []:
video_id = episode.get('id')
yield self.url_result(
'zee5:%s' % video_id,
ie=Zee5IE.ie_key(), video_id=video_id)
next_url = url_or_none(episodes_json.get('next_episode_api'))
def _real_extract(self, url):
show_id = self._match_id(url)
return self.playlist_result(self._entries(show_id), playlist_id=show_id)
+99 -117
View File
@@ -1,94 +1,93 @@
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
import re
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
int_or_none, int_or_none,
update_url_query,
) )
class ZingMp3BaseIE(InfoExtractor): class ZingMp3BaseInfoExtractor(InfoExtractor):
_VALID_URL_TMPL = r'https?://(?:mp3\.zing|zingmp3)\.vn/(?:%s)/[^/]+/(?P<id>\w+)\.html'
_GEO_COUNTRIES = ['VN']
def _extract_item(self, item, fatal): def _extract_item(self, item, page_type, fatal=True):
item_id = item['id'] error_message = item.get('msg')
title = item.get('name') or item['title'] if error_message:
formats = []
for k, v in (item.get('source') or {}).items():
if not v:
continue
if k in ('mp4', 'hls'):
for res, video_url in v.items():
if not video_url:
continue
if k == 'hls':
formats.extend(self._extract_m3u8_formats(
video_url, item_id, 'mp4',
'm3u8_native', m3u8_id=k, fatal=False))
elif k == 'mp4':
formats.append({
'format_id': 'mp4-' + res,
'url': video_url,
'height': int_or_none(self._search_regex(
r'^(\d+)p', res, 'resolution', default=None)),
})
else:
formats.append({
'ext': 'mp3',
'format_id': k,
'tbr': int_or_none(k),
'url': self._proto_relative_url(v),
'vcodec': 'none',
})
if not formats:
if not fatal: if not fatal:
return return
msg = item['msg'] raise ExtractorError(
if msg == 'Sorry, this content is not available in your country.': '%s returned error: %s' % (self.IE_NAME, error_message),
self.raise_geo_restricted(countries=self._GEO_COUNTRIES) expected=True)
raise ExtractorError(msg, expected=True)
self._sort_formats(formats)
subtitles = None formats = []
lyric = item.get('lyric') for quality, source_url in zip(item.get('qualities') or item.get('quality', []), item.get('source_list') or item.get('source', [])):
if lyric: if not source_url or source_url == 'require vip':
subtitles = { continue
'origin': [{ if not re.match(r'https?://', source_url):
'url': lyric, source_url = '//' + source_url
}], source_url = self._proto_relative_url(source_url, 'http:')
quality_num = int_or_none(quality)
f = {
'format_id': quality,
'url': source_url,
} }
if page_type == 'video':
f.update({
'height': quality_num,
'ext': 'mp4',
})
else:
f.update({
'abr': quality_num,
'ext': 'mp3',
})
formats.append(f)
album = item.get('album') or {} cover = item.get('cover')
return { return {
'id': item_id, 'title': (item.get('name') or item.get('title')).strip(),
'title': title,
'formats': formats, 'formats': formats,
'thumbnail': item.get('thumbnail'), 'thumbnail': 'http:/' + cover if cover else None,
'subtitles': subtitles, 'artist': item.get('artist'),
'duration': int_or_none(item.get('duration')),
'track': title,
'artist': item.get('artists_names'),
'album': album.get('name') or album.get('title'),
'album_artist': album.get('artists_names'),
} }
def _real_extract(self, url): def _extract_player_json(self, player_json_url, id, page_type, playlist_title=None):
page_id = self._match_id(url) player_json = self._download_json(player_json_url, id, 'Downloading Player JSON')
webpage = self._download_webpage( items = player_json['data']
url.replace('://zingmp3.vn/', '://mp3.zing.vn/'), if 'item' in items:
page_id, query={'play_song': 1}) items = items['item']
data_path = self._search_regex(
r'data-xml="([^"]+)', webpage, 'data path') if len(items) == 1:
return self._process_data(self._download_json( # one single song
'https://mp3.zing.vn/xhr' + data_path, page_id)['data']) data = self._extract_item(items[0], page_type)
data['id'] = id
return data
else:
# playlist of songs
entries = []
for i, item in enumerate(items, 1):
entry = self._extract_item(item, page_type, fatal=False)
if not entry:
continue
entry['id'] = '%s-%d' % (id, i)
entries.append(entry)
return {
'_type': 'playlist',
'id': id,
'title': playlist_title,
'entries': entries,
}
class ZingMp3IE(ZingMp3BaseIE): class ZingMp3IE(ZingMp3BaseInfoExtractor):
_VALID_URL = ZingMp3BaseIE._VALID_URL_TMPL % 'bai-hat|video-clip' _VALID_URL = r'https?://mp3\.zing\.vn/(?:bai-hat|album|playlist|video-clip)/[^/]+/(?P<id>\w+)\.html'
_TESTS = [{ _TESTS = [{
'url': 'http://mp3.zing.vn/bai-hat/Xa-Mai-Xa-Bao-Thy/ZWZB9WAB.html', 'url': 'http://mp3.zing.vn/bai-hat/Xa-Mai-Xa-Bao-Thy/ZWZB9WAB.html',
'md5': 'ead7ae13693b3205cbc89536a077daed', 'md5': 'ead7ae13693b3205cbc89536a077daed',
@@ -96,66 +95,49 @@ class ZingMp3IE(ZingMp3BaseIE):
'id': 'ZWZB9WAB', 'id': 'ZWZB9WAB',
'title': 'Xa Mãi Xa', 'title': 'Xa Mãi Xa',
'ext': 'mp3', 'ext': 'mp3',
'thumbnail': r're:^https?://.+\.jpg', 'thumbnail': r're:^https?://.*\.jpg$',
'subtitles': {
'origin': [{
'ext': 'lrc',
}]
},
'duration': 255,
'track': 'Xa Mãi Xa',
'artist': 'Bảo Thy',
'album': 'Special Album',
'album_artist': 'Bảo Thy',
}, },
}, { }, {
'url': 'https://mp3.zing.vn/video-clip/Suong-Hoa-Dua-Loi-K-ICM-RYO/ZO8ZF7C7.html', 'url': 'http://mp3.zing.vn/video-clip/Let-It-Go-Frozen-OST-Sungha-Jung/ZW6BAEA0.html',
'md5': 'e9c972b693aa88301ef981c8151c4343', 'md5': '870295a9cd8045c0e15663565902618d',
'info_dict': { 'info_dict': {
'id': 'ZO8ZF7C7', 'id': 'ZW6BAEA0',
'title': 'Sương Hoa Đưa Lối', 'title': 'Let It Go (Frozen OST)',
'ext': 'mp4', 'ext': 'mp4',
'thumbnail': r're:^https?://.+\.jpg',
'duration': 207,
'track': 'Sương Hoa Đưa Lối',
'artist': 'K-ICM, RYO',
}, },
}, { }, {
'url': 'https://zingmp3.vn/bai-hat/Xa-Mai-Xa-Bao-Thy/ZWZB9WAB.html', 'url': 'http://mp3.zing.vn/album/Lau-Dai-Tinh-Ai-Bang-Kieu-Minh-Tuyet/ZWZBWDAF.html',
'info_dict': {
'_type': 'playlist',
'id': 'ZWZBWDAF',
'title': 'Lâu Đài Tình Ái - Bằng Kiều,Minh Tuyết | Album 320 lossless',
},
'playlist_count': 10,
'skip': 'removed at the request of the owner',
}, {
'url': 'http://mp3.zing.vn/playlist/Duong-Hong-Loan-apollobee/IWCAACCB.html',
'only_matching': True, 'only_matching': True,
}] }]
IE_NAME = 'zingmp3' IE_NAME = 'zingmp3'
IE_DESC = 'mp3.zing.vn' IE_DESC = 'mp3.zing.vn'
def _process_data(self, data): def _real_extract(self, url):
return self._extract_item(data, True) page_id = self._match_id(url)
webpage = self._download_webpage(url, page_id)
class ZingMp3AlbumIE(ZingMp3BaseIE): player_json_url = self._search_regex([
_VALID_URL = ZingMp3BaseIE._VALID_URL_TMPL % 'album|playlist' r'data-xml="([^"]+)',
_TESTS = [{ r'&amp;xmlURL=([^&]+)&'
'url': 'http://mp3.zing.vn/album/Lau-Dai-Tinh-Ai-Bang-Kieu-Minh-Tuyet/ZWZBWDAF.html', ], webpage, 'player xml url')
'info_dict': {
'_type': 'playlist',
'id': 'ZWZBWDAF',
'title': 'Lâu Đài Tình Ái',
},
'playlist_count': 10,
}, {
'url': 'http://mp3.zing.vn/playlist/Duong-Hong-Loan-apollobee/IWCAACCB.html',
'only_matching': True,
}, {
'url': 'https://zingmp3.vn/album/Lau-Dai-Tinh-Ai-Bang-Kieu-Minh-Tuyet/ZWZBWDAF.html',
'only_matching': True,
}]
IE_NAME = 'zingmp3:album'
def _process_data(self, data): playlist_title = None
def entries(): page_type = self._search_regex(r'/(?:html5)?xml/([^/-]+)', player_json_url, 'page type')
for item in (data.get('items') or []): if page_type == 'video':
entry = self._extract_item(item, False) player_json_url = update_url_query(player_json_url, {'format': 'json'})
if entry: else:
yield entry player_json_url = player_json_url.replace('/xml/', '/html5xml/')
info = data.get('info') or {} if page_type == 'album':
return self.playlist_result( playlist_title = self._og_search_title(webpage)
entries(), info.get('id'), info.get('name') or info.get('title'))
return self._extract_player_json(player_json_url, page_id, page_type, playlist_title)
+58 -44
View File
@@ -1,68 +1,82 @@
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
import re
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
int_or_none, int_or_none,
js_to_json, url_or_none,
parse_filesize, parse_filesize,
urlencode_postdata, urlencode_postdata
) )
class ZoomIE(InfoExtractor): class ZoomIE(InfoExtractor):
IE_NAME = 'zoom' IE_NAME = 'zoom'
_VALID_URL = r'(?P<base_url>https?://(?:[^.]+\.)?zoom.us/)rec(?:ording)?/(?:play|share)/(?P<id>[A-Za-z0-9_.-]+)' _VALID_URL = r'https://(?:.*).?zoom.us/rec(?:ording)?/(play|share)/(?P<id>[A-Za-z0-9\-_.]+)'
_TEST = { _TEST = {
'url': 'https://economist.zoom.us/rec/play/dUk_CNBETmZ5VA2BwEl-jjakPpJ3M1pcfVYAPRsoIbEByGsLjUZtaa4yCATQuOL3der8BlTwxQePl_j0.EImBkXzTIaPvdZO5', 'url': 'https://zoom.us/recording/play/SILVuCL4bFtRwWTtOCFQQxAsBQsJljFtm9e4Z_bvo-A8B-nzUSYZRNuPl3qW5IGK',
'md5': 'ab445e8c911fddc4f9adc842c2c5d434',
'info_dict': { 'info_dict': {
'id': 'dUk_CNBETmZ5VA2BwEl-jjakPpJ3M1pcfVYAPRsoIbEByGsLjUZtaa4yCATQuOL3der8BlTwxQePl_j0.EImBkXzTIaPvdZO5', 'md5': '031a5b379f1547a8b29c5c4c837dccf2',
'ext': 'mp4', 'title': "GAZ Transformational Tuesdays W/ Landon & Stapes",
'title': 'China\'s "two sessions" and the new five-year plan', 'id': "SILVuCL4bFtRwWTtOCFQQxAsBQsJljFtm9e4Z_bvo-A8B-nzUSYZRNuPl3qW5IGK",
'ext': "mp4"
} }
} }
def _real_extract(self, url): def _real_extract(self, url):
base_url, play_id = re.match(self._VALID_URL, url).groups() display_id = self._match_id(url)
webpage = self._download_webpage(url, play_id) webpage = self._download_webpage(url, display_id)
try: password_protected = self._search_regex(r'<form[^>]+?id="(password_form)"', webpage, 'password field', fatal=False, default=None)
form = self._form_hidden_inputs('password_form', webpage) if password_protected is not None:
except ExtractorError: self._verify_video_password(url, display_id, webpage)
form = None webpage = self._download_webpage(url, display_id)
if form:
password = self._downloader.params.get('videopassword')
if not password:
raise ExtractorError(
'This video is protected by a passcode, use the --video-password option', expected=True)
is_meeting = form.get('useWhichPasswd') == 'meeting'
validation = self._download_json(
base_url + 'rec/validate%s_passwd' % ('_meet' if is_meeting else ''),
play_id, 'Validating passcode', 'Wrong passcode', data=urlencode_postdata({
'id': form[('meet' if is_meeting else 'file') + 'Id'],
'passwd': password,
'action': form.get('action'),
}))
if not validation.get('status'):
raise ExtractorError(validation['errorMessage'], expected=True)
webpage = self._download_webpage(url, play_id)
data = self._parse_json(self._search_regex( video_url = self._search_regex(r"viewMp4Url: \'(.*)\'", webpage, 'video url')
r'(?s)window\.__data__\s*=\s*({.+?});', title = self._html_search_regex([r"topic: \"(.*)\",", r"<title>(.*) - Zoom</title>"], webpage, 'title')
webpage, 'data'), play_id, js_to_json) viewResolvtionsWidth = self._search_regex(r"viewResolvtionsWidth: (\d*)", webpage, 'res width', fatal=False)
viewResolvtionsHeight = self._search_regex(r"viewResolvtionsHeight: (\d*)", webpage, 'res height', fatal=False)
fileSize = parse_filesize(self._search_regex(r"fileSize: \'(.+)\'", webpage, 'fileSize', fatal=False))
urlprefix = url.split("zoom.us")[0] + "zoom.us/"
formats = []
formats.append({
'url': url_or_none(video_url),
'width': int_or_none(viewResolvtionsWidth),
'height': int_or_none(viewResolvtionsHeight),
'http_headers': {'Accept': 'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5',
'Referer': urlprefix},
'ext': "mp4",
'filesize_approx': int_or_none(fileSize)
})
self._sort_formats(formats)
return { return {
'id': play_id, 'id': display_id,
'title': data['topic'], 'title': title,
'url': data['viewMp4Url'], 'formats': formats
'width': int_or_none(data.get('viewResolvtionsWidth')),
'height': int_or_none(data.get('viewResolvtionsHeight')),
'http_headers': {
'Referer': base_url,
},
'filesize_approx': parse_filesize(data.get('fileSize')),
} }
def _verify_video_password(self, url, video_id, webpage):
password = self._downloader.params.get('videopassword')
if password is None:
raise ExtractorError('This video is protected by a password, use the --video-password option', expected=True)
meetId = self._search_regex(r'<input[^>]+?id="meetId" value="([^\"]+)"', webpage, 'meetId')
data = urlencode_postdata({
'id': meetId,
'passwd': password,
'action': "viewdetailedpage",
'recaptcha': ""
})
validation_url = url.split("zoom.us")[0] + "zoom.us/rec/validate_meet_passwd"
validation_response = self._download_json(
validation_url, video_id,
note='Validating Password...',
errnote='Wrong password?',
data=data)
if validation_response['errorCode'] != 0:
raise ExtractorError('Login failed, %s said: %r' % (self.IE_NAME, validation_response['errorMessage']))
+28 -51
View File
@@ -214,11 +214,12 @@ def parseOpts(overrideArguments=None):
help='Mark videos watched (YouTube only)') help='Mark videos watched (YouTube only)')
general.add_option( general.add_option(
'--no-mark-watched', '--no-mark-watched',
action='store_false', dest='mark_watched', action='store_false', dest='mark_watched', default=False,
help='Do not mark videos watched (default)') help='Do not mark videos watched')
general.add_option( general.add_option(
'--no-colors', '--no-colors',
action='store_true', dest='no_color', default=False, action='store_true', dest='no_color',
default=False,
help='Do not emit color codes in output') help='Do not emit color codes in output')
network = optparse.OptionGroup(parser, 'Network Options') network = optparse.OptionGroup(parser, 'Network Options')
@@ -533,11 +534,11 @@ def parseOpts(overrideArguments=None):
subtitles.add_option( subtitles.add_option(
'--write-auto-subs', '--write-automatic-subs', '--write-auto-subs', '--write-automatic-subs',
action='store_true', dest='writeautomaticsub', default=False, action='store_true', dest='writeautomaticsub', default=False,
help='Write automatically generated subtitle file (Alias: --write-automatic-subs)') help='Write automatically generated subtitle file (YouTube only)')
subtitles.add_option( subtitles.add_option(
'--no-write-auto-subs', '--no-write-automatic-subs', '--no-write-auto-subs', '--no-write-automatic-subs',
action='store_false', dest='writeautomaticsub', default=False, action='store_false', dest='writeautomaticsub', default=False,
help='Do not write auto-generated subtitles (default) (Alias: --no-write-automatic-subs)') help='Do not write automatically generated subtitle file (default)')
subtitles.add_option( subtitles.add_option(
'--all-subs', '--all-subs',
action='store_true', dest='allsubtitles', default=False, action='store_true', dest='allsubtitles', default=False,
@@ -551,16 +552,12 @@ def parseOpts(overrideArguments=None):
action='store', dest='subtitlesformat', metavar='FORMAT', default='best', action='store', dest='subtitlesformat', metavar='FORMAT', default='best',
help='Subtitle format, accepts formats preference, for example: "srt" or "ass/srt/best"') help='Subtitle format, accepts formats preference, for example: "srt" or "ass/srt/best"')
subtitles.add_option( subtitles.add_option(
'--sub-langs', '--srt-langs', '--sub-lang', '--sub-langs', '--srt-lang',
action='callback', dest='subtitleslangs', metavar='LANGS', type='str', action='callback', dest='subtitleslangs', metavar='LANGS', type='str',
default=[], callback=_comma_separated_values_options_callback, default=[], callback=_comma_separated_values_options_callback,
help='Languages of the subtitles to download (optional) separated by commas, use --list-subs for available language tags') help='Languages of the subtitles to download (optional) separated by commas, use --list-subs for available language tags')
downloader = optparse.OptionGroup(parser, 'Download Options') downloader = optparse.OptionGroup(parser, 'Download Options')
downloader.add_option(
'-N', '--concurrent-fragments',
dest='concurrent_fragment_downloads', metavar='N', default=1, type=int,
help='Number of fragments to download concurrently (default is %default)')
downloader.add_option( downloader.add_option(
'-r', '--limit-rate', '--rate-limit', '-r', '--limit-rate', '--rate-limit',
dest='ratelimit', metavar='RATE', dest='ratelimit', metavar='RATE',
@@ -679,7 +676,7 @@ def parseOpts(overrideArguments=None):
workarounds.add_option( workarounds.add_option(
'--prefer-insecure', '--prefer-unsecure', '--prefer-insecure', '--prefer-unsecure',
action='store_true', dest='prefer_insecure', action='store_true', dest='prefer_insecure',
help='Use an unencrypted connection to retrieve information about the video (Currently supported only for YouTube)') help='Use an unencrypted connection to retrieve information about the video. (Currently supported only for YouTube)')
workarounds.add_option( workarounds.add_option(
'--user-agent', '--user-agent',
metavar='UA', dest='user_agent', metavar='UA', dest='user_agent',
@@ -707,13 +704,17 @@ def parseOpts(overrideArguments=None):
'--sleep-interval', '--min-sleep-interval', metavar='SECONDS', '--sleep-interval', '--min-sleep-interval', metavar='SECONDS',
dest='sleep_interval', type=float, dest='sleep_interval', type=float,
help=( help=(
'Number of seconds to sleep before each download. ' 'Number of seconds to sleep before each download when used alone '
'This is the minimum time to sleep when used along with --max-sleep-interval ' 'or a lower bound of a range for randomized sleep before each download '
'(Alias: --min-sleep-interval)')) '(minimum possible number of seconds to sleep) when used along with '
'--max-sleep-interval'))
workarounds.add_option( workarounds.add_option(
'--max-sleep-interval', metavar='SECONDS', '--max-sleep-interval', metavar='SECONDS',
dest='max_sleep_interval', type=float, dest='max_sleep_interval', type=float,
help='Maximum number of seconds to sleep. Can only be used along with --min-sleep-interval') help=(
'Upper bound of a range for randomized sleep before each download '
'(maximum possible number of seconds to sleep). Must only be used '
'along with --min-sleep-interval'))
workarounds.add_option( workarounds.add_option(
'--sleep-subtitles', metavar='SECONDS', '--sleep-subtitles', metavar='SECONDS',
dest='sleep_interval_subtitles', default=0, type=int, dest='sleep_interval_subtitles', default=0, type=int,
@@ -735,7 +736,7 @@ def parseOpts(overrideArguments=None):
verbosity.add_option( verbosity.add_option(
'--skip-download', '--no-download', '--skip-download', '--no-download',
action='store_true', dest='skip_download', default=False, action='store_true', dest='skip_download', default=False,
help='Do not download the video but write all related files (Alias: --no-download)') help='Do not download the video')
verbosity.add_option( verbosity.add_option(
'-g', '--get-url', '-g', '--get-url',
action='store_true', dest='geturl', default=False, action='store_true', dest='geturl', default=False,
@@ -978,17 +979,9 @@ def parseOpts(overrideArguments=None):
filesystem.add_option( filesystem.add_option(
'--no-write-playlist-metafiles', '--no-write-playlist-metafiles',
action='store_false', dest='allow_playlist_files', action='store_false', dest='allow_playlist_files',
help='Do not write playlist metadata when using --write-info-json, --write-description etc.')
filesystem.add_option(
'--clean-infojson',
action='store_true', dest='clean_infojson', default=True,
help=( help=(
'Remove some private fields such as filenames from the infojson. ' 'Do not write playlist metadata when using '
'Note that it could still contain some personal information (default)')) '--write-info-json, --write-description etc.'))
filesystem.add_option(
'--no-clean-infojson',
action='store_false', dest='clean_infojson',
help='Write all fields to the infojson')
filesystem.add_option( filesystem.add_option(
'--get-comments', '--get-comments',
action='store_true', dest='getcomments', default=False, action='store_true', dest='getcomments', default=False,
@@ -1090,12 +1083,12 @@ def parseOpts(overrideArguments=None):
'Specify the postprocessor/executable name and the arguments separated by a colon ":" ' 'Specify the postprocessor/executable name and the arguments separated by a colon ":" '
'to give the argument to the specified postprocessor/executable. Supported postprocessors are: ' 'to give the argument to the specified postprocessor/executable. Supported postprocessors are: '
'SponSkrub, ExtractAudio, VideoRemuxer, VideoConvertor, EmbedSubtitle, Metadata, Merger, ' 'SponSkrub, ExtractAudio, VideoRemuxer, VideoConvertor, EmbedSubtitle, Metadata, Merger, '
'FixupStretched, FixupM4a, FixupM3u8, SubtitlesConvertor, EmbedThumbnail and SplitChapters. ' 'FixupStretched, FixupM4a, FixupM3u8, SubtitlesConvertor and EmbedThumbnail. '
'The supported executables are: SponSkrub, FFmpeg, FFprobe, and AtomicParsley. ' 'The supported executables are: SponSkrub, FFmpeg, FFprobe, and AtomicParsley. '
'You can also specify "PP+EXE:ARGS" to give the arguments to the specified executable ' 'You can also specify "PP+EXE:ARGS" to give the arguments to the specified executable '
'only when being used by the specified postprocessor. Additionally, for ffmpeg/ffprobe, ' 'only when being used by the specified postprocessor. Additionally, for ffmpeg/ffprobe, '
'"_i"/"_o" can be appended to the prefix optionally followed by a number to pass the argument ' 'a number can be appended to the exe name seperated by "_i" to pass the argument '
'before the specified input/output file. Eg: --ppa "Merger+ffmpeg_i1:-v quiet". ' 'before the specified input file. Eg: --ppa "Merger+ffmpeg_i1:-v quiet". '
'You can use this option multiple times to give different arguments to different ' 'You can use this option multiple times to give different arguments to different '
'postprocessors. (Alias: --ppa)')) 'postprocessors. (Alias: --ppa)'))
postproc.add_option( postproc.add_option(
@@ -1147,18 +1140,13 @@ def parseOpts(overrideArguments=None):
metavar='FIELD:FORMAT', dest='metafromfield', action='append', metavar='FIELD:FORMAT', dest='metafromfield', action='append',
help=( help=(
'Parse additional metadata like title/artist from other fields. ' 'Parse additional metadata like title/artist from other fields. '
'Give a template or field name to extract data from and the ' 'Give field name to extract data from, and format of the field seperated by a ":". '
'format to interpret it as, seperated by a ":". '
'Either regular expression with named capture groups or a ' 'Either regular expression with named capture groups or a '
'similar syntax to the output template can be used for the FORMAT. ' 'similar syntax to the output template can also be used. '
'Similarly, the syntax for output template can be used for FIELD ' 'The parsed parameters replace any existing values and can be use in output template. '
'to parse the data from multiple fields. '
'The parsed parameters replace any existing values and can be used in output templates. '
'This option can be used multiple times. ' 'This option can be used multiple times. '
'Example: --parse-metadata "title:%(artist)s - %(title)s" matches a title like ' 'Example: --parse-metadata "title:%(artist)s - %(title)s" matches a title like '
'"Coldplay - Paradise". ' '"Coldplay - Paradise". '
'Example: --parse-metadata "%(series)s %(episode_number)s:%(title)s" '
'sets the title using series and episode number. '
'Example (regex): --parse-metadata "description:Artist - (?P<artist>.+?)"')) 'Example (regex): --parse-metadata "description:Artist - (?P<artist>.+?)"'))
postproc.add_option( postproc.add_option(
'--xattrs', '--xattrs',
@@ -1166,7 +1154,7 @@ def parseOpts(overrideArguments=None):
help='Write metadata to the video file\'s xattrs (using dublin core and xdg standards)') help='Write metadata to the video file\'s xattrs (using dublin core and xdg standards)')
postproc.add_option( postproc.add_option(
'--fixup', '--fixup',
metavar='POLICY', dest='fixup', default=None, metavar='POLICY', dest='fixup', default='detect_or_warn',
help=( help=(
'Automatically correct known faults of the file. ' 'Automatically correct known faults of the file. '
'One of never (do nothing), warn (only emit a warning), ' 'One of never (do nothing), warn (only emit a warning), '
@@ -1188,20 +1176,9 @@ def parseOpts(overrideArguments=None):
metavar='CMD', dest='exec_cmd', metavar='CMD', dest='exec_cmd',
help='Execute a command on the file after downloading and post-processing, similar to find\'s -exec syntax. Example: --exec \'adb push {} /sdcard/Music/ && rm {}\'') help='Execute a command on the file after downloading and post-processing, similar to find\'s -exec syntax. Example: --exec \'adb push {} /sdcard/Music/ && rm {}\'')
postproc.add_option( postproc.add_option(
'--convert-subs', '--convert-sub', '--convert-subtitles', '--convert-subs', '--convert-subtitles',
metavar='FORMAT', dest='convertsubtitles', default=None, metavar='FORMAT', dest='convertsubtitles', default=None,
help='Convert the subtitles to another format (currently supported: srt|ass|vtt|lrc) (Alias: --convert-subtitles)') help='Convert the subtitles to other format (currently supported: srt|ass|vtt|lrc)')
postproc.add_option(
'--split-chapters', '--split-tracks',
dest='split_chapters', action='store_true', default=False,
help=(
'Split video into multiple files based on internal chapters. '
'The "chapter:" prefix can be used with "--paths" and "--output" to '
'set the output filename for the split files. See "OUTPUT TEMPLATE" for details'))
postproc.add_option(
'--no-split-chapters', '--no-split-tracks',
dest='split_chapters', action='store_false',
help='Do not split video based on chapters (default)')
sponskrub = optparse.OptionGroup(parser, 'SponSkrub (SponsorBlock) Options', description=( sponskrub = optparse.OptionGroup(parser, 'SponSkrub (SponsorBlock) Options', description=(
'SponSkrub (https://github.com/yt-dlp/SponSkrub) is a utility to mark/remove sponsor segments ' 'SponSkrub (https://github.com/yt-dlp/SponSkrub) is a utility to mark/remove sponsor segments '
-2
View File
@@ -13,7 +13,6 @@ from .ffmpeg import (
FFmpegVideoConvertorPP, FFmpegVideoConvertorPP,
FFmpegVideoRemuxerPP, FFmpegVideoRemuxerPP,
FFmpegSubtitlesConvertorPP, FFmpegSubtitlesConvertorPP,
FFmpegSplitChaptersPP,
) )
from .xattrpp import XAttrMetadataPP from .xattrpp import XAttrMetadataPP
from .execafterdownload import ExecAfterDownloadPP from .execafterdownload import ExecAfterDownloadPP
@@ -32,7 +31,6 @@ __all__ = [
'ExecAfterDownloadPP', 'ExecAfterDownloadPP',
'FFmpegEmbedSubtitlePP', 'FFmpegEmbedSubtitlePP',
'FFmpegExtractAudioPP', 'FFmpegExtractAudioPP',
'FFmpegSplitChaptersPP',
'FFmpegFixupM3u8PP', 'FFmpegFixupM3u8PP',
'FFmpegFixupM4aPP', 'FFmpegFixupM4aPP',
'FFmpegFixupStretchedPP', 'FFmpegFixupStretchedPP',
+2 -10
View File
@@ -91,18 +91,10 @@ class PostProcessor(object):
except Exception: except Exception:
self.report_warning(errnote) self.report_warning(errnote)
def _configuration_args(self, exe, keys=None, default=[], use_compat=True): def _configuration_args(self, *args, **kwargs):
pp_key = self.pp_key().lower()
exe = exe.lower()
root_key = exe if pp_key == exe else '%s+%s' % (pp_key, exe)
keys = ['%s%s' % (root_key, k) for k in (keys or [''])]
if root_key in keys:
keys += [root_key] + ([] if pp_key == exe else [(self.pp_key(), exe)]) + ['default']
else:
use_compat = False
return cli_configuration_args( return cli_configuration_args(
self._downloader.params.get('postprocessor_args'), self._downloader.params.get('postprocessor_args'),
keys, default, use_compat) self.pp_key().lower(), *args, **kwargs)
class AudioConversionError(PostProcessingError): class AudioConversionError(PostProcessingError):
+4 -11
View File
@@ -47,7 +47,7 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
self.to_screen('There aren\'t any thumbnails to embed') self.to_screen('There aren\'t any thumbnails to embed')
return [], info return [], info
initial_thumbnail = original_thumbnail = thumbnail_filename = info['thumbnails'][-1]['filepath'] original_thumbnail = thumbnail_filename = info['thumbnails'][-1]['filename']
if not os.path.exists(encodeFilename(thumbnail_filename)): if not os.path.exists(encodeFilename(thumbnail_filename)):
self.report_warning('Skipping embedding the thumbnail because the file is missing.') self.report_warning('Skipping embedding the thumbnail because the file is missing.')
@@ -65,8 +65,6 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
if thumbnail_ext != 'webp' and is_webp(thumbnail_filename): if thumbnail_ext != 'webp' and is_webp(thumbnail_filename):
self.to_screen('Correcting extension to webp and escaping path for thumbnail "%s"' % thumbnail_filename) self.to_screen('Correcting extension to webp and escaping path for thumbnail "%s"' % thumbnail_filename)
thumbnail_webp_filename = replace_extension(thumbnail_filename, 'webp') thumbnail_webp_filename = replace_extension(thumbnail_filename, 'webp')
if os.path.exists(thumbnail_webp_filename):
os.remove(thumbnail_webp_filename)
os.rename(encodeFilename(thumbnail_filename), encodeFilename(thumbnail_webp_filename)) os.rename(encodeFilename(thumbnail_filename), encodeFilename(thumbnail_webp_filename))
original_thumbnail = thumbnail_filename = thumbnail_webp_filename original_thumbnail = thumbnail_filename = thumbnail_webp_filename
thumbnail_ext = 'webp' thumbnail_ext = 'webp'
@@ -87,8 +85,6 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
thumbnail_filename = thumbnail_jpg_filename thumbnail_filename = thumbnail_jpg_filename
thumbnail_ext = 'jpg' thumbnail_ext = 'jpg'
mtime = os.stat(encodeFilename(filename)).st_mtime
success = True success = True
if info['ext'] == 'mp3': if info['ext'] == 'mp3':
options = [ options = [
@@ -135,7 +131,7 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
x for x in ['AtomicParsley', 'atomicparsley'] x for x in ['AtomicParsley', 'atomicparsley']
if check_executable(x, ['-v'])), None) if check_executable(x, ['-v'])), None)
if atomicparsley is None: if atomicparsley is None:
raise EmbedThumbnailPPError('AtomicParsley was not found. Please install') raise EmbedThumbnailPPError('AtomicParsley was not found. Please install.')
cmd = [encodeFilename(atomicparsley, True), cmd = [encodeFilename(atomicparsley, True),
encodeFilename(filename, True), encodeFilename(filename, True),
@@ -143,7 +139,7 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
encodeFilename(thumbnail_filename, True), encodeFilename(thumbnail_filename, True),
encodeArgument('-o'), encodeArgument('-o'),
encodeFilename(temp_filename, True)] encodeFilename(temp_filename, True)]
cmd += [encodeArgument(o) for o in self._configuration_args('AtomicParsley')] cmd += [encodeArgument(o) for o in self._configuration_args(exe='AtomicParsley')]
self.to_screen('Adding thumbnail to "%s"' % filename) self.to_screen('Adding thumbnail to "%s"' % filename)
self.write_debug('AtomicParsley command line: %s' % shell_quote(cmd)) self.write_debug('AtomicParsley command line: %s' % shell_quote(cmd))
@@ -191,13 +187,10 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
os.remove(encodeFilename(filename)) os.remove(encodeFilename(filename))
os.rename(encodeFilename(temp_filename), encodeFilename(filename)) os.rename(encodeFilename(temp_filename), encodeFilename(filename))
self.try_utime(filename, mtime, mtime)
files_to_delete = [thumbnail_filename] files_to_delete = [thumbnail_filename]
if self._already_have_thumbnail: if self._already_have_thumbnail:
info['__files_to_move'][original_thumbnail] = replace_extension( info['__files_to_move'][original_thumbnail] = replace_extension(
info['__files_to_move'][initial_thumbnail], info['__thumbnail_filename'], os.path.splitext(original_thumbnail)[1][1:])
os.path.splitext(original_thumbnail)[1][1:])
if original_thumbnail == thumbnail_filename: if original_thumbnail == thumbnail_filename:
files_to_delete = [] files_to_delete = []
elif original_thumbnail != thumbnail_filename: elif original_thumbnail != thumbnail_filename:
+20 -72
View File
@@ -10,7 +10,6 @@ import json
from .common import AudioConversionError, PostProcessor from .common import AudioConversionError, PostProcessor
from ..compat import compat_str
from ..utils import ( from ..utils import (
encodeArgument, encodeArgument,
encodeFilename, encodeFilename,
@@ -19,6 +18,7 @@ from ..utils import (
PostProcessingError, PostProcessingError,
prepend_extension, prepend_extension,
shell_quote, shell_quote,
subtitles_filename,
dfxp2srt, dfxp2srt,
ISO639Utils, ISO639Utils,
process_communicate_or_kill, process_communicate_or_kill,
@@ -61,7 +61,7 @@ class FFmpegPostProcessor(PostProcessor):
def check_version(self): def check_version(self):
if not self.available: if not self.available:
raise FFmpegPostProcessorError('ffmpeg not found. Please install or provide the path using --ffmpeg-location') raise FFmpegPostProcessorError('ffmpeg not found. Please install')
required_version = '10-0' if self.basename == 'avconv' else '1.0' required_version = '10-0' if self.basename == 'avconv' else '1.0'
if is_outdated_version( if is_outdated_version(
@@ -165,7 +165,7 @@ class FFmpegPostProcessor(PostProcessor):
def get_audio_codec(self, path): def get_audio_codec(self, path):
if not self.probe_available and not self.available: if not self.probe_available and not self.available:
raise PostProcessingError('ffprobe and ffmpeg not found. Please install or provide the path using --ffmpeg-location') raise PostProcessingError('ffprobe and ffmpeg not found. Please install')
try: try:
if self.probe_available: if self.probe_available:
cmd = [ cmd = [
@@ -207,7 +207,7 @@ class FFmpegPostProcessor(PostProcessor):
if self.probe_basename != 'ffprobe': if self.probe_basename != 'ffprobe':
if self.probe_available: if self.probe_available:
self.report_warning('Only ffprobe is supported for metadata extraction') self.report_warning('Only ffprobe is supported for metadata extraction')
raise PostProcessingError('ffprobe not found. Please install or provide the path using --ffmpeg-location') raise PostProcessingError('ffprobe not found. Please install.')
self.check_version() self.check_version()
cmd = [ cmd = [
@@ -234,35 +234,25 @@ class FFmpegPostProcessor(PostProcessor):
return num, len(streams) return num, len(streams)
def run_ffmpeg_multiple_files(self, input_paths, out_path, opts): def run_ffmpeg_multiple_files(self, input_paths, out_path, opts):
return self.real_run_ffmpeg(
[(path, []) for path in input_paths],
[(out_path, opts)])
def real_run_ffmpeg(self, input_path_opts, output_path_opts):
self.check_version() self.check_version()
oldest_mtime = min( oldest_mtime = min(
os.stat(encodeFilename(path)).st_mtime for path, _ in input_path_opts) os.stat(encodeFilename(path)).st_mtime for path in input_paths)
cmd = [encodeFilename(self.executable, True), encodeArgument('-y')] cmd = [encodeFilename(self.executable, True), encodeArgument('-y')]
# avconv does not have repeat option # avconv does not have repeat option
if self.basename == 'ffmpeg': if self.basename == 'ffmpeg':
cmd += [encodeArgument('-loglevel'), encodeArgument('repeat+info')] cmd += [encodeArgument('-loglevel'), encodeArgument('repeat+info')]
def make_args(file, args, name, number): def make_args(file, pre=[], post=[], *args, **kwargs):
keys = ['_%s%d' % (name, number), '_%s' % name] args = pre + self._configuration_args(*args, **kwargs) + post
if name == 'o' and number == 1:
keys.append('')
args += self._configuration_args(self.basename, keys)
if name == 'i':
args.append('-i')
return ( return (
[encodeArgument(arg) for arg in args] [encodeArgument(o) for o in args]
+ [encodeFilename(self._ffmpeg_filename_argument(file), True)]) + [encodeFilename(self._ffmpeg_filename_argument(file), True)])
for arg_type, path_opts in (('i', input_path_opts), ('o', output_path_opts)): for i, path in enumerate(input_paths):
cmd += [arg for i, o in enumerate(path_opts) cmd += make_args(path, post=['-i'], exe='%s_i%d' % (self.basename, i + 1), use_default_arg=False)
for arg in make_args(o[0], o[1], arg_type, i + 1)] cmd += make_args(out_path, pre=opts, exe=self.basename)
self.write_debug('ffmpeg command line: %s' % shell_quote(cmd)) self.write_debug('ffmpeg command line: %s' % shell_quote(cmd))
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
@@ -272,8 +262,7 @@ class FFmpegPostProcessor(PostProcessor):
if self.get_param('verbose', False): if self.get_param('verbose', False):
self.report_error(stderr) self.report_error(stderr)
raise FFmpegPostProcessorError(stderr.split('\n')[-1]) raise FFmpegPostProcessorError(stderr.split('\n')[-1])
for out_path, _ in output_path_opts: self.try_utime(out_path, oldest_mtime, oldest_mtime)
self.try_utime(out_path, oldest_mtime, oldest_mtime)
return stderr.decode('utf-8', 'replace') return stderr.decode('utf-8', 'replace')
def run_ffmpeg(self, path, out_path, opts): def run_ffmpeg(self, path, out_path, opts):
@@ -485,7 +474,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
self.report_warning('JSON subtitles cannot be embedded') self.report_warning('JSON subtitles cannot be embedded')
elif ext != 'webm' or ext == 'webm' and sub_ext == 'vtt': elif ext != 'webm' or ext == 'webm' and sub_ext == 'vtt':
sub_langs.append(lang) sub_langs.append(lang)
sub_filenames.append(sub_info['filepath']) sub_filenames.append(subtitles_filename(filename, lang, sub_ext, ext))
else: else:
if not webm_vtt_warn and ext == 'webm' and sub_ext != 'vtt': if not webm_vtt_warn and ext == 'webm' and sub_ext != 'vtt':
webm_vtt_warn = True webm_vtt_warn = True
@@ -550,8 +539,8 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
add('title', ('track', 'title')) add('title', ('track', 'title'))
add('date', 'upload_date') add('date', 'upload_date')
add(('description', 'synopsis'), 'description') add(('description', 'comment'), 'description')
add(('purl', 'comment'), 'webpage_url') add('purl', 'webpage_url')
add('track', 'track_number') add('track', 'track_number')
add('artist', ('artist', 'creator', 'uploader', 'uploader_id')) add('artist', ('artist', 'creator', 'uploader', 'uploader_id'))
add('genre') add('genre')
@@ -711,6 +700,7 @@ class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor):
def run(self, info): def run(self, info):
subs = info.get('requested_subtitles') subs = info.get('requested_subtitles')
filename = info['filepath']
new_ext = self.format new_ext = self.format
new_format = new_ext new_format = new_ext
if new_format == 'vtt': if new_format == 'vtt':
@@ -730,9 +720,9 @@ class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor):
'You have requested to convert json subtitles into another format, ' 'You have requested to convert json subtitles into another format, '
'which is currently not possible') 'which is currently not possible')
continue continue
old_file = sub['filepath'] old_file = subtitles_filename(filename, lang, ext, info.get('ext'))
sub_filenames.append(old_file) sub_filenames.append(old_file)
new_file = replace_extension(old_file, new_ext) new_file = subtitles_filename(filename, lang, new_ext, info.get('ext'))
if ext in ('dfxp', 'ttml', 'tt'): if ext in ('dfxp', 'ttml', 'tt'):
self.report_warning( self.report_warning(
@@ -740,7 +730,7 @@ class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor):
'which results in style information loss') 'which results in style information loss')
dfxp_file = old_file dfxp_file = old_file
srt_file = replace_extension(old_file, 'srt') srt_file = subtitles_filename(filename, lang, 'srt', info.get('ext'))
with open(dfxp_file, 'rb') as f: with open(dfxp_file, 'rb') as f:
srt_data = dfxp2srt(f.read()) srt_data = dfxp2srt(f.read())
@@ -751,8 +741,7 @@ class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor):
subs[lang] = { subs[lang] = {
'ext': 'srt', 'ext': 'srt',
'data': srt_data, 'data': srt_data
'filepath': srt_file,
} }
if new_ext == 'srt': if new_ext == 'srt':
@@ -766,47 +755,6 @@ class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor):
subs[lang] = { subs[lang] = {
'ext': new_ext, 'ext': new_ext,
'data': f.read(), 'data': f.read(),
'filepath': new_file,
} }
info['__files_to_move'][new_file] = replace_extension(
info['__files_to_move'][old_file], new_ext)
return sub_filenames, info return sub_filenames, info
class FFmpegSplitChaptersPP(FFmpegPostProcessor):
def _prepare_filename(self, number, chapter, info):
info = info.copy()
info.update({
'section_number': number,
'section_title': chapter.get('title'),
'section_start': chapter.get('start_time'),
'section_end': chapter.get('end_time'),
})
return self._downloader.prepare_filename(info, 'chapter')
def _ffmpeg_args_for_chapter(self, number, chapter, info):
destination = self._prepare_filename(number, chapter, info)
if not self._downloader._ensure_dir_exists(encodeFilename(destination)):
return
chapter['filepath'] = destination
self.to_screen('Chapter %03d; Destination: %s' % (number, destination))
return (
destination,
['-ss', compat_str(chapter['start_time']),
'-t', compat_str(chapter['end_time'] - chapter['start_time'])])
def run(self, info):
chapters = info.get('chapters') or []
if not chapters:
self.report_warning('Chapter information is unavailable')
return [], info
self.to_screen('Splitting video by chapters; %d chapters found' % len(chapters))
for idx, chapter in enumerate(chapters):
destination, opts = self._ffmpeg_args_for_chapter(idx + 1, chapter, info)
self.real_run_ffmpeg([(info['filepath'], opts)], [(destination, ['-c', 'copy'])])
return [], info
+19 -21
View File
@@ -4,10 +4,11 @@ import re
from .common import PostProcessor from .common import PostProcessor
from ..compat import compat_str from ..compat import compat_str
from ..utils import str_or_none
class MetadataFromFieldPP(PostProcessor): class MetadataFromFieldPP(PostProcessor):
regex = r'(?P<in>.+):(?P<out>.+)$' regex = r'(?P<field>\w+):(?P<format>.+)$'
def __init__(self, downloader, formats): def __init__(self, downloader, formats):
PostProcessor.__init__(self, downloader) PostProcessor.__init__(self, downloader)
@@ -18,20 +19,11 @@ class MetadataFromFieldPP(PostProcessor):
match = re.match(self.regex, f) match = re.match(self.regex, f)
assert match is not None assert match is not None
self._data.append({ self._data.append({
'in': match.group('in'), 'field': match.group('field'),
'out': match.group('out'), 'format': match.group('format'),
'tmpl': self.field_to_template(match.group('in')), 'regex': self.format_to_regex(match.group('format'))})
'regex': self.format_to_regex(match.group('out')),
})
@staticmethod def format_to_regex(self, fmt):
def field_to_template(tmpl):
if re.match(r'\w+$', tmpl):
return '%%(%s)s' % tmpl
return tmpl
@staticmethod
def format_to_regex(fmt):
r""" r"""
Converts a string like Converts a string like
'%(title)s - %(artist)s' '%(title)s - %(artist)s'
@@ -45,7 +37,7 @@ class MetadataFromFieldPP(PostProcessor):
# replace %(..)s with regex group and escape other string parts # replace %(..)s with regex group and escape other string parts
for match in re.finditer(r'%\((\w+)\)s', fmt): for match in re.finditer(r'%\((\w+)\)s', fmt):
regex += re.escape(fmt[lastpos:match.start()]) regex += re.escape(fmt[lastpos:match.start()])
regex += r'(?P<%s>[^\r\n]+)' % match.group(1) regex += r'(?P<' + match.group(1) + r'>[^\r\n]+)'
lastpos = match.end() lastpos = match.end()
if lastpos < len(fmt): if lastpos < len(fmt):
regex += re.escape(fmt[lastpos:]) regex += re.escape(fmt[lastpos:])
@@ -53,16 +45,22 @@ class MetadataFromFieldPP(PostProcessor):
def run(self, info): def run(self, info):
for dictn in self._data: for dictn in self._data:
tmpl, info_copy = self._downloader.prepare_outtmpl(dictn['tmpl'], info) field, regex = dictn['field'], dictn['regex']
data_to_parse = tmpl % info_copy if field not in info:
self.write_debug('Searching for r"%s" in %s' % (dictn['regex'], tmpl)) self.report_warning('Video doesnot have a %s' % field)
match = re.search(dictn['regex'], data_to_parse) continue
data_to_parse = str_or_none(info[field])
if data_to_parse is None:
self.report_warning('Field %s cannot be parsed' % field)
continue
self.write_debug('Searching for r"%s" in %s' % (regex, field))
match = re.search(regex, data_to_parse)
if match is None: if match is None:
self.report_warning('Could not interpret video %s as "%s"' % (dictn['in'], dictn['out'])) self.report_warning('Could not interpret video %s as "%s"' % (field, dictn['format']))
continue continue
for attribute, value in match.groupdict().items(): for attribute, value in match.groupdict().items():
info[attribute] = value info[attribute] = value
self.to_screen('parsed %s from "%s": %s' % (attribute, dictn['in'], value if value is not None else 'NA')) self.to_screen('parsed %s from %s: %s' % (attribute, field, value if value is not None else 'NA'))
return [], info return [], info
@@ -13,6 +13,10 @@ from ..utils import (
class MoveFilesAfterDownloadPP(PostProcessor): class MoveFilesAfterDownloadPP(PostProcessor):
def __init__(self, downloader, files_to_move):
PostProcessor.__init__(self, downloader)
self.files_to_move = files_to_move
@classmethod @classmethod
def pp_key(cls): def pp_key(cls):
return 'MoveFiles' return 'MoveFiles'
@@ -21,10 +25,11 @@ class MoveFilesAfterDownloadPP(PostProcessor):
dl_path, dl_name = os.path.split(encodeFilename(info['filepath'])) dl_path, dl_name = os.path.split(encodeFilename(info['filepath']))
finaldir = info.get('__finaldir', dl_path) finaldir = info.get('__finaldir', dl_path)
finalpath = os.path.join(finaldir, dl_name) finalpath = os.path.join(finaldir, dl_name)
info['__files_to_move'][info['filepath']] = decodeFilename(finalpath) self.files_to_move.update(info['__files_to_move'])
self.files_to_move[info['filepath']] = decodeFilename(finalpath)
make_newfilename = lambda old: decodeFilename(os.path.join(finaldir, os.path.basename(encodeFilename(old)))) make_newfilename = lambda old: decodeFilename(os.path.join(finaldir, os.path.basename(encodeFilename(old))))
for oldfile, newfile in info['__files_to_move'].items(): for oldfile, newfile in self.files_to_move.items():
if not newfile: if not newfile:
newfile = make_newfilename(oldfile) newfile = make_newfilename(oldfile)
if os.path.abspath(encodeFilename(oldfile)) == os.path.abspath(encodeFilename(newfile)): if os.path.abspath(encodeFilename(oldfile)) == os.path.abspath(encodeFilename(newfile)):
+2 -4
View File
@@ -6,7 +6,6 @@ from .common import PostProcessor
from ..compat import compat_shlex_split from ..compat import compat_shlex_split
from ..utils import ( from ..utils import (
check_executable, check_executable,
cli_option,
encodeArgument, encodeArgument,
encodeFilename, encodeFilename,
shell_quote, shell_quote,
@@ -32,7 +31,7 @@ class SponSkrubPP(PostProcessor):
if path: if path:
raise PostProcessingError('sponskrub not found in "%s"' % path) raise PostProcessingError('sponskrub not found in "%s"' % path)
else: else:
raise PostProcessingError('sponskrub not found. Please install or provide the path using --sponskrub-path') raise PostProcessingError('sponskrub not found. Please install or provide the path using --sponskrub-path.')
def get_exe(self, path=''): def get_exe(self, path=''):
if not path or not check_executable(path, ['-h']): if not path or not check_executable(path, ['-h']):
@@ -71,9 +70,8 @@ class SponSkrubPP(PostProcessor):
cmd = [self.path] cmd = [self.path]
if not self.cutout: if not self.cutout:
cmd += ['-chapter'] cmd += ['-chapter']
cmd += cli_option(self._downloader.params, '-proxy', 'proxy')
cmd += compat_shlex_split(self.args) # For backward compatibility cmd += compat_shlex_split(self.args) # For backward compatibility
cmd += self._configuration_args(self._exe_name, use_compat=False) cmd += self._configuration_args(exe=self._exe_name, use_default_arg='no_compat')
cmd += ['--', information['id'], filename, temp_filename] cmd += ['--', information['id'], filename, temp_filename]
cmd = [encodeArgument(i) for i in cmd] cmd = [encodeArgument(i) for i in cmd]
+24 -13
View File
@@ -49,16 +49,12 @@ def update_self(to_screen, verbose, opener):
h.update(mv[:n]) h.update(mv[:n])
return h.hexdigest() return h.hexdigest()
to_screen('Current Build Hash %s' % calc_sha256sum(sys.executable))
if not isinstance(globals().get('__loader__'), zipimporter) and not hasattr(sys, 'frozen'): if not isinstance(globals().get('__loader__'), zipimporter) and not hasattr(sys, 'frozen'):
to_screen('It looks like you installed yt-dlp with a package manager, pip, setup.py or a tarball. Please use that to update.') to_screen('It looks like you installed yt-dlp with a package manager, pip, setup.py or a tarball. Please use that to update.')
return return
# sys.executable is set to the full pathname of the exe-file for py2exe
# though symlinks are not followed so that we need to do this manually
# with help of realpath
filename = compat_realpath(sys.executable if hasattr(sys, 'frozen') else sys.argv[0])
to_screen('Current Build Hash %s' % calc_sha256sum(filename))
# Download and check versions info # Download and check versions info
try: try:
version_info = opener.open(JSON_URL).read().decode('utf-8') version_info = opener.open(JSON_URL).read().decode('utf-8')
@@ -107,6 +103,11 @@ def update_self(to_screen, verbose, opener):
(i[1] for i in hashes if i[0] == 'yt-dlp%s' % label), (i[1] for i in hashes if i[0] == 'yt-dlp%s' % label),
None) None)
# sys.executable is set to the full pathname of the exe-file for py2exe
# though symlinks are not followed so that we need to do this manually
# with help of realpath
filename = compat_realpath(sys.executable if hasattr(sys, 'frozen') else sys.argv[0])
if not os.access(filename, os.W_OK): if not os.access(filename, os.W_OK):
to_screen('ERROR: no write permissions on %s' % filename) to_screen('ERROR: no write permissions on %s' % filename)
return return
@@ -197,18 +198,28 @@ def update_self(to_screen, verbose, opener):
to_screen('Visit https://github.com/yt-dlp/yt-dlp/releases/latest') to_screen('Visit https://github.com/yt-dlp/yt-dlp/releases/latest')
return return
expected_sum = get_sha256sum('zip', py_ver)
if expected_sum and hashlib.sha256(newcontent).hexdigest() != expected_sum:
to_screen('ERROR: unable to verify the new zip')
to_screen('Visit https://github.com/yt-dlp/yt-dlp/releases/latest')
return
try: try:
with open(filename, 'wb') as outf: with open(filename + '.new', 'wb') as outf:
outf.write(newcontent) outf.write(newcontent)
except (IOError, OSError): except (IOError, OSError):
if verbose: if verbose:
to_screen(encode_compat_str(traceback.format_exc())) to_screen(encode_compat_str(traceback.format_exc()))
to_screen('ERROR: unable to write the new version')
return
expected_sum = get_sha256sum('zip', py_ver)
if expected_sum and calc_sha256sum(filename + '.new') != expected_sum:
to_screen('ERROR: unable to verify the new zip')
to_screen('Visit https://github.com/yt-dlp/yt-dlp/releases/latest')
try:
os.remove(filename + '.new')
except OSError:
to_screen('ERROR: unable to remove corrupt zip')
return
try:
os.rename(filename + '.new', filename)
except OSError:
to_screen('ERROR: unable to overwrite current version') to_screen('ERROR: unable to overwrite current version')
return return
+23 -39
View File
@@ -1836,7 +1836,7 @@ def write_json_file(obj, fn):
try: try:
with tf: with tf:
json.dump(obj, tf, default=repr) json.dump(obj, tf)
if sys.platform == 'win32': if sys.platform == 'win32':
# Need to remove existing file on Windows, else os.rename raises # Need to remove existing file on Windows, else os.rename raises
# WindowsError or FileExistsError. # WindowsError or FileExistsError.
@@ -2423,15 +2423,6 @@ class DownloadError(YoutubeDLError):
self.exc_info = exc_info self.exc_info = exc_info
class EntryNotInPlaylist(YoutubeDLError):
"""Entry not in playlist exception.
This exception will be thrown by YoutubeDL when a requested entry
is not found in the playlist info_dict
"""
pass
class SameFileError(YoutubeDLError): class SameFileError(YoutubeDLError):
"""Same File exception. """Same File exception.
@@ -4115,7 +4106,6 @@ def parse_age_limit(s):
m = re.match(r'^(?P<age>\d{1,2})\+?$', s) m = re.match(r'^(?P<age>\d{1,2})\+?$', s)
if m: if m:
return int(m.group('age')) return int(m.group('age'))
s = s.upper()
if s in US_RATINGS: if s in US_RATINGS:
return US_RATINGS[s] return US_RATINGS[s]
m = re.match(r'^TV[_-]?(%s)$' % '|'.join(k[3:] for k in TV_PARENTAL_GUIDELINES), s) m = re.match(r'^TV[_-]?(%s)$' % '|'.join(k[3:] for k in TV_PARENTAL_GUIDELINES), s)
@@ -4192,10 +4182,8 @@ def qualities(quality_ids):
DEFAULT_OUTTMPL = { DEFAULT_OUTTMPL = {
'default': '%(title)s [%(id)s].%(ext)s', 'default': '%(title)s [%(id)s].%(ext)s',
'chapter': '%(title)s - %(section_number)03d %(section_title)s [%(id)s].%(ext)s',
} }
OUTTMPL_TYPES = { OUTTMPL_TYPES = {
'chapter': None,
'subtitle': None, 'subtitle': None,
'thumbnail': None, 'thumbnail': None,
'description': 'description', 'description': 'description',
@@ -4205,20 +4193,6 @@ OUTTMPL_TYPES = {
'pl_infojson': 'info.json', 'pl_infojson': 'info.json',
} }
# As of [1] format syntax is:
# %[mapping_key][conversion_flags][minimum_width][.precision][length_modifier]type
# 1. https://docs.python.org/2/library/stdtypes.html#string-formatting
FORMAT_RE = r'''(?x)
(?<!%)
%
\({0}\) # mapping key
(?:[#0\-+ ]+)? # conversion flags (optional)
(?:\d+)? # minimum field width (optional)
(?:\.\d+)? # precision (optional)
[hlL]? # length modifier (optional)
(?P<type>[diouxXeEfFgGcrs%]) # conversion type
'''
def limit_length(s, length): def limit_length(s, length):
""" Add ellipses to overly long strings """ """ Add ellipses to overly long strings """
@@ -4718,26 +4692,36 @@ def cli_valueless_option(params, command_option, param, expected_value=True):
return [command_option] if param == expected_value else [] return [command_option] if param == expected_value else []
def cli_configuration_args(argdict, keys, default=[], use_compat=True): def cli_configuration_args(argdict, key, default=[], exe=None, use_default_arg=True):
# use_default_arg can be True, False, or 'no_compat'
if isinstance(argdict, (list, tuple)): # for backward compatibility if isinstance(argdict, (list, tuple)): # for backward compatibility
if use_compat: if use_default_arg is True:
return argdict return argdict
else: else:
argdict = None argdict = None
if argdict is None: if argdict is None:
return default return default
assert isinstance(argdict, dict) assert isinstance(argdict, dict)
assert isinstance(keys, (list, tuple)) key = key.lower()
for key_list in keys: args = exe_args = None
if isinstance(key_list, compat_str): if exe is not None:
key_list = (key_list,) assert isinstance(exe, compat_str)
arg_list = list(filter( exe = exe.lower()
lambda x: x is not None, args = argdict.get('%s+%s' % (key, exe))
[argdict.get(key.lower()) for key in key_list])) if args is None:
if arg_list: exe_args = argdict.get(exe)
return [arg for args in arg_list for arg in args]
return default if args is None:
args = argdict.get(key) if key != exe else None
if args is None and exe_args is None:
args = argdict.get('default', default) if use_default_arg else default
args, exe_args = args or [], exe_args or []
assert isinstance(args, (list, tuple))
assert isinstance(exe_args, (list, tuple))
return args + exe_args
class ISO639Utils(object): class ISO639Utils(object):
+1 -1
View File
@@ -1,3 +1,3 @@
from __future__ import unicode_literals from __future__ import unicode_literals
__version__ = '2021.03.24' __version__ = '2021.03.01'