Compare commits

...

35 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
pukkandan 3721515bde Update to ytdl-2021.03.03 2021-03-03 16:04:01 +05:30
Matthew a5c5623470 [YouTube] Use new browse API for continuation page extraction. (#131)
Known issues (these issues existed in previous API as well)
* Mix playlists only give 1 page (25 vids)
* Trending only gives 1 video
* History gives 5 pages (200 vids)

Co-authored-by: colethedj, pukkandan
2021-03-03 16:02:40 +05:30
pukkandan c705177da2 [youtube] Throw error when --extractor-retries are exhausted (Closes #130) 2021-03-03 03:05:31 +05:30
pukkandan d6e51845b7 Reduce default of --extractor-retries to 3
so that even those not using sleep won't get 429'd on youtube
2021-03-03 03:04:08 +05:30
hseg da7f321e93 Fix packaging bugs (#129)
* Autogenerate `AUTHORS`
* Fix `setup.py` using wrong completion files
* Complete `ChangeLog` -> `Changelog.md` rename
* Make `make tar` respect DESTDIR
* Remove `bin/` `yt-dlp` and `docs/` from tar and sdist
* Make `pypi-files` build all files needed for `python setup.py`
* Add `completions` alias
* Add `devscripts/` and `supportedsites.md` to pip sdist
* Remove `man` target
* Remove `README.txt` from sdist
* Make `clean` more granular
* Move aliases to top

Authored by: hseg <gesh@gesh.uni.cx>
2021-03-03 02:17:44 +05:30
Ashutosh Chaudhary 097b056c5a [mxplayer] Add new extractor
https://github.com/ytdl-org/youtube-dl/pull/27325
Authored by: codeasashu
2021-03-02 17:49:48 +05:30
Han Dai f3b737ed19 [nick] fix extraction
https://github.com/ytdl-org/youtube-dl/pull/27900
Authored by: DennyDai
2021-03-02 17:02:45 +05:30
pukkandan ee1e05581e [mtv] Fix extractor by reverting changes made in youtube-dlc
youtube-dl has since fixed the extractor and the changes from the two sources are incompatible
2021-03-02 16:55:17 +05:30
pukkandan ec5e77c558 Update to ytdl-2021.03.02 2021-03-02 13:56:07 +05:30
shirt-dev b3b30a4bca Fix HLS playlist downloading (#127)
Co-authored-by: shirtjs <2660574+shirtjs@users.noreply.github.com>
2021-03-01 12:05:45 -05:00
pukkandan 5372545ddb [version] update :ci skip 2021-03-01 05:46:00 +05:30
pukkandan 5ef7d9bdd8 Release 2021.03.01 2021-03-01 05:39:50 +05:30
pukkandan 62bff2c170 Add option --extractor-retries to retry on known extractor errors
* Currently only used by youtube

Fixes https://github.com/ytdl-org/youtube-dl/issues/28194
Possibly also fixes: https://github.com/ytdl-org/youtube-dl/issues/28289 (can not confirm since the issue isn't reliably reproducible)
2021-03-01 05:18:37 +05:30
pukkandan f0884c8b3f Cleanup some code (see desc)
* `--get-comments` doesn't imply `--write-info-json` if `-J`, `-j` or `--print-json` are used
* Don't pass `config_location` to `YoutubeDL` (it is unused)
* [bilibiliaudio] Recognize the file as audio-only
* Update gitignore
* Fix typos
2021-02-28 20:56:32 +05:30
pukkandan 277d6ff5f2 Extract comments only when needed #95 (Closes #94) 2021-02-28 20:26:08 +05:30
pukkandan 1cf376f55a Add option --sleep-requests to sleep b/w requests (Closes #106)
* Also fix documentation of `sleep_interval_subtitles`

Related issues:
https://github.com/blackjack4494/yt-dlc/issues/158
https://github.com/blackjack4494/youtube-dlc/issues/195
https://github.com/ytdl-org/youtube-dl/pull/28270
https://github.com/ytdl-org/youtube-dl/pull/28144
https://github.com/ytdl-org/youtube-dl/issues/27767
https://github.com/ytdl-org/youtube-dl/issues/23638
https://github.com/ytdl-org/youtube-dl/issues/26287
https://github.com/ytdl-org/youtube-dl/issues/26319
2021-02-27 18:14:42 +05:30
pukkandan 7f7de7f94d Allow specifying path in --external-downloader 2021-02-27 16:52:27 +05:30
pukkandan 86878b6cd9 [hrfensehen] Fix wrong import 2021-02-27 15:35:41 +05:30
pukkandan b3d1242534 [youtube] Fix inconsistent webpage_url (closes #119) 2021-02-27 14:45:56 +05:30
pukkandan 9bd2020476 [hls] Enable --hls-use-mpegts by default when downloading live-streams
* Also added option `--no-hls-use-mpegts` to disable this

Related: #96
2021-02-26 21:52:16 +05:30
pukkandan ed9b7e3dd3 Fix bug with m3u8 format extraction 2021-02-26 18:32:28 +05:30
shirt-dev c552ae8838 Fix get_executable_path (#117)
Authored-by: shirtjs <2660574+shirtjs@users.noreply.github.com>
2021-02-26 04:28:02 +05:30
Robin Dunn 31a5e037a7 [viki] Fix viki play pass authentication (#111)
Authored by: RobinD42
2021-02-26 03:33:00 +05:30
pukkandan 3638226215 [ci] Disable download tests unless specifically invoked
Tests can be enabled/disabled using the following in the commit message
* Run Download: `ci-run-dl`
* Skip Core: `ci-skip`
* Skip Quick & Core: `ci-skip-all`
(replace "-" by a space " ")
2021-02-26 03:28:18 +05:30
pukkandan 14fdfea973 [youtube] Retry on incomplete ytInitialData
Related: #116
2021-02-26 03:23:08 +05:30
shirt-dev b45d4e4a8e Fix completion paths, zsh pip completion install (#114) 2021-02-25 11:00:29 -05:00
pukkandan 3e39273418 Merge branch 'master' into fix-paths 2021-02-25 21:17:46 +05:30
shirt-dev b965087396 Readthedocs improvements (#115)
Authored-by: shirtjs <2660574+shirtjs@users.noreply.github.com>

:ci skip dl
2021-02-25 21:16:08 +05:30
hseg 359d6d8650 Fix completion paths, zsh pip completion install
Closes: #108, #110
2021-02-25 16:49:57 +02:00
pukkandan 0e0040519b [embedthumbnail] Fix bug with deleting original thumbnail (Closes #113)
:ci skip dl
2021-02-25 18:35:04 +05:30
pukkandan 127d075955 [documentation] Fix typos (Closes #112)
:ci skip all
2021-02-25 16:08:25 +05:30
pukkandan bce8cbb089 [tennistv] Fix format sorting 2021-02-25 16:07:38 +05:30
pukkandan aae273ded8 [version] update :ci skip dl 2021-02-25 02:44:10 +05:30
64 changed files with 2427 additions and 2086 deletions
+1
View File
@@ -0,0 +1 @@
Makefile* text whitespace=-tab-in-indent
+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.02.19. 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.02.19** - [ ] 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.02.19 [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.02.19. 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.02.19** - [ ] 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.02.19. 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.02.19** - [ ] 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.02.19. 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.02.19** - [ ] 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.02.19 [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.02.19. 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.02.19** - [ ] 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 -1
View File
@@ -3,7 +3,7 @@ on: [push, pull_request]
jobs: jobs:
tests: tests:
name: Core Tests name: Core Tests
if: "!contains(github.event.head_commit.message, 'ci skip all')" if: "!contains(github.event.head_commit.message, 'ci skip')"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: true fail-fast: true
+1 -1
View File
@@ -3,7 +3,7 @@ on: [push, pull_request]
jobs: jobs:
tests: tests:
name: Download Tests name: Download Tests
if: "!contains(github.event.head_commit.message, 'ci skip dl') && !contains(github.event.head_commit.message, 'ci skip all')" if: "contains(github.event.head_commit.message, 'ci run dl')"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: true fail-fast: true
+8 -2
View File
@@ -8,6 +8,7 @@ dist/
zip/ zip/
tmp/ tmp/
venv/ venv/
completions/
# Misc # Misc
*~ *~
@@ -24,7 +25,9 @@ updates_key.pem
*.class *.class
# Generated # Generated
AUTHORS
README.txt README.txt
.mailmap
*.1 *.1
*.bash-completion *.bash-completion
*.fish *.fish
@@ -34,8 +37,9 @@ README.txt
*.spec *.spec
# Binary # Binary
youtube-dl /youtube-dl
youtube-dlc /youtube-dlc
/yt-dlp
yt-dlp.zip yt-dlp.zip
*.exe *.exe
@@ -50,12 +54,14 @@ yt-dlp.zip
*.m4v *.m4v
*.mp3 *.mp3
*.3gp *.3gp
*.webm
*.wav *.wav
*.ape *.ape
*.mkv *.mkv
*.swf *.swf
*.part *.part
*.ytdl *.ytdl
*.dump
*.frag *.frag
*.frag.urls *.frag.urls
*.aria2 *.aria2
-1
View File
@@ -20,4 +20,3 @@ python:
version: 3 version: 3
install: install:
- requirements: docs/requirements.txt - requirements: docs/requirements.txt
- requirements: requirements.txt
-38
View File
@@ -1,38 +0,0 @@
language: python
python:
- "2.6"
- "2.7"
- "3.2"
- "3.3"
- "3.4"
- "3.5"
- "3.6"
- "pypy"
- "pypy3"
dist: trusty
env:
- YTDL_TEST_SET=core
jobs:
include:
- python: 3.7
dist: xenial
env: YTDL_TEST_SET=core
- python: 3.8
dist: xenial
env: YTDL_TEST_SET=core
- python: 3.8-dev
dist: xenial
env: YTDL_TEST_SET=core
- env: JYTHON=true; YTDL_TEST_SET=core
- name: flake8
python: 3.8
dist: xenial
install: pip install flake8
script: flake8 .
fast_finish: true
allow_failures:
- env: YTDL_TEST_SET=download
- env: JYTHON=true; YTDL_TEST_SET=core
before_install:
- if [ "$JYTHON" == "true" ]; then ./devscripts/install_jython.sh; export PATH="$HOME/jython/bin:$PATH"; fi
script: ./devscripts/run_tests.sh
+5
View File
@@ -23,3 +23,8 @@ tsukumi
bbepis bbepis
Pccode66 Pccode66
Ashish Ashish
RobinD42
hseg
colethedj
DennyDai
codeasashu
+38 -3
View File
@@ -17,9 +17,44 @@
--> -->
### 2021.03.03
* [youtube] Use new browse API for continuation page extraction by @colethedj and @pukkandan
* 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)
* [mtv] Fix extractor
* [nick] Fix extractor by @DennyDai
* [mxplayer] Add new extractor by@codeasashu
* [youtube] Throw error when `--extractor-retries` are exhausted
* Reduce default of `--extractor-retries` to 3
* Fix packaging bugs by @hseg
### 2021.03.01
* Allow specifying path in `--external-downloader`
* Add option `--sleep-requests` to sleep b/w requests
* Add option `--extractor-retries` to retry on known extractor errors
* Extract comments only when needed
* `--get-comments` doesn't imply `--write-info-json` if `-J`, `-j` or `--print-json` are used
* Fix `get_executable_path` by [shirt](https://github.com/shirt-dev)
* [youtube] Retry on more known errors than just HTTP-5xx
* [youtube] Fix inconsistent `webpage_url`
* [tennistv] Fix format sorting
* [bilibiliaudio] Recognize the file as audio-only
* [hrfensehen] Fix wrong import
* [viki] Fix viki play pass authentication by [RobinD42](https://github.com/RobinD42)
* [readthedocs] Improvements by [shirt](https://github.com/shirt-dev)
* [hls] Fix bug with m3u8 format extraction
* [hls] Enable `--hls-use-mpegts` by default when downloading live-streams
* [embedthumbnail] Fix bug with deleting original thumbnail
* [build] Fix completion paths, zsh pip completion install by [hseg](https://github.com/hseg)
* [ci] Disable download tests unless specifically invoked
* Cleanup some code and fix typos
### 2021.02.24 ### 2021.02.24
* 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
* **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`)
@@ -28,12 +63,12 @@
* [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)
* [hls] Added options `--hls-split-discontinuity` to better support media discontinuity by [shirt](https://github.com/shirt-dev) * [hls] Added options `--hls-split-discontinuity` to better support media discontinuity by [shirt](https://github.com/shirt-dev)
* [ffmpeg] Allow passing custom arguments before -i using `--ppa "ffmpeg_i1:ARGS"` synatax * [ffmpeg] Allow passing custom arguments before -i using `--ppa "ffmpeg_i1:ARGS"` syntax
* Fix `--windows-filenames` removing `/` from UNIX paths * Fix `--windows-filenames` removing `/` from UNIX paths
* [hls] Show warning if pycryptodome is not found * [hls] Show warning if pycryptodome is not found
* [documentation] Improvements * [documentation] Improvements
* Fix documentation of `Extractor Options` * Fix documentation of `Extractor Options`
* Document `all` in format selection (Closes #101) * Document `all` in format selection
* Document `playable_in_embed` in output templates * Document `playable_in_embed` in output templates
@@ -276,7 +311,7 @@
* 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-options-sponsorblock) 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`
* **Negative Options:** Makes it possible to negate most boolean options by adding a `no-` to the switch. Usefull when you want to reverse an option that is defined in a config file * **Negative Options:** Makes it possible to negate most boolean options by adding a `no-` to the switch. Usefull when you want to reverse an option that is defined in a config file
+6 -6
View File
@@ -1,9 +1,9 @@
include README.md
include LICENSE
include AUTHORS include AUTHORS
include ChangeLog include Changelog.md
include yt-dlp.bash-completion include LICENSE
include yt-dlp.fish include README.md
include completions/*/*
include supportedsites.md
include yt-dlp.1 include yt-dlp.1
recursive-include docs Makefile conf.py *.rst recursive-include devscripts *
recursive-include test * recursive-include test *
+47 -44
View File
@@ -1,12 +1,28 @@
all: yt-dlp doc man all: yt-dlp doc
clean: clean-test clean-dist clean-cache
completions: completion-bash completion-fish completion-zsh
doc: README.md CONTRIBUTING.md issuetemplates supportedsites doc: README.md CONTRIBUTING.md issuetemplates supportedsites
man: README.txt yt-dlp.1 yt-dlp.bash-completion yt-dlp.zsh yt-dlp.fish ot: offlinetest
tar: yt-dlp.tar.gz
# Keep this list in sync with MANIFEST.in
# intended use: when building a source distribution,
# make pypi-files && python setup.py sdist
pypi-files: AUTHORS Changelog.md LICENSE README.md README.txt supportedsites completions yt-dlp.1 devscripts/* test/*
clean: .PHONY: all clean install test tar pypi-files completions ot offlinetest codetest supportedsites
rm -rf yt-dlp.1.temp.md yt-dlp.1 yt-dlp.bash-completion README.txt MANIFEST build/ dist/ .coverage cover/ yt-dlp.tar.gz yt-dlp.zsh yt-dlp.fish yt_dlp/extractor/lazy_extractors.py *.dump *.part* *.ytdl *.info.json *.mp4 *.m4a *.flv *.mp3 *.avi *.mkv *.webm *.3gp *.wav *.ape *.swf *.jpg *.png *.spec *.frag *.frag.urls *.frag.aria2 CONTRIBUTING.md.tmp yt-dlp yt-dlp.exe
find . -name "*.pyc" -delete clean-test:
find . -name "*.class" -delete rm -rf *.dump *.part* *.ytdl *.info.json *.mp4 *.m4a *.flv *.mp3 *.avi *.mkv *.webm *.3gp *.wav *.ape *.swf *.jpg *.png *.frag *.frag.urls *.frag.aria2
clean-dist:
rm -rf yt-dlp.1.temp.md yt-dlp.1 README.txt MANIFEST build/ dist/ .coverage cover/ yt-dlp.tar.gz completions/ yt_dlp/extractor/lazy_extractors.py *.spec CONTRIBUTING.md.tmp yt-dlp yt-dlp.exe yt_dlp.egg-info/ AUTHORS .mailmap
clean-cache:
find . -name "*.pyc" -o -name "*.class" -delete
completion-bash: completions/bash/yt-dlp
completion-fish: completions/fish/yt-dlp.fish
completion-zsh: completions/zsh/_yt-dlp
lazy-extractors: yt_dlp/extractor/lazy_extractors.py
PREFIX ?= /usr/local PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/bin BINDIR ?= $(PREFIX)/bin
@@ -21,17 +37,12 @@ SYSCONFDIR = $(shell if [ $(PREFIX) = /usr -o $(PREFIX) = /usr/local ]; then ech
# set markdown input format to "markdown-smart" for pandoc version 2 and to "markdown" for pandoc prior to version 2 # set markdown input format to "markdown-smart" for pandoc version 2 and to "markdown" for pandoc prior to version 2
MARKDOWN = $(shell if [ `pandoc -v | head -n1 | cut -d" " -f2 | head -c1` = "2" ]; then echo markdown-smart; else echo markdown; fi) MARKDOWN = $(shell if [ `pandoc -v | head -n1 | cut -d" " -f2 | head -c1` = "2" ]; then echo markdown-smart; else echo markdown; fi)
install: yt-dlp yt-dlp.1 yt-dlp.bash-completion yt-dlp.zsh yt-dlp.fish install: yt-dlp yt-dlp.1 completions
install -d $(DESTDIR)$(BINDIR) install -Dm755 yt-dlp $(DESTDIR)$(BINDIR)
install -m 755 yt-dlp $(DESTDIR)$(BINDIR) install -Dm644 yt-dlp.1 $(DESTDIR)$(MANDIR)/man1
install -d $(DESTDIR)$(MANDIR)/man1 install -Dm644 completions/bash/yt-dlp $(DESTDIR)$(SHAREDIR)/bash-completion/completions/yt-dlp
install -m 644 yt-dlp.1 $(DESTDIR)$(MANDIR)/man1 install -Dm644 completions/zsh/_yt-dlp $(DESTDIR)$(SHAREDIR)/zsh/site-functions/_yt-dlp
install -d $(DESTDIR)$(SYSCONFDIR)/bash_completion.d install -Dm644 completions/fish/yt-dlp.fish $(DESTDIR)$(SHAREDIR)/fish/vendor_completions.d/yt-dlp.fish
install -m 644 yt-dlp.bash-completion $(DESTDIR)$(SYSCONFDIR)/bash_completion.d/yt-dlp
install -d $(DESTDIR)$(SHAREDIR)/zsh/site-functions
install -m 644 yt-dlp.zsh $(DESTDIR)$(SHAREDIR)/zsh/site-functions/_yt-dlp
install -d $(DESTDIR)$(SYSCONFDIR)/fish/completions
install -m 644 yt-dlp.fish $(DESTDIR)$(SYSCONFDIR)/fish/completions/yt-dlp.fish
codetest: codetest:
flake8 . flake8 .
@@ -41,8 +52,6 @@ test:
nosetests --verbose test nosetests --verbose test
$(MAKE) codetest $(MAKE) codetest
ot: offlinetest
# Keep this list in sync with devscripts/run_tests.sh # Keep this list in sync with devscripts/run_tests.sh
offlinetest: codetest offlinetest: codetest
$(PYTHON) -m nose --verbose test \ $(PYTHON) -m nose --verbose test \
@@ -57,12 +66,6 @@ offlinetest: codetest
--exclude test_youtube_signature.py \ --exclude test_youtube_signature.py \
--exclude test_post_hooks.py --exclude test_post_hooks.py
tar: yt-dlp.tar.gz
.PHONY: all clean install test tar bash-completion pypi-files zsh-completion fish-completion ot offlinetest codetest supportedsites
pypi-files: yt-dlp.bash-completion README.txt yt-dlp.1 yt-dlp.fish
yt-dlp: yt_dlp/*.py yt_dlp/*/*.py yt-dlp: yt_dlp/*.py yt_dlp/*/*.py
mkdir -p zip mkdir -p zip
for d in yt_dlp yt_dlp/downloader yt_dlp/extractor yt_dlp/postprocessor ; do \ for d in yt_dlp yt_dlp/downloader yt_dlp/extractor yt_dlp/postprocessor ; do \
@@ -92,7 +95,7 @@ issuetemplates: devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/1_
$(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/5_feature_request.md .github/ISSUE_TEMPLATE/5_feature_request.md $(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/5_feature_request.md .github/ISSUE_TEMPLATE/5_feature_request.md
supportedsites: supportedsites:
$(PYTHON) devscripts/make_supportedsites.py docs/supportedsites.md $(PYTHON) devscripts/make_supportedsites.py supportedsites.md
README.txt: README.md README.txt: README.md
pandoc -f $(MARKDOWN) -t plain README.md -o README.txt pandoc -f $(MARKDOWN) -t plain README.md -o README.txt
@@ -102,29 +105,24 @@ yt-dlp.1: README.md
pandoc -s -f $(MARKDOWN) -t man yt-dlp.1.temp.md -o yt-dlp.1 pandoc -s -f $(MARKDOWN) -t man yt-dlp.1.temp.md -o yt-dlp.1
rm -f yt-dlp.1.temp.md rm -f yt-dlp.1.temp.md
yt-dlp.bash-completion: yt_dlp/*.py yt_dlp/*/*.py devscripts/bash-completion.in completions/bash/yt-dlp: yt_dlp/*.py yt_dlp/*/*.py devscripts/bash-completion.in
mkdir -p completions/bash
$(PYTHON) devscripts/bash-completion.py $(PYTHON) devscripts/bash-completion.py
bash-completion: yt-dlp.bash-completion completions/zsh/_yt-dlp: yt_dlp/*.py yt_dlp/*/*.py devscripts/zsh-completion.in
mkdir -p completions/zsh
yt-dlp.zsh: yt_dlp/*.py yt_dlp/*/*.py devscripts/zsh-completion.in
$(PYTHON) devscripts/zsh-completion.py $(PYTHON) devscripts/zsh-completion.py
zsh-completion: yt-dlp.zsh completions/fish/yt-dlp.fish: yt_dlp/*.py yt_dlp/*/*.py devscripts/fish-completion.in
mkdir -p completions/fish
yt-dlp.fish: yt_dlp/*.py yt_dlp/*/*.py devscripts/fish-completion.in
$(PYTHON) devscripts/fish-completion.py $(PYTHON) devscripts/fish-completion.py
fish-completion: yt-dlp.fish
lazy-extractors: yt_dlp/extractor/lazy_extractors.py
_EXTRACTOR_FILES = $(shell find yt_dlp/extractor -iname '*.py' -and -not -iname 'lazy_extractors.py') _EXTRACTOR_FILES = $(shell find yt_dlp/extractor -iname '*.py' -and -not -iname 'lazy_extractors.py')
yt_dlp/extractor/lazy_extractors.py: devscripts/make_lazy_extractors.py devscripts/lazy_load_template.py $(_EXTRACTOR_FILES) yt_dlp/extractor/lazy_extractors.py: devscripts/make_lazy_extractors.py devscripts/lazy_load_template.py $(_EXTRACTOR_FILES)
$(PYTHON) devscripts/make_lazy_extractors.py $@ $(PYTHON) devscripts/make_lazy_extractors.py $@
yt-dlp.tar.gz: yt-dlp README.md README.txt yt-dlp.1 yt-dlp.bash-completion yt-dlp.zsh yt-dlp.fish ChangeLog AUTHORS yt-dlp.tar.gz: README.md yt-dlp.1 completions Changelog.md AUTHORS
@tar -czf yt-dlp.tar.gz --transform "s|^|yt-dlp/|" --owner 0 --group 0 \ @tar -czf $(DESTDIR)/yt-dlp.tar.gz --transform "s|^|yt-dlp/|" --owner 0 --group 0 \
--exclude '*.DS_Store' \ --exclude '*.DS_Store' \
--exclude '*.kate-swp' \ --exclude '*.kate-swp' \
--exclude '*.pyc' \ --exclude '*.pyc' \
@@ -134,8 +132,13 @@ yt-dlp.tar.gz: yt-dlp README.md README.txt yt-dlp.1 yt-dlp.bash-completion yt-dl
--exclude '.git' \ --exclude '.git' \
--exclude 'docs/_build' \ --exclude 'docs/_build' \
-- \ -- \
bin devscripts test yt_dlp docs \ devscripts test \
ChangeLog AUTHORS LICENSE README.md README.txt \ Changelog.md AUTHORS LICENSE README.md supportedsites.md \
Makefile MANIFEST.in yt-dlp.1 yt-dlp.bash-completion \ Makefile MANIFEST.in yt-dlp.1 completions \
yt-dlp.zsh yt-dlp.fish setup.py setup.cfg \ setup.py setup.cfg yt-dlp
yt-dlp
AUTHORS: .mailmap
git shortlog -s -n | cut -f2 | sort > AUTHORS
.mailmap:
git shortlog -s -e -n | awk '!(out[$$NF]++) { $$1="";sub(/^[ \t]+/,""); print}' > .mailmap
+30 -17
View File
@@ -11,7 +11,7 @@
[![PyPi Downloads](https://img.shields.io/pypi/dm/yt-dlp?label=PyPi)](https://pypi.org/project/yt-dlp) [![PyPi Downloads](https://img.shields.io/pypi/dm/yt-dlp?label=PyPi)](https://pypi.org/project/yt-dlp)
[![Doc Status](https://readthedocs.org/projects/yt-dlp/badge/?version=latest)](https://yt-dlp.readthedocs.io) [![Doc Status](https://readthedocs.org/projects/yt-dlp/badge/?version=latest)](https://yt-dlp.readthedocs.io)
A command-line program to download videos from youtube.com and many other [video platforms](docs/supportedsites.md) A command-line program to download videos from youtube.com and many other [video platforms](supportedsites.md)
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) 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)
@@ -57,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.02.10**: 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.
@@ -92,7 +92,7 @@ See [changelog](Changelog.md) or [commits](https://github.com/yt-dlp/yt-dlp/comm
**PS**: Some of these changes are already in youtube-dlc, but are still unreleased. See [this](Changelog.md#unreleased-changes-in-blackjack4494yt-dlc) for details **PS**: Some of these changes are already in youtube-dlc, but are still unreleased. See [this](Changelog.md#unreleased-changes-in-blackjack4494yt-dlc) for details
If you are coming from [youtube-dl](https://github.com/ytdl-org/youtube-dl), the amount of changes are very large. Compare [options](#options) and [supported sites](docs/supportedsites.md) with youtube-dl's to get an idea of the massive number of features/patches [youtube-dlc](https://github.com/blackjack4494/yt-dlc) has accumulated. If you are coming from [youtube-dl](https://github.com/ytdl-org/youtube-dl), the amount of changes are very large. Compare [options](#options) and [supported sites](supportedsites.md) with youtube-dl's to get an idea of the massive number of features/patches [youtube-dlc](https://github.com/blackjack4494/yt-dlc) has accumulated.
# INSTALLATION # INSTALLATION
@@ -122,12 +122,12 @@ You can also build the executable without any version info or metadata by using:
**For Unix**: **For Unix**:
You will need the required build tools: `python`, `make` (GNU), `pandoc`, `zip`, `nosetests` You will need the required build tools: `python`, `make` (GNU), `pandoc`, `zip`, `nosetests`
Then simply run `make`. You can also run `make youtube_dlc` instead to compile only the binary without updating any of the additional files Then simply run `make`. You can also run `make yt-dlp` instead to compile only the binary without updating any of the additional files
**Note**: In either platform, `devscripts\update-version.py` can be used to automatically update the version number **Note**: In either platform, `devscripts\update-version.py` can be used to automatically update the version number
# DESCRIPTION # DESCRIPTION
**yt-dlp** is a command-line program to download videos from youtube.com many other [video platforms](docs/supportedsites.md). It requires the Python interpreter, version 2.6, 2.7, or 3.2+, and it is not platform specific. It should work on your Unix box, on Windows or on macOS. It is released to the public domain, which means you can modify it, redistribute it or use it however you like. **yt-dlp** is a command-line program to download videos from youtube.com many other [video platforms](supportedsites.md). It requires the Python interpreter, version 2.6, 2.7, or 3.2+, and it is not platform specific. It should work on your Unix box, on Windows or on macOS. It is released to the public domain, which means you can modify it, redistribute it or use it however you like.
yt-dlp [OPTIONS] [--] URL [URL...] yt-dlp [OPTIONS] [--] URL [URL...]
@@ -245,7 +245,7 @@ Then simply run `make`. You can also run `make youtube_dlc` instead to compile o
"OUTPUT TEMPLATE" for a list of available "OUTPUT TEMPLATE" for a list of available
keys) to match if the key is present, !key keys) to match if the key is present, !key
to check if the key is not present, to check if the key is not present,
key>NUMBER (like "comment_count > 12", also key>NUMBER (like "view_count > 12", also
works with >=, <, <=, !=, =) to compare works with >=, <, <=, !=, =) to compare
against a number, key = 'LITERAL' (like against a number, key = 'LITERAL' (like
"uploader = 'Mike Smith'", also works with "uploader = 'Mike Smith'", also works with
@@ -317,13 +317,19 @@ Then simply run `make`. You can also run `make youtube_dlc` instead to compile o
ffmpeg ffmpeg
--hls-prefer-ffmpeg Use ffmpeg instead of the native HLS --hls-prefer-ffmpeg Use ffmpeg instead of the native HLS
downloader downloader
--hls-use-mpegts Use the mpegts container for HLS videos, --hls-use-mpegts Use the mpegts container for HLS videos;
allowing to play the video while allowing some players to play the video
downloading (some players may not be able while downloading, and reducing the chance
to play it) of file corruption if download is
--external-downloader NAME Use the specified external downloader. interrupted. This is enabled by default for
Currently supports aria2c, avconv, axel, live streams
curl, ffmpeg, httpie, wget --no-hls-use-mpegts Do not use the mpegts container for HLS
videos. This is default when not
downloading live streams
--external-downloader NAME Name or path of the external downloader to
use. Currently supports aria2c, avconv,
axel, curl, ffmpeg, httpie, wget
(Recommended: aria2c)
--downloader-args NAME:ARGS Give these arguments to the external --downloader-args NAME:ARGS Give these arguments to the external
downloader. Specify the downloader name and downloader. Specify the downloader name and
the arguments separated by a colon ":". You the arguments separated by a colon ":". You
@@ -397,7 +403,9 @@ Then simply run `make`. You can also run `make youtube_dlc` instead to compile o
--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.
--get-comments Retrieve video comments to be placed in the --get-comments Retrieve video comments to be placed in the
.info.json file .info.json file. The comments are fetched
even without this option if the extraction
is known to be quick
--load-info-json FILE JSON file containing the video information --load-info-json FILE JSON file containing the video information
(created with the "--write-info-json" (created with the "--write-info-json"
option) option)
@@ -485,6 +493,8 @@ Then simply run `make`. You can also run `make youtube_dlc` instead to compile o
--bidi-workaround Work around terminals that lack --bidi-workaround Work around terminals that lack
bidirectional text support. Requires bidiv bidirectional text support. Requires bidiv
or fribidi executable in PATH or fribidi executable in PATH
--sleep-requests SECONDS Number of seconds to sleep between requests
during data extraction
--sleep-interval SECONDS Number of seconds to sleep before each --sleep-interval SECONDS Number of seconds to sleep before each
download when used alone or a lower bound download when used alone or a lower bound
of a range for randomized sleep before each of a range for randomized sleep before each
@@ -495,7 +505,8 @@ Then simply run `make`. You can also run `make youtube_dlc` instead to compile o
before each download (maximum possible before each download (maximum possible
number of seconds to sleep). Must only be number of seconds to sleep). Must only be
used along with --min-sleep-interval used along with --min-sleep-interval
--sleep-subtitles SECONDS Enforce sleep interval on subtitles as well --sleep-subtitles SECONDS Number of seconds to sleep before each
subtitle download
## Video Format Options: ## Video Format Options:
-f, --format FORMAT Video format code, see "FORMAT SELECTION" -f, --format FORMAT Video format code, see "FORMAT SELECTION"
@@ -641,7 +652,7 @@ Then simply run `make`. You can also run `make youtube_dlc` instead to compile o
similar syntax to the output template can similar syntax to the output template can
also be used. The parsed parameters replace also be used. The parsed parameters replace
any existing values and can be use in any existing values and can be use in
output templateThis option can be used output template. This option can be used
multiple times. Example: --parse-metadata multiple times. Example: --parse-metadata
"title:%(artist)s - %(title)s" matches a "title:%(artist)s - %(title)s" matches a
title like "Coldplay - Paradise". Example title like "Coldplay - Paradise". Example
@@ -686,6 +697,8 @@ Then simply run `make`. You can also run `make youtube_dlc` instead to compile o
directory directory
## Extractor Options: ## Extractor Options:
--extractor-retries RETRIES Number of retries for known extractor
errors (default is 3), or "infinite"
--allow-dynamic-mpd Process dynamic DASH manifests (default) --allow-dynamic-mpd Process dynamic DASH manifests (default)
(Alias: --no-ignore-dynamic-mpd) (Alias: --no-ignore-dynamic-mpd)
--ignore-dynamic-mpd Do not process dynamic DASH manifests --ignore-dynamic-mpd Do not process dynamic DASH manifests
@@ -805,7 +818,7 @@ The available fields are:
- `dislike_count` (numeric): Number of negative ratings of the video - `dislike_count` (numeric): Number of negative ratings of the video
- `repost_count` (numeric): Number of reposts of the video - `repost_count` (numeric): Number of reposts of the video
- `average_rating` (numeric): Average rating give by users, the scale used depends on the webpage - `average_rating` (numeric): Average rating give by users, the scale used depends on the webpage
- `comment_count` (numeric): Number of comments on the video - `comment_count` (numeric): Number of comments on the video (For some extractors, comments are only downloaded at the end, and so this field cannot be used)
- `age_limit` (numeric): Age restriction for the video (years) - `age_limit` (numeric): Age restriction for the video (years)
- `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
+1 -1
View File
@@ -8,7 +8,7 @@ import sys
sys.path.insert(0, dirn(dirn((os.path.abspath(__file__))))) sys.path.insert(0, dirn(dirn((os.path.abspath(__file__)))))
import yt_dlp import yt_dlp
BASH_COMPLETION_FILE = "yt-dlp.bash-completion" BASH_COMPLETION_FILE = "completions/bash/yt-dlp"
BASH_COMPLETION_TEMPLATE = "devscripts/bash-completion.in" BASH_COMPLETION_TEMPLATE = "devscripts/bash-completion.in"
+1 -1
View File
@@ -10,7 +10,7 @@ sys.path.insert(0, dirn(dirn((os.path.abspath(__file__)))))
import yt_dlp import yt_dlp
from yt_dlp.utils import shell_quote from yt_dlp.utils import shell_quote
FISH_COMPLETION_FILE = 'yt-dlp.fish' FISH_COMPLETION_FILE = 'completions/fish/yt-dlp.fish'
FISH_COMPLETION_TEMPLATE = 'devscripts/fish-completion.in' FISH_COMPLETION_TEMPLATE = 'devscripts/fish-completion.in'
EXTRA_ARGS = { EXTRA_ARGS = {
+5 -5
View File
@@ -61,7 +61,7 @@ if ! type pandoc >/dev/null 2>/dev/null; then echo 'ERROR: pandoc is missing'; e
if ! python3 -c 'import rsa' 2>/dev/null; then echo 'ERROR: python3-rsa is missing'; exit 1; fi if ! python3 -c 'import rsa' 2>/dev/null; then echo 'ERROR: python3-rsa is missing'; exit 1; fi
if ! python3 -c 'import wheel' 2>/dev/null; then echo 'ERROR: wheel is missing'; exit 1; fi if ! python3 -c 'import wheel' 2>/dev/null; then echo 'ERROR: wheel is missing'; exit 1; fi
read -p "Is ChangeLog up to date? (y/n) " -n 1 read -p "Is Changelog up to date? (y/n) " -n 1
if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1; fi if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1; fi
/bin/echo -e "\n### First of all, testing..." /bin/echo -e "\n### First of all, testing..."
@@ -75,12 +75,12 @@ fi
/bin/echo -e "\n### Changing version in version.py..." /bin/echo -e "\n### Changing version in version.py..."
sed -i "s/__version__ = '.*'/__version__ = '$version'/" yt_dlp/version.py sed -i "s/__version__ = '.*'/__version__ = '$version'/" yt_dlp/version.py
/bin/echo -e "\n### Changing version in ChangeLog..." /bin/echo -e "\n### Changing version in Changelog..."
sed -i "s/<unreleased>/$version/" ChangeLog sed -i "s/<unreleased>/$version/" Changelog.md
/bin/echo -e "\n### Committing documentation, templates and yt_dlp/version.py..." /bin/echo -e "\n### Committing documentation, templates and yt_dlp/version.py..."
make README.md CONTRIBUTING.md issuetemplates supportedsites make README.md CONTRIBUTING.md issuetemplates supportedsites
git add README.md CONTRIBUTING.md .github/ISSUE_TEMPLATE/1_broken_site.md .github/ISSUE_TEMPLATE/2_site_support_request.md .github/ISSUE_TEMPLATE/3_site_feature_request.md .github/ISSUE_TEMPLATE/4_bug_report.md .github/ISSUE_TEMPLATE/5_feature_request.md .github/ISSUE_TEMPLATE/6_question.md docs/supportedsites.md yt_dlp/version.py ChangeLog git add README.md CONTRIBUTING.md .github/ISSUE_TEMPLATE/1_broken_site.md .github/ISSUE_TEMPLATE/2_site_support_request.md .github/ISSUE_TEMPLATE/3_site_feature_request.md .github/ISSUE_TEMPLATE/4_bug_report.md .github/ISSUE_TEMPLATE/5_feature_request.md .github/ISSUE_TEMPLATE/6_question.md docs/supportedsites.md yt_dlp/version.py Changelog.md
git commit $gpg_sign_commits -m "release $version" git commit $gpg_sign_commits -m "release $version"
/bin/echo -e "\n### Now tagging, signing and pushing..." /bin/echo -e "\n### Now tagging, signing and pushing..."
@@ -111,7 +111,7 @@ RELEASE_FILES="yt-dlp yt-dlp.exe yt-dlp-$version.tar.gz"
for f in $RELEASE_FILES; do gpg --passphrase-repeat 5 --detach-sig "build/$version/$f"; done for f in $RELEASE_FILES; do gpg --passphrase-repeat 5 --detach-sig "build/$version/$f"; done
ROOT=$(pwd) ROOT=$(pwd)
python devscripts/create-github-release.py ChangeLog $version "$ROOT/build/$version" python devscripts/create-github-release.py Changelog.md $version "$ROOT/build/$version"
ssh ytdl@yt-dl.org "sh html/update_latest.sh $version" ssh ytdl@yt-dl.org "sh html/update_latest.sh $version"
+1 -1
View File
@@ -8,7 +8,7 @@ import sys
sys.path.insert(0, dirn(dirn((os.path.abspath(__file__))))) sys.path.insert(0, dirn(dirn((os.path.abspath(__file__)))))
import yt_dlp import yt_dlp
ZSH_COMPLETION_FILE = "yt-dlp.zsh" ZSH_COMPLETION_FILE = "completions/zsh/_yt-dlp"
ZSH_COMPLETION_TEMPLATE = "devscripts/zsh-completion.in" ZSH_COMPLETION_TEMPLATE = "devscripts/zsh-completion.in"
+5
View File
@@ -0,0 +1,5 @@
---
orphan: true
---
```{include} ../Changelog.md
```
+6
View File
@@ -0,0 +1,6 @@
---
orphan: true
---
# LICENSE
```{include} ../LICENSE
```
+2
View File
@@ -0,0 +1,2 @@
```{include} ../README.md
```
+8 -15
View File
@@ -7,26 +7,21 @@ import os
# Allows to import yt-dlp # Allows to import yt-dlp
sys.path.insert(0, os.path.abspath('..')) sys.path.insert(0, os.path.abspath('..'))
from recommonmark.transform import AutoStructify
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
# The suffix of source filenames.
source_suffix = ['.rst', '.md']
# Add any Sphinx extension module names here, as strings. They can be # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
'sphinx.ext.autodoc', 'myst_parser',
'recommonmark',
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = 'README'
# General information about the project. # General information about the project.
project = u'yt-dlp' project = u'yt-dlp'
@@ -64,12 +59,10 @@ highlight_language = 'none'
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static'] # html_static_path = ['_static']
# Enable heading anchors
myst_heading_anchors = 4
def setup(app): # Suppress heading warnings
app.add_config_value('recommonmark_config', { suppress_warnings = [
'enable_math': False, 'myst.header',
'enable_inline_math': False, ]
'enable_eval_rst': True,
'enable_auto_toc_tree': True,
}, True)
app.add_transform(AutoStructify)
-1
View File
@@ -1 +0,0 @@
../README.md
+1 -2
View File
@@ -1,2 +1 @@
recommonmark>=0.6.0 myst-parser
m2r2
+5 -1246
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
---
orphan: true
---
# ytdlp_plugins
See [https://github.com/yt-dlp/yt-dlp/tree/master/ytdlp_plugins](https://github.com/yt-dlp/yt-dlp/tree/master/ytdlp_plugins).
+7 -6
View File
@@ -27,8 +27,9 @@ if len(sys.argv) >= 2 and sys.argv[1] == 'py2exe':
print("inv") print("inv")
else: else:
files_spec = [ files_spec = [
('etc/bash_completion.d', ['yt-dlp.bash-completion']), ('share/bash-completion/completions', ['completions/bash/yt-dlp']),
('etc/fish/completions', ['yt-dlp.fish']), ('share/zsh/site-functions', ['completions/zsh/_yt-dlp']),
('share/fish/vendor_completions.d', ['completions/fish/yt-dlp.fish']),
('share/doc/yt_dlp', ['README.txt']), ('share/doc/yt_dlp', ['README.txt']),
('share/man/man1', ['yt-dlp.1']) ('share/man/man1', ['yt-dlp.1'])
] ]
@@ -38,7 +39,7 @@ else:
resfiles = [] resfiles = []
for fn in files: for fn in files:
if not os.path.exists(fn): if not os.path.exists(fn):
warnings.warn('Skipping file %s since it is not present. Type make to build all automatically generated files.' % fn) warnings.warn('Skipping file %s since it is not present. Try running `make pypi-files` first.' % fn)
else: else:
resfiles.append(fn) resfiles.append(fn)
data_files.append((dirname, resfiles)) data_files.append((dirname, resfiles))
@@ -86,7 +87,7 @@ setup(
#'Funding': 'https://donate.pypi.org', #'Funding': 'https://donate.pypi.org',
}, },
classifiers=[ classifiers=[
"Topic :: Multimedia :: Video", "Topic :: Multimedia :: Video",
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Environment :: Console", "Environment :: Console",
"Programming Language :: Python", "Programming Language :: Python",
@@ -110,7 +111,7 @@ setup(
"Operating System :: OS Independent", "Operating System :: OS Independent",
], ],
python_requires='>=2.6', python_requires='>=2.6',
cmdclass={'build_lazy_extractors': build_lazy_extractors}, cmdclass={'build_lazy_extractors': build_lazy_extractors},
**params **params
) )
+1248
View File
File diff suppressed because it is too large Load Diff
+36 -18
View File
@@ -324,6 +324,8 @@ class YoutubeDL(object):
source_address: Client-side IP address to bind to. source_address: Client-side IP address to bind to.
call_home: Boolean, true iff we are allowed to contact the call_home: Boolean, true iff we are allowed to contact the
yt-dlp servers for debugging. (BROKEN) yt-dlp servers for debugging. (BROKEN)
sleep_interval_requests: Number of seconds to sleep between requests
during extraction
sleep_interval: Number of seconds to sleep before each download when sleep_interval: Number of seconds to sleep before each download when
used alone or a lower bound of a range for randomized used alone or a lower bound of a range for randomized
sleep before each download (minimum possible number sleep before each download (minimum possible number
@@ -334,6 +336,7 @@ class YoutubeDL(object):
Must only be used along with sleep_interval. Must only be used along with sleep_interval.
Actual sleep time will be a random float from range Actual sleep time will be a random float from range
[sleep_interval; max_sleep_interval]. [sleep_interval; max_sleep_interval].
sleep_interval_subtitles: Number of seconds to sleep before each subtitle download
listformats: Print an overview of available video formats and exit. listformats: Print an overview of available video formats and exit.
list_thumbnails: Print a table of all thumbnails and exit. list_thumbnails: Print a table of all thumbnails and exit.
match_filter: A function that gets called with the info_dict of match_filter: A function that gets called with the info_dict of
@@ -378,17 +381,18 @@ class YoutubeDL(object):
Use 'default' as the name for arguments to passed to all PP Use 'default' as the name for arguments to passed to all PP
The following options are used by the extractors: The following options are used by the extractors:
dynamic_mpd: Whether to process dynamic DASH manifests (default: True) extractor_retries: Number of times to retry for known errors
dynamic_mpd: Whether to process dynamic DASH manifests (default: True)
hls_split_discontinuity: Split HLS playlists to different formats at hls_split_discontinuity: Split HLS playlists to different formats at
discontinuities such as ad breaks (default: False) discontinuities such as ad breaks (default: False)
youtube_include_dash_manifest: If True (default), DASH manifests and related youtube_include_dash_manifest: If True (default), DASH manifests and related
data will be downloaded and processed by extractor. data will be downloaded and processed by extractor.
You can reduce network I/O by disabling it if you don't You can reduce network I/O by disabling it if you don't
care about DASH. (only for youtube) care about DASH. (only for youtube)
youtube_include_hls_manifest: If True (default), HLS manifests and related youtube_include_hls_manifest: If True (default), HLS manifests and related
data will be downloaded and processed by extractor. data will be downloaded and processed by extractor.
You can reduce network I/O by disabling it if you don't You can reduce network I/O by disabling it if you don't
care about HLS. (only for youtube) care about HLS. (only for youtube)
""" """
_NUMERIC_FIELDS = set(( _NUMERIC_FIELDS = set((
@@ -406,6 +410,7 @@ class YoutubeDL(object):
_ies = [] _ies = []
_pps = {'beforedl': [], 'aftermove': [], 'normal': []} _pps = {'beforedl': [], 'aftermove': [], 'normal': []}
__prepare_filename_warned = False __prepare_filename_warned = False
_first_webpage_request = True
_download_retcode = None _download_retcode = None
_num_downloads = None _num_downloads = None
_playlist_level = 0 _playlist_level = 0
@@ -420,6 +425,7 @@ class YoutubeDL(object):
self._ies_instances = {} self._ies_instances = {}
self._pps = {'beforedl': [], 'aftermove': [], 'normal': []} self._pps = {'beforedl': [], 'aftermove': [], 'normal': []}
self.__prepare_filename_warned = False self.__prepare_filename_warned = False
self._first_webpage_request = True
self._post_hooks = [] self._post_hooks = []
self._progress_hooks = [] self._progress_hooks = []
self._download_retcode = 0 self._download_retcode = 0
@@ -2036,6 +2042,7 @@ class YoutubeDL(object):
self.to_stdout(formatSeconds(info_dict['duration'])) self.to_stdout(formatSeconds(info_dict['duration']))
print_mandatory('format') print_mandatory('format')
if self.params.get('forcejson', False): if self.params.get('forcejson', False):
self.post_extract(info_dict)
self.to_stdout(json.dumps(info_dict)) self.to_stdout(json.dumps(info_dict))
def process_info(self, info_dict): def process_info(self, info_dict):
@@ -2059,6 +2066,7 @@ class YoutubeDL(object):
if self._match_entry(info_dict, incomplete=False) is not None: if self._match_entry(info_dict, incomplete=False) is not None:
return return
self.post_extract(info_dict)
self._num_downloads += 1 self._num_downloads += 1
info_dict = self.pre_process(info_dict) info_dict = self.pre_process(info_dict)
@@ -2166,15 +2174,6 @@ class YoutubeDL(object):
else: else:
try: try:
dl(sub_filename, sub_info, subtitle=True) dl(sub_filename, sub_info, subtitle=True)
'''
if self.params.get('sleep_interval_subtitles', False):
dl(sub_filename, sub_info)
else:
sub_data = ie._request_webpage(
sub_info['url'], info_dict['id'], note=False).read()
with io.open(encodeFilename(sub_filename), 'wb') as subfile:
subfile.write(sub_data)
'''
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' %
@@ -2501,6 +2500,7 @@ class YoutubeDL(object):
raise raise
else: else:
if self.params.get('dump_single_json', False): if self.params.get('dump_single_json', False):
self.post_extract(res)
self.to_stdout(json.dumps(res)) self.to_stdout(json.dumps(res))
return self._download_retcode return self._download_retcode
@@ -2549,6 +2549,24 @@ class YoutubeDL(object):
del files_to_move[old_filename] del files_to_move[old_filename]
return files_to_move, infodict return files_to_move, infodict
@staticmethod
def post_extract(info_dict):
def actual_post_extract(info_dict):
if info_dict.get('_type') in ('playlist', 'multi_video'):
for video_dict in info_dict.get('entries', {}):
actual_post_extract(video_dict)
return
if '__post_extractor' not in info_dict:
return
post_extractor = info_dict['__post_extractor']
if post_extractor:
info_dict.update(post_extractor().items())
del info_dict['__post_extractor']
return
actual_post_extract(info_dict)
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']:
@@ -2940,7 +2958,7 @@ class YoutubeDL(object):
self.to_screen('[%s] %s: Thumbnail %sis already present' % self.to_screen('[%s] %s: Thumbnail %sis already present' %
(info_dict['extractor'], info_dict['id'], thumb_display_id)) (info_dict['extractor'], info_dict['id'], thumb_display_id))
else: else:
self.to_screen('[%s] %s: Downloading thumbnail %s...' % self.to_screen('[%s] %s: Downloading thumbnail %s ...' %
(info_dict['extractor'], info_dict['id'], thumb_display_id)) (info_dict['extractor'], info_dict['id'], thumb_display_id))
try: try:
uf = self.urlopen(t['url']) uf = self.urlopen(t['url'])
+19 -5
View File
@@ -169,25 +169,33 @@ def _real_main(argv=None):
parser.error('max sleep interval must be greater than or equal to min sleep interval') parser.error('max sleep interval must be greater than or equal to min sleep interval')
else: else:
opts.max_sleep_interval = opts.sleep_interval opts.max_sleep_interval = opts.sleep_interval
if opts.sleep_interval_subtitles is not None:
if opts.sleep_interval_subtitles < 0:
parser.error('subtitles sleep interval must be positive or 0')
if opts.sleep_interval_requests is not None:
if opts.sleep_interval_requests < 0:
parser.error('requests sleep interval must be positive or 0')
if opts.ap_mso and opts.ap_mso not in MSO_INFO: if opts.ap_mso and opts.ap_mso not in MSO_INFO:
parser.error('Unsupported TV Provider, use --ap-list-mso to get a list of supported TV Providers') parser.error('Unsupported TV Provider, use --ap-list-mso to get a list of supported TV Providers')
if opts.overwrites: if opts.overwrites:
# --yes-overwrites implies --no-continue # --yes-overwrites implies --no-continue
opts.continue_dl = False opts.continue_dl = False
def parse_retries(retries): def parse_retries(retries, name=''):
if retries in ('inf', 'infinite'): if retries in ('inf', 'infinite'):
parsed_retries = float('inf') parsed_retries = float('inf')
else: else:
try: try:
parsed_retries = int(retries) parsed_retries = int(retries)
except (TypeError, ValueError): except (TypeError, ValueError):
parser.error('invalid retry count specified') parser.error('invalid %sretry count specified' % name)
return parsed_retries return parsed_retries
if opts.retries is not None: if opts.retries is not None:
opts.retries = parse_retries(opts.retries) opts.retries = parse_retries(opts.retries)
if opts.fragment_retries is not None: if opts.fragment_retries is not None:
opts.fragment_retries = parse_retries(opts.fragment_retries) opts.fragment_retries = parse_retries(opts.fragment_retries, 'fragment ')
if opts.extractor_retries is not None:
opts.extractor_retries = parse_retries(opts.extractor_retries, 'extractor ')
if opts.buffersize is not None: if opts.buffersize is not None:
numeric_buffersize = FileDownloader.parse_bytes(opts.buffersize) numeric_buffersize = FileDownloader.parse_bytes(opts.buffersize)
if numeric_buffersize is None: if numeric_buffersize is None:
@@ -262,6 +270,11 @@ def _real_main(argv=None):
any_printing = opts.print_json any_printing = opts.print_json
download_archive_fn = expand_path(opts.download_archive) if opts.download_archive is not None else opts.download_archive download_archive_fn = expand_path(opts.download_archive) if opts.download_archive is not None else opts.download_archive
# If JSON is not printed anywhere, but comments are requested, save it to file
printing_json = opts.dumpjson or opts.print_json or opts.dump_single_json
if opts.getcomments and not printing_json:
opts.writeinfojson = True
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:
@@ -447,6 +460,7 @@ def _real_main(argv=None):
'overwrites': opts.overwrites, 'overwrites': opts.overwrites,
'retries': opts.retries, 'retries': opts.retries,
'fragment_retries': opts.fragment_retries, 'fragment_retries': opts.fragment_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,
'buffersize': opts.buffersize, 'buffersize': opts.buffersize,
@@ -466,7 +480,7 @@ def _real_main(argv=None):
'updatetime': opts.updatetime, 'updatetime': opts.updatetime,
'writedescription': opts.writedescription, 'writedescription': opts.writedescription,
'writeannotations': opts.writeannotations, 'writeannotations': opts.writeannotations,
'writeinfojson': opts.writeinfojson or opts.getcomments, 'writeinfojson': opts.writeinfojson,
'allow_playlist_files': opts.allow_playlist_files, 'allow_playlist_files': opts.allow_playlist_files,
'getcomments': opts.getcomments, 'getcomments': opts.getcomments,
'writethumbnail': opts.writethumbnail, 'writethumbnail': opts.writethumbnail,
@@ -524,6 +538,7 @@ def _real_main(argv=None):
'fixup': opts.fixup, 'fixup': opts.fixup,
'source_address': opts.source_address, 'source_address': opts.source_address,
'call_home': opts.call_home, 'call_home': opts.call_home,
'sleep_interval_requests': opts.sleep_interval_requests,
'sleep_interval': opts.sleep_interval, 'sleep_interval': opts.sleep_interval,
'max_sleep_interval': opts.max_sleep_interval, 'max_sleep_interval': opts.max_sleep_interval,
'sleep_interval_subtitles': opts.sleep_interval_subtitles, 'sleep_interval_subtitles': opts.sleep_interval_subtitles,
@@ -541,7 +556,6 @@ def _real_main(argv=None):
'postprocessor_args': opts.postprocessor_args, 'postprocessor_args': opts.postprocessor_args,
'cn_verification_proxy': opts.cn_verification_proxy, 'cn_verification_proxy': opts.cn_verification_proxy,
'geo_verification_proxy': opts.geo_verification_proxy, 'geo_verification_proxy': opts.geo_verification_proxy,
'config_location': opts.config_location,
'geo_bypass': opts.geo_bypass, 'geo_bypass': opts.geo_bypass,
'geo_bypass_country': opts.geo_bypass_country, 'geo_bypass_country': opts.geo_bypass_country,
'geo_bypass_ip_block': opts.geo_bypass_ip_block, 'geo_bypass_ip_block': opts.geo_bypass_ip_block,
+1 -1
View File
@@ -53,7 +53,7 @@ def get_suitable_downloader(info_dict, params={}, default=HttpFD):
external_downloader = params.get('external_downloader') external_downloader = params.get('external_downloader')
if external_downloader is not None: if external_downloader is not None:
ed = get_external_downloader(external_downloader) ed = get_external_downloader(external_downloader)
if ed.can_download(info_dict): if ed.can_download(info_dict, external_downloader):
return ed return ed
if protocol.startswith('m3u8'): if protocol.startswith('m3u8'):
+3 -3
View File
@@ -312,7 +312,7 @@ class FileDownloader(object):
def report_retry(self, err, count, retries): def report_retry(self, err, count, retries):
"""Report retry in case of HTTP error 5xx""" """Report retry in case of HTTP error 5xx"""
self.to_screen( self.to_screen(
'[download] Got server HTTP error: %s. Retrying (attempt %d of %s)...' '[download] Got server HTTP error: %s. Retrying (attempt %d of %s) ...'
% (error_to_compat_str(err), count, self.format_retries(retries))) % (error_to_compat_str(err), count, self.format_retries(retries)))
def report_file_already_downloaded(self, file_name): def report_file_already_downloaded(self, file_name):
@@ -359,7 +359,7 @@ class FileDownloader(object):
max_sleep_interval = self.params.get('max_sleep_interval', min_sleep_interval) max_sleep_interval = self.params.get('max_sleep_interval', min_sleep_interval)
sleep_interval = random.uniform(min_sleep_interval, max_sleep_interval) sleep_interval = random.uniform(min_sleep_interval, max_sleep_interval)
self.to_screen( self.to_screen(
'[download] Sleeping %s seconds...' % ( '[download] Sleeping %s seconds ...' % (
int(sleep_interval) if sleep_interval.is_integer() int(sleep_interval) if sleep_interval.is_integer()
else '%.2f' % sleep_interval)) else '%.2f' % sleep_interval))
time.sleep(sleep_interval) time.sleep(sleep_interval)
@@ -369,7 +369,7 @@ class FileDownloader(object):
sleep_interval_sub = self.params.get('sleep_interval_subtitles') sleep_interval_sub = self.params.get('sleep_interval_subtitles')
if sleep_interval_sub > 0: if sleep_interval_sub > 0:
self.to_screen( self.to_screen(
'[download] Sleeping %s seconds...' % ( '[download] Sleeping %s seconds ...' % (
sleep_interval_sub)) sleep_interval_sub))
time.sleep(sleep_interval_sub) time.sleep(sleep_interval_sub)
return self.real_download(filename, info_dict), True return self.real_download(filename, info_dict), True
+8 -5
View File
@@ -85,16 +85,16 @@ class ExternalFD(FileDownloader):
return self.params.get('external_downloader') return self.params.get('external_downloader')
@classmethod @classmethod
def available(cls): def available(cls, path=None):
return check_executable(cls.get_basename(), [cls.AVAILABLE_OPT]) return check_executable(path or cls.get_basename(), [cls.AVAILABLE_OPT])
@classmethod @classmethod
def supports(cls, info_dict): def supports(cls, info_dict):
return info_dict['protocol'] in cls.SUPPORTED_PROTOCOLS return info_dict['protocol'] in cls.SUPPORTED_PROTOCOLS
@classmethod @classmethod
def can_download(cls, info_dict): def can_download(cls, info_dict, path=None):
return cls.available() and cls.supports(info_dict) return cls.available(path) and cls.supports(info_dict)
def _option(self, command_option, param): def _option(self, command_option, param):
return cli_option(self.params, command_option, param) return cli_option(self.params, command_option, param)
@@ -398,7 +398,10 @@ class FFmpegFD(ExternalFD):
args += ['-fs', compat_str(self._TEST_FILE_SIZE)] args += ['-fs', compat_str(self._TEST_FILE_SIZE)]
if protocol in ('m3u8', 'm3u8_native'): if protocol in ('m3u8', 'm3u8_native'):
if self.params.get('hls_use_mpegts', False) or tmpfilename == '-': use_mpegts = (tmpfilename == '-') or self.params.get('hls_use_mpegts')
if use_mpegts is None:
use_mpegts = info_dict.get('is_live')
if use_mpegts:
args += ['-f', 'mpegts'] args += ['-f', 'mpegts']
else: else:
args += ['-f', 'mp4'] args += ['-f', 'mp4']
+3 -3
View File
@@ -55,11 +55,11 @@ class FragmentFD(FileDownloader):
def report_retry_fragment(self, err, frag_index, count, retries): def report_retry_fragment(self, err, frag_index, count, retries):
self.to_screen( self.to_screen(
'[download] Got server HTTP error: %s. Retrying fragment %d (attempt %d of %s)...' '[download] Got server HTTP error: %s. Retrying fragment %d (attempt %d of %s) ...'
% (error_to_compat_str(err), frag_index, count, self.format_retries(retries))) % (error_to_compat_str(err), frag_index, count, self.format_retries(retries)))
def report_skip_fragment(self, frag_index): def report_skip_fragment(self, frag_index):
self.to_screen('[download] Skipping fragment %d...' % frag_index) self.to_screen('[download] Skipping fragment %d ...' % frag_index)
def _prepare_url(self, info_dict, url): def _prepare_url(self, info_dict, url):
headers = info_dict.get('http_headers') headers = info_dict.get('http_headers')
@@ -174,7 +174,7 @@ class FragmentFD(FileDownloader):
'.ytdl file is corrupt' if is_corrupt else '.ytdl file is corrupt' if is_corrupt else
'Inconsistent state of incomplete fragment download') 'Inconsistent state of incomplete fragment download')
self.report_warning( self.report_warning(
'%s. Restarting from the beginning...' % message) '%s. Restarting from the beginning ...' % message)
ctx['fragment_index'] = resume_len = 0 ctx['fragment_index'] = resume_len = 0
if 'ytdl_corrupt' in ctx: if 'ytdl_corrupt' in ctx:
del ctx['ytdl_corrupt'] del ctx['ytdl_corrupt']
+1 -1
View File
@@ -29,7 +29,7 @@ class NiconicoDmcFD(FileDownloader):
heartbeat_url = heartbeat_info_dict['url'] heartbeat_url = heartbeat_info_dict['url']
heartbeat_data = heartbeat_info_dict['data'] heartbeat_data = heartbeat_info_dict['data']
heartbeat_interval = heartbeat_info_dict.get('interval', 30) heartbeat_interval = heartbeat_info_dict.get('interval', 30)
self.to_screen('[%s] Heartbeat with %s second interval...' % (self.FD_NAME, heartbeat_interval)) self.to_screen('[%s] Heartbeat with %s second interval ...' % (self.FD_NAME, heartbeat_interval))
def heartbeat(): def heartbeat():
try: try:
+37
View File
@@ -0,0 +1,37 @@
# coding: utf-8
from __future__ import unicode_literals
from .brightcove import BrightcoveNewIE
from ..utils import extract_attributes
class BandaiChannelIE(BrightcoveNewIE):
IE_NAME = 'bandaichannel'
_VALID_URL = r'https?://(?:www\.)?b-ch\.com/titles/(?P<id>\d+/\d+)'
_TESTS = [{
'url': 'https://www.b-ch.com/titles/514/001',
'md5': 'a0f2d787baa5729bed71108257f613a4',
'info_dict': {
'id': '6128044564001',
'ext': 'mp4',
'title': 'メタルファイターMIKU 第1話',
'timestamp': 1580354056,
'uploader_id': '5797077852001',
'upload_date': '20200130',
'duration': 1387.733,
},
'params': {
'format': 'bestvideo',
'skip_download': True,
},
}]
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
attrs = extract_attributes(self._search_regex(
r'(<video-js[^>]+\bid="bcplayer"[^>]*>)', webpage, 'player'))
bc = self._download_json(
'https://pbifcd.b-ch.com/v1/playbackinfo/ST/70/' + attrs['data-info'],
video_id, headers={'X-API-KEY': attrs['data-auth'].strip()})['bc']
return self._parse_brightcove_metadata(bc, bc['id'])
+53 -8
View File
@@ -5,10 +5,15 @@ import itertools
import re import re
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import (
compat_etree_Element,
compat_HTTPError,
compat_urlparse,
)
from ..utils import ( from ..utils import (
ExtractorError,
clean_html, clean_html,
dict_get, dict_get,
ExtractorError,
float_or_none, float_or_none,
get_element_by_class, get_element_by_class,
int_or_none, int_or_none,
@@ -21,11 +26,6 @@ from ..utils import (
urlencode_postdata, urlencode_postdata,
urljoin, urljoin,
) )
from ..compat import (
compat_etree_Element,
compat_HTTPError,
compat_urlparse,
)
class BBCCoUkIE(InfoExtractor): class BBCCoUkIE(InfoExtractor):
@@ -793,6 +793,20 @@ class BBCIE(BBCCoUkIE):
'description': 'Learn English words and phrases from this story', 'description': 'Learn English words and phrases from this story',
}, },
'add_ie': [BBCCoUkIE.ie_key()], 'add_ie': [BBCCoUkIE.ie_key()],
}, {
# BBC Reel
'url': 'https://www.bbc.com/reel/video/p07c6sb6/how-positive-thinking-is-harming-your-happiness',
'info_dict': {
'id': 'p07c6sb9',
'ext': 'mp4',
'title': 'How positive thinking is harming your happiness',
'alt_title': 'The downsides of positive thinking',
'description': 'md5:fad74b31da60d83b8265954ee42d85b4',
'duration': 235,
'thumbnail': r're:https?://.+/p07c9dsr.jpg',
'upload_date': '20190604',
'categories': ['Psychology'],
},
}] }]
@classmethod @classmethod
@@ -929,7 +943,7 @@ class BBCIE(BBCCoUkIE):
else: else:
entry['title'] = info['title'] entry['title'] = info['title']
entry['formats'].extend(info['formats']) entry['formats'].extend(info['formats'])
except Exception as e: except ExtractorError as e:
# Some playlist URL may fail with 500, at the same time # Some playlist URL may fail with 500, at the same time
# the other one may work fine (e.g. # the other one may work fine (e.g.
# http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu) # http://www.bbc.com/turkce/haberler/2015/06/150615_telabyad_kentin_cogu)
@@ -980,6 +994,37 @@ class BBCIE(BBCCoUkIE):
'subtitles': subtitles, 'subtitles': subtitles,
} }
# bbc reel (e.g. https://www.bbc.com/reel/video/p07c6sb6/how-positive-thinking-is-harming-your-happiness)
initial_data = self._parse_json(self._html_search_regex(
r'<script[^>]+id=(["\'])initial-data\1[^>]+data-json=(["\'])(?P<json>(?:(?!\2).)+)',
webpage, 'initial data', default='{}', group='json'), playlist_id, fatal=False)
if initial_data:
init_data = try_get(
initial_data, lambda x: x['initData']['items'][0], dict) or {}
smp_data = init_data.get('smpData') or {}
clip_data = try_get(smp_data, lambda x: x['items'][0], dict) or {}
version_id = clip_data.get('versionID')
if version_id:
title = smp_data['title']
formats, subtitles = self._download_media_selector(version_id)
self._sort_formats(formats)
image_url = smp_data.get('holdingImageURL')
display_date = init_data.get('displayDate')
topic_title = init_data.get('topicTitle')
return {
'id': version_id,
'title': title,
'formats': formats,
'alt_title': init_data.get('shortTitle'),
'thumbnail': image_url.replace('$recipe', 'raw') if image_url else None,
'description': smp_data.get('summary') or init_data.get('shortSummary'),
'upload_date': display_date.replace('-', '') if display_date else None,
'subtitles': subtitles,
'duration': int_or_none(clip_data.get('duration')),
'categories': [topic_title] if topic_title else None,
}
# Morph based embed (e.g. http://www.bbc.co.uk/sport/live/olympics/36895975) # Morph based embed (e.g. http://www.bbc.co.uk/sport/live/olympics/36895975)
# There are several setPayload calls may be present but the video # There are several setPayload calls may be present but the video
# seems to be always related to the first one # seems to be always related to the first one
@@ -1041,7 +1086,7 @@ class BBCIE(BBCCoUkIE):
thumbnail = None thumbnail = None
image_url = current_programme.get('image_url') image_url = current_programme.get('image_url')
if image_url: if image_url:
thumbnail = image_url.replace('{recipe}', '1920x1920') thumbnail = image_url.replace('{recipe}', 'raw')
return { return {
'id': programme_id, 'id': programme_id,
'title': title, 'title': title,
+10 -6
View File
@@ -255,10 +255,6 @@ class BiliBiliIE(InfoExtractor):
info['uploader'] = self._html_search_meta( info['uploader'] = self._html_search_meta(
'author', webpage, 'uploader', default=None) 'author', webpage, 'uploader', default=None)
comments = None
if self._downloader.params.get('getcomments', False):
comments = self._get_all_comment_pages(video_id)
raw_danmaku = self._get_raw_danmaku(video_id, cid) raw_danmaku = self._get_raw_danmaku(video_id, cid)
raw_tags = self._get_tags(video_id) raw_tags = self._get_tags(video_id)
@@ -266,11 +262,18 @@ class BiliBiliIE(InfoExtractor):
top_level_info = { top_level_info = {
'raw_danmaku': raw_danmaku, 'raw_danmaku': raw_danmaku,
'comments': comments,
'comment_count': len(comments) if comments is not None else None,
'tags': tags, 'tags': tags,
'raw_tags': raw_tags, 'raw_tags': raw_tags,
} }
if self._downloader.params.get('getcomments', False):
def get_comments():
comments = self._get_all_comment_pages(video_id)
return {
'comments': comments,
'comment_count': len(comments)
}
top_level_info['__post_extractor'] = get_comments
''' '''
# Requires https://github.com/m13253/danmaku2ass which is licenced under GPL3 # Requires https://github.com/m13253/danmaku2ass which is licenced under GPL3
@@ -555,6 +558,7 @@ class BilibiliAudioIE(BilibiliAudioBaseIE):
formats = [{ formats = [{
'url': play_data['cdns'][0], 'url': play_data['cdns'][0],
'filesize': int_or_none(play_data.get('size')), 'filesize': int_or_none(play_data.get('size')),
'vcodec': 'none'
}] }]
song = self._call_api('song/info', au_id) song = self._call_api('song/info', au_id)
+27 -6
View File
@@ -294,6 +294,14 @@ class InfoExtractor(object):
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').
__post_extractor: A function to be called just before the metadata is
written to either disk, logger or console. The function
must return a dict which will be added to the info_dict.
This is usefull for additional information that is
time-consuming to extract. Note that the fields thus
extracted will not be available to output template and
match_filter. So, only "comments" and "comment_count" are
currently allowed to be extracted via this method.
The following fields should only be used when the video belongs to some logical The following fields should only be used when the video belongs to some logical
chapter or section: chapter or section:
@@ -606,6 +614,14 @@ class InfoExtractor(object):
See _download_webpage docstring for arguments specification. See _download_webpage docstring for arguments specification.
""" """
if not self._downloader._first_webpage_request:
sleep_interval = float_or_none(self._downloader.params.get('sleep_interval_requests')) or 0
if sleep_interval > 0:
self.to_screen('Sleeping %s seconds ...' % sleep_interval)
time.sleep(sleep_interval)
else:
self._downloader._first_webpage_request = False
if note is None: if note is None:
self.report_download_webpage(video_id) self.report_download_webpage(video_id)
elif note is not False: elif note is not False:
@@ -1888,13 +1904,16 @@ class InfoExtractor(object):
# media playlist and MUST NOT appear in master playlist thus we can # media playlist and MUST NOT appear in master playlist thus we can
# clearly detect media playlist with this criterion. # clearly detect media playlist with this criterion.
def _extract_m3u8_playlist_formats(format_url, m3u8_doc=None): def _extract_m3u8_playlist_formats(format_url=None, m3u8_doc=None, video_id=None,
fatal=True, data=None, headers={}):
if not m3u8_doc: if not m3u8_doc:
if not format_url:
return []
res = self._download_webpage_handle( res = self._download_webpage_handle(
format_url, video_id, format_url, video_id,
note=False, note=False,
errnote=errnote or 'Failed to download m3u8 playlist information', errnote='Failed to download m3u8 playlist information',
fatal=fatal, data=data, headers=headers, query=query) fatal=fatal, data=data, headers=headers)
if res is False: if res is False:
return [] return []
@@ -1928,7 +1947,7 @@ class InfoExtractor(object):
if '#EXT-X-TARGETDURATION' in m3u8_doc: # media playlist, return as is if '#EXT-X-TARGETDURATION' in m3u8_doc: # media playlist, return as is
playlist_formats = _extract_m3u8_playlist_formats(m3u8_doc, True) playlist_formats = _extract_m3u8_playlist_formats(m3u8_doc=m3u8_doc)
for format in playlist_formats: for format in playlist_formats:
format_id = [] format_id = []
@@ -1966,7 +1985,8 @@ class InfoExtractor(object):
if media_url: if media_url:
manifest_url = format_url(media_url) manifest_url = format_url(media_url)
format_id = [] format_id = []
playlist_formats = _extract_m3u8_playlist_formats(manifest_url) playlist_formats = _extract_m3u8_playlist_formats(manifest_url, video_id=video_id,
fatal=fatal, data=data, headers=headers)
for format in playlist_formats: for format in playlist_formats:
format_index = format.get('index') format_index = format.get('index')
@@ -2027,7 +2047,8 @@ class InfoExtractor(object):
or last_stream_inf.get('BANDWIDTH'), scale=1000) or last_stream_inf.get('BANDWIDTH'), scale=1000)
manifest_url = format_url(line.strip()) manifest_url = format_url(line.strip())
playlist_formats = _extract_m3u8_playlist_formats(manifest_url) playlist_formats = _extract_m3u8_playlist_formats(manifest_url, video_id=video_id,
fatal=fatal, data=data, headers=headers)
for format in playlist_formats: for format in playlist_formats:
format_id = [] format_id = []
+35 -185
View File
@@ -1,193 +1,43 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import re from .zdf import ZDFIE
from .common import InfoExtractor
from ..utils import (
int_or_none,
unified_strdate,
xpath_text,
determine_ext,
float_or_none,
ExtractorError,
)
class DreiSatIE(InfoExtractor): class DreiSatIE(ZDFIE):
IE_NAME = '3sat' IE_NAME = '3sat'
_GEO_COUNTRIES = ['DE'] _VALID_URL = r'https?://(?:www\.)?3sat\.de/(?:[^/]+/)*(?P<id>[^/?#&]+)\.html'
_VALID_URL = r'https?://(?:www\.)?3sat\.de/mediathek/(?:(?:index|mediathek)\.php)?\?(?:(?:mode|display)=[^&]+&)*obj=(?P<id>[0-9]+)' _TESTS = [{
_TESTS = [ # Same as https://www.zdf.de/dokumentation/ab-18/10-wochen-sommer-102.html
{ 'url': 'https://www.3sat.de/film/ab-18/10-wochen-sommer-108.html',
'url': 'http://www.3sat.de/mediathek/index.php?mode=play&obj=45918', 'md5': '0aff3e7bc72c8813f5e0fae333316a1d',
'md5': 'be37228896d30a88f315b638900a026e', 'info_dict': {
'info_dict': { 'id': '141007_ab18_10wochensommer_film',
'id': '45918', 'ext': 'mp4',
'ext': 'mp4', 'title': 'Ab 18! - 10 Wochen Sommer',
'title': 'Waidmannsheil', 'description': 'md5:8253f41dc99ce2c3ff892dac2d65fe26',
'description': 'md5:cce00ca1d70e21425e72c86a98a56817', 'duration': 2660,
'uploader': 'SCHWEIZWEIT', 'timestamp': 1608604200,
'uploader_id': '100000210', 'upload_date': '20201222',
'upload_date': '20140913'
},
'params': {
'skip_download': True, # m3u8 downloads
}
}, },
{ }, {
'url': 'http://www.3sat.de/mediathek/mediathek.php?mode=play&obj=51066', 'url': 'https://www.3sat.de/gesellschaft/schweizweit/waidmannsheil-100.html',
'only_matching': True, 'info_dict': {
'id': '140913_sendung_schweizweit',
'ext': 'mp4',
'title': 'Waidmannsheil',
'description': 'md5:cce00ca1d70e21425e72c86a98a56817',
'timestamp': 1410623100,
'upload_date': '20140913'
}, },
] 'params': {
'skip_download': True,
def _parse_smil_formats(self, smil, smil_url, video_id, namespace=None, f4m_params=None, transform_rtmp_url=None):
param_groups = {}
for param_group in smil.findall(self._xpath_ns('./head/paramGroup', namespace)):
group_id = param_group.get(self._xpath_ns(
'id', 'http://www.w3.org/XML/1998/namespace'))
params = {}
for param in param_group:
params[param.get('name')] = param.get('value')
param_groups[group_id] = params
formats = []
for video in smil.findall(self._xpath_ns('.//video', namespace)):
src = video.get('src')
if not src:
continue
bitrate = int_or_none(self._search_regex(r'_(\d+)k', src, 'bitrate', None)) or float_or_none(video.get('system-bitrate') or video.get('systemBitrate'), 1000)
group_id = video.get('paramGroup')
param_group = param_groups[group_id]
for proto in param_group['protocols'].split(','):
formats.append({
'url': '%s://%s' % (proto, param_group['host']),
'app': param_group['app'],
'play_path': src,
'ext': 'flv',
'format_id': '%s-%d' % (proto, bitrate),
'tbr': bitrate,
})
self._sort_formats(formats)
return formats
def extract_from_xml_url(self, video_id, xml_url):
doc = self._download_xml(
xml_url, video_id,
note='Downloading video info',
errnote='Failed to download video info')
status_code = xpath_text(doc, './status/statuscode')
if status_code and status_code != 'ok':
if status_code == 'notVisibleAnymore':
message = 'Video %s is not available' % video_id
else:
message = '%s returned error: %s' % (self.IE_NAME, status_code)
raise ExtractorError(message, expected=True)
title = xpath_text(doc, './/information/title', 'title', True)
urls = []
formats = []
for fnode in doc.findall('.//formitaeten/formitaet'):
video_url = xpath_text(fnode, 'url')
if not video_url or video_url in urls:
continue
urls.append(video_url)
is_available = 'http://www.metafilegenerator' not in video_url
geoloced = 'static_geoloced_online' in video_url
if not is_available or geoloced:
continue
format_id = fnode.attrib['basetype']
format_m = re.match(r'''(?x)
(?P<vcodec>[^_]+)_(?P<acodec>[^_]+)_(?P<container>[^_]+)_
(?P<proto>[^_]+)_(?P<index>[^_]+)_(?P<indexproto>[^_]+)
''', format_id)
ext = determine_ext(video_url, None) or format_m.group('container')
if ext == 'meta':
continue
elif ext == 'smil':
formats.extend(self._extract_smil_formats(
video_url, video_id, fatal=False))
elif ext == 'm3u8':
# the certificates are misconfigured (see
# https://github.com/ytdl-org/youtube-dl/issues/8665)
if video_url.startswith('https://'):
continue
formats.extend(self._extract_m3u8_formats(
video_url, video_id, 'mp4', 'm3u8_native',
m3u8_id=format_id, fatal=False))
elif ext == 'f4m':
formats.extend(self._extract_f4m_formats(
video_url, video_id, f4m_id=format_id, fatal=False))
else:
quality = xpath_text(fnode, './quality')
if quality:
format_id += '-' + quality
abr = int_or_none(xpath_text(fnode, './audioBitrate'), 1000)
vbr = int_or_none(xpath_text(fnode, './videoBitrate'), 1000)
tbr = int_or_none(self._search_regex(
r'_(\d+)k', video_url, 'bitrate', None))
if tbr and vbr and not abr:
abr = tbr - vbr
formats.append({
'format_id': format_id,
'url': video_url,
'ext': ext,
'acodec': format_m.group('acodec'),
'vcodec': format_m.group('vcodec'),
'abr': abr,
'vbr': vbr,
'tbr': tbr,
'width': int_or_none(xpath_text(fnode, './width')),
'height': int_or_none(xpath_text(fnode, './height')),
'filesize': int_or_none(xpath_text(fnode, './filesize')),
'protocol': format_m.group('proto').lower(),
})
geolocation = xpath_text(doc, './/details/geolocation')
if not formats and geolocation and geolocation != 'none':
self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
self._sort_formats(formats)
thumbnails = []
for node in doc.findall('.//teaserimages/teaserimage'):
thumbnail_url = node.text
if not thumbnail_url:
continue
thumbnail = {
'url': thumbnail_url,
}
thumbnail_key = node.get('key')
if thumbnail_key:
m = re.match('^([0-9]+)x([0-9]+)$', thumbnail_key)
if m:
thumbnail['width'] = int(m.group(1))
thumbnail['height'] = int(m.group(2))
thumbnails.append(thumbnail)
upload_date = unified_strdate(xpath_text(doc, './/details/airtime'))
return {
'id': video_id,
'title': title,
'description': xpath_text(doc, './/information/detail'),
'duration': int_or_none(xpath_text(doc, './/details/lengthSec')),
'thumbnails': thumbnails,
'uploader': xpath_text(doc, './/details/originChannelTitle'),
'uploader_id': xpath_text(doc, './/details/originChannelId'),
'upload_date': upload_date,
'formats': formats,
} }
}, {
def _real_extract(self, url): # Same as https://www.zdf.de/filme/filme-sonstige/der-hauptmann-112.html
video_id = self._match_id(url) 'url': 'https://www.3sat.de/film/spielfilm/der-hauptmann-100.html',
details_url = 'http://www.3sat.de/mediathek/xmlservice/web/beitragsDetails?id=%s' % video_id 'only_matching': True,
return self.extract_from_xml_url(video_id, details_url) }, {
# Same as https://www.zdf.de/wissen/nano/nano-21-mai-2019-102.html, equal media ids
'url': 'https://www.3sat.de/wissen/nano/nano-21-mai-2019-102.html',
'only_matching': True,
}]
+2
View File
@@ -103,6 +103,7 @@ from .awaan import (
) )
from .azmedien import AZMedienIE from .azmedien import AZMedienIE
from .baidu import BaiduVideoIE from .baidu import BaiduVideoIE
from .bandaichannel import BandaiChannelIE
from .bandcamp import BandcampIE, BandcampAlbumIE, BandcampWeeklyIE from .bandcamp import BandcampIE, BandcampAlbumIE, BandcampWeeklyIE
from .bbc import ( from .bbc import (
BBCCoUkIE, BBCCoUkIE,
@@ -737,6 +738,7 @@ from .mtv import (
) )
from .muenchentv import MuenchenTVIE from .muenchentv import MuenchenTVIE
from .mwave import MwaveIE, MwaveMeetGreetIE from .mwave import MwaveIE, MwaveMeetGreetIE
from .mxplayer import MxplayerIE
from .mychannels import MyChannelsIE from .mychannels import MyChannelsIE
from .myspace import MySpaceIE, MySpaceAlbumIE from .myspace import MySpaceIE, MySpaceAlbumIE
from .myspass import MySpassIE from .myspass import MySpassIE
+1 -1
View File
@@ -4,7 +4,7 @@ from __future__ import unicode_literals
import json import json
import re import re
from yt_dlp.utils import int_or_none, unified_timestamp, unescapeHTML from ..utils import int_or_none, unified_timestamp, unescapeHTML
from .common import InfoExtractor from .common import InfoExtractor
+7 -61
View File
@@ -7,7 +7,6 @@ from .common import InfoExtractor
from ..compat import ( from ..compat import (
compat_str, compat_str,
compat_xpath, compat_xpath,
compat_urlparse,
) )
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
@@ -23,7 +22,6 @@ from ..utils import (
unescapeHTML, unescapeHTML,
update_url_query, update_url_query,
url_basename, url_basename,
get_domain,
xpath_text, xpath_text,
) )
@@ -45,7 +43,7 @@ class MTVServicesInfoExtractor(InfoExtractor):
# Remove the templates, like &device={device} # Remove the templates, like &device={device}
return re.sub(r'&[^=]*?={.*?}(?=(&|$))', '', url) return re.sub(r'&[^=]*?={.*?}(?=(&|$))', '', url)
def _get_feed_url(self, uri, url=None): def _get_feed_url(self, uri):
return self._FEED_URL return self._FEED_URL
def _get_thumbnail_url(self, uri, itemdoc): def _get_thumbnail_url(self, uri, itemdoc):
@@ -211,9 +209,9 @@ class MTVServicesInfoExtractor(InfoExtractor):
data['lang'] = self._LANG data['lang'] = self._LANG
return data return data
def _get_videos_info(self, uri, use_hls=True, url=None): def _get_videos_info(self, uri, use_hls=True):
video_id = self._id_from_uri(uri) video_id = self._id_from_uri(uri)
feed_url = self._get_feed_url(uri, url) feed_url = self._get_feed_url(uri)
info_url = update_url_query(feed_url, self._get_feed_query(uri)) info_url = update_url_query(feed_url, self._get_feed_query(uri))
return self._get_videos_info_from_url(info_url, video_id, use_hls) return self._get_videos_info_from_url(info_url, video_id, use_hls)
@@ -259,41 +257,7 @@ class MTVServicesInfoExtractor(InfoExtractor):
def _extract_child_with_type(parent, t): def _extract_child_with_type(parent, t):
return next(c for c in parent['children'] if c.get('type') == t) return next(c for c in parent['children'] if c.get('type') == t)
def _extract_new_triforce_mgid(self, webpage, url='', video_id=None): def _extract_mgid(self, webpage):
if url == '':
return
domain = get_domain(url)
if domain is None:
raise ExtractorError(
'[%s] could not get domain' % self.IE_NAME,
expected=True)
url = url.replace("https://", "http://")
enc_url = compat_urlparse.quote(url, safe='')
_TRIFORCE_V8_TEMPLATE = 'https://%s/feeds/triforce/manifest/v8?url=%s'
triforce_manifest_url = _TRIFORCE_V8_TEMPLATE % (domain, enc_url)
manifest = self._download_json(triforce_manifest_url, video_id, fatal=False)
if manifest:
if manifest.get('manifest').get('type') == 'redirect':
self.to_screen('Found a redirect. Downloading manifest from new location')
new_loc = manifest.get('manifest').get('newLocation')
new_loc = new_loc.replace("https://", "http://")
enc_new_loc = compat_urlparse.quote(new_loc, safe='')
triforce_manifest_new_loc = _TRIFORCE_V8_TEMPLATE % (domain, enc_new_loc)
manifest = self._download_json(triforce_manifest_new_loc, video_id, fatal=False)
item_id = try_get(manifest, lambda x: x['manifest']['reporting']['itemId'], compat_str)
if not item_id:
self.to_screen('No id found!')
return
# 'episode' can be anything. 'content' is used often as well
_MGID_TEMPLATE = 'mgid:arc:episode:%s:%s'
mgid = _MGID_TEMPLATE % (domain, item_id)
return mgid
def _extract_mgid(self, webpage, url, title=None, data_zone=None):
try: try:
# the url can be http://media.mtvnservices.com/fb/{mgid}.swf # the url can be http://media.mtvnservices.com/fb/{mgid}.swf
# or http://media.mtvnservices.com/{mgid} # or http://media.mtvnservices.com/{mgid}
@@ -304,21 +268,6 @@ class MTVServicesInfoExtractor(InfoExtractor):
except RegexNotFoundError: except RegexNotFoundError:
mgid = None mgid = None
if not title:
title = url_basename(url)
try:
window_data = self._parse_json(self._search_regex(
r'(?s)window.__DATA__ = (?P<json>{.+});', webpage,
'JSON Window Data', default=None, fatal=False, group='json'), title, fatal=False)
main_container = None
for i in range(len(window_data['children'])):
if window_data['children'][i]['type'] == 'MainContainer':
main_container = window_data['children'][i]
mgid = main_container['children'][0]['props']['media']['video']['config']['uri']
except (KeyError, IndexError, TypeError):
pass
if mgid is None or ':' not in mgid: if mgid is None or ':' not in mgid:
mgid = self._search_regex( mgid = self._search_regex(
[r'data-mgid="(.*?)"', r'swfobject\.embedSWF\(".*?(mgid:.*?)"'], [r'data-mgid="(.*?)"', r'swfobject\.embedSWF\(".*?(mgid:.*?)"'],
@@ -331,10 +280,7 @@ class MTVServicesInfoExtractor(InfoExtractor):
r'embed/(mgid:.+?)["\'&?/]', sm4_embed, 'mgid', default=None) r'embed/(mgid:.+?)["\'&?/]', sm4_embed, 'mgid', default=None)
if not mgid: if not mgid:
mgid = self._extract_new_triforce_mgid(webpage, url) mgid = self._extract_triforce_mgid(webpage)
if not mgid:
mgid = self._extract_triforce_mgid(webpage, data_zone)
if not mgid: if not mgid:
data = self._parse_json(self._search_regex( data = self._parse_json(self._search_regex(
@@ -348,8 +294,8 @@ class MTVServicesInfoExtractor(InfoExtractor):
def _real_extract(self, url): def _real_extract(self, url):
title = url_basename(url) title = url_basename(url)
webpage = self._download_webpage(url, title) webpage = self._download_webpage(url, title)
mgid = self._extract_mgid(webpage, url, title=title) mgid = self._extract_mgid(webpage)
videos_info = self._get_videos_info(mgid, url=url) videos_info = self._get_videos_info(mgid)
return videos_info return videos_info
+103
View File
@@ -0,0 +1,103 @@
from __future__ import unicode_literals
import re
from .common import InfoExtractor
from ..utils import (
ExtractorError,
js_to_json,
url_or_none,
urljoin,
)
VALID_STREAMS = ('dash', )
class MxplayerIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?mxplayer\.in/movie/(?P<slug>[a-z0-9]+(?:-[a-z0-9]+)*)'
_TEST = {
'url': 'https://www.mxplayer.in/movie/watch-knock-knock-hindi-dubbed-movie-online-b9fa28df3bfb8758874735bbd7d2655a?watch=true',
'info_dict': {
'id': 'b9fa28df3bfb8758874735bbd7d2655a',
'ext': 'mp4',
'title': 'Knock Knock Movie | Watch 2015 Knock Knock Full Movie Online- MX Player',
'description': 'md5:b195ba93ff1987309cfa58e2839d2a5b'
},
'params': {
'skip_download': True,
'format': 'bestvideo'
}
}
def _get_best_stream_url(self, stream):
best_stream = list(filter(None, [v for k, v in stream.items()]))
return best_stream.pop(0) if len(best_stream) else None
def _get_stream_urls(self, video_dict):
stream_dict = video_dict.get('stream', {'provider': {}})
stream_provider = stream_dict.get('provider')
if not stream_dict[stream_provider]:
message = 'No stream provider found'
raise ExtractorError('%s said: %s' % (self.IE_NAME, message), expected=True)
streams = []
for stream_name, v in stream_dict[stream_provider].items():
if stream_name in VALID_STREAMS:
stream_url = self._get_best_stream_url(v)
if stream_url is None:
continue
streams.append((stream_name, stream_url))
return streams
def _real_extract(self, url):
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)
window_state_json = self._html_search_regex(
r'(?s)<script>window\.state\s*[:=]\s(\{.+\})\n(\w+).*(</script>).*',
webpage, 'WindowState')
source = self._parse_json(js_to_json(window_state_json), video_id)
if not source:
raise ExtractorError('Cannot find source', expected=True)
config_dict = source['config']
video_dict = source['entities'][video_id]
stream_urls = self._get_stream_urls(video_dict)
title = self._og_search_title(webpage, fatal=True, default=video_dict['title'])
formats = []
headers = {'Referer': url}
for stream_name, stream_url in stream_urls:
if stream_name == 'dash':
format_url = url_or_none(urljoin(config_dict['videoCdnBaseUrl'], stream_url))
if not format_url:
continue
formats.extend(self._extract_mpd_formats(
format_url, video_id, mpd_id='dash', headers=headers))
self._sort_formats(formats)
info = {
'id': video_id,
'title': title,
'description': video_dict.get('description'),
'formats': formats
}
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
+38 -25
View File
@@ -8,59 +8,66 @@ from ..utils import update_url_query
class NickIE(MTVServicesInfoExtractor): class NickIE(MTVServicesInfoExtractor):
# None of videos on the website are still alive?
IE_NAME = 'nick.com' IE_NAME = 'nick.com'
_VALID_URL = r'https?://(?P<domain>(?:(?:www|beta)\.)?nick(?:jr)?\.com)/(?:[^/]+/)?(?:videos/clip|[^/]+/videos)/(?P<id>[^/?#.]+)' _VALID_URL = r'https?://(?P<domain>(?:www\.)?nick(?:jr)?\.com)/(?:[^/]+/)?(?P<type>videos/clip|[^/]+/videos|episodes/[^/]+)/(?P<id>[^/?#.]+)'
_FEED_URL = 'http://udat.mtvnservices.com/service1/dispatch.htm' _FEED_URL = 'http://udat.mtvnservices.com/service1/dispatch.htm'
_GEO_COUNTRIES = ['US'] _GEO_COUNTRIES = ['US']
_TESTS = [{ _TESTS = [{
'url': 'http://www.nick.com/videos/clip/alvinnn-and-the-chipmunks-112-full-episode.html', 'url': 'https://www.nick.com/episodes/sq47rw/spongebob-squarepants-a-place-for-pets-lockdown-for-love-season-13-ep-1',
'info_dict': {
'description': 'md5:0650a9eb88955609d5c1d1c79292e234',
'title': 'A Place for Pets/Lockdown for Love',
},
'playlist': [ 'playlist': [
{ {
'md5': '6e5adc1e28253bbb1b28ab05403dd4d4', 'md5': 'cb8a2afeafb7ae154aca5a64815ec9d6',
'info_dict': { 'info_dict': {
'id': 'be6a17b0-412d-11e5-8ff7-0026b9414f30', 'id': '85ee8177-d6ce-48f8-9eee-a65364f8a6df',
'ext': 'mp4', 'ext': 'mp4',
'title': 'ALVINNN!!! and The Chipmunks: "Mojo Missing/Who\'s The Animal" S1', 'title': 'SpongeBob SquarePants: "A Place for Pets/Lockdown for Love" S1',
'description': 'Alvin is convinced his mojo was in a cap he gave to a fan, and must find a way to get his hat back before the Chipmunks big concert.\nDuring a costume visit to the zoo, Alvin finds himself mistaken for the real Tasmanian devil.', 'description': 'A Place for Pets/Lockdown for Love: When customers bring pets into the Krusty Krab, Mr. Krabs realizes pets are more profitable than owners. Plankton ruins another date with Karen, so she puts the Chum Bucket on lockdown until he proves his affection.',
} }
}, },
{ {
'md5': 'd7be441fc53a1d4882fa9508a1e5b3ce', 'md5': '839a04f49900a1fcbf517020d94e0737',
'info_dict': { 'info_dict': {
'id': 'be6b8f96-412d-11e5-8ff7-0026b9414f30', 'id': '2e2a9960-8fd4-411d-868b-28eb1beb7fae',
'ext': 'mp4', 'ext': 'mp4',
'title': 'ALVINNN!!! and The Chipmunks: "Mojo Missing/Who\'s The Animal" S2', 'title': 'SpongeBob SquarePants: "A Place for Pets/Lockdown for Love" S2',
'description': 'Alvin is convinced his mojo was in a cap he gave to a fan, and must find a way to get his hat back before the Chipmunks big concert.\nDuring a costume visit to the zoo, Alvin finds himself mistaken for the real Tasmanian devil.', 'description': 'A Place for Pets/Lockdown for Love: When customers bring pets into the Krusty Krab, Mr. Krabs realizes pets are more profitable than owners. Plankton ruins another date with Karen, so she puts the Chum Bucket on lockdown until he proves his affection.',
} }
}, },
{ {
'md5': 'efffe1728a234b2b0d2f2b343dd1946f', 'md5': 'f1145699f199770e2919ee8646955d46',
'info_dict': { 'info_dict': {
'id': 'be6cf7e6-412d-11e5-8ff7-0026b9414f30', 'id': 'dc91c304-6876-40f7-84a6-7aece7baa9d0',
'ext': 'mp4', 'ext': 'mp4',
'title': 'ALVINNN!!! and The Chipmunks: "Mojo Missing/Who\'s The Animal" S3', 'title': 'SpongeBob SquarePants: "A Place for Pets/Lockdown for Love" S3',
'description': 'Alvin is convinced his mojo was in a cap he gave to a fan, and must find a way to get his hat back before the Chipmunks big concert.\nDuring a costume visit to the zoo, Alvin finds himself mistaken for the real Tasmanian devil.', 'description': 'A Place for Pets/Lockdown for Love: When customers bring pets into the Krusty Krab, Mr. Krabs realizes pets are more profitable than owners. Plankton ruins another date with Karen, so she puts the Chum Bucket on lockdown until he proves his affection.',
} }
}, },
{ {
'md5': '1ec6690733ab9f41709e274a1d5c7556', 'md5': 'd463116875aee2585ee58de3b12caebd',
'info_dict': { 'info_dict': {
'id': 'be6e3354-412d-11e5-8ff7-0026b9414f30', 'id': '5d929486-cf4c-42a1-889a-6e0d183a101a',
'ext': 'mp4', 'ext': 'mp4',
'title': 'ALVINNN!!! and The Chipmunks: "Mojo Missing/Who\'s The Animal" S4', 'title': 'SpongeBob SquarePants: "A Place for Pets/Lockdown for Love" S4',
'description': 'Alvin is convinced his mojo was in a cap he gave to a fan, and must find a way to get his hat back before the Chipmunks big concert.\nDuring a costume visit to the zoo, Alvin finds himself mistaken for the real Tasmanian devil.', 'description': 'A Place for Pets/Lockdown for Love: When customers bring pets into the Krusty Krab, Mr. Krabs realizes pets are more profitable than owners. Plankton ruins another date with Karen, so she puts the Chum Bucket on lockdown until he proves his affection.',
} }
}, },
], ],
}, { }, {
'url': 'http://www.nickjr.com/paw-patrol/videos/pups-save-a-goldrush-s3-ep302-full-episode/', 'url': 'http://www.nickjr.com/blues-clues-and-you/videos/blues-clues-and-you-original-209-imagination-station/',
'only_matching': True, 'info_dict': {
}, { 'id': '31631529-2fc5-430b-b2ef-6a74b4609abd',
'url': 'http://beta.nick.com/nicky-ricky-dicky-and-dawn/videos/nicky-ricky-dicky-dawn-301-full-episode/', 'ext': 'mp4',
'only_matching': True, 'description': 'md5:9d65a66df38e02254852794b2809d1cf',
'title': 'Blue\'s Imagination Station',
},
}] }]
def _get_feed_query(self, uri): def _get_feed_query(self, uri):
@@ -69,8 +76,14 @@ class NickIE(MTVServicesInfoExtractor):
'mgid': uri, 'mgid': uri,
} }
def _extract_mgid(self, webpage):
mgid = self._search_regex(r'"media":{"video":{"config":{"uri":"(mgid:.*?)"', webpage, 'mgid', default=None)
return mgid
def _real_extract(self, url): def _real_extract(self, url):
domain, display_id = re.match(self._VALID_URL, url).groups() domain, video_type, display_id = re.match(self._VALID_URL, url).groups()
if video_type.startswith("episodes"):
return super()._real_extract(url)
video_data = self._download_json( video_data = self._download_json(
'http://%s/data/video.endLevel.json' % domain, 'http://%s/data/video.endLevel.json' % domain,
display_id, query={ display_id, query={
+1 -3
View File
@@ -23,11 +23,9 @@ class NineCNineMediaIE(InfoExtractor):
destination_code, content_id = re.match(self._VALID_URL, url).groups() destination_code, content_id = re.match(self._VALID_URL, url).groups()
api_base_url = self._API_BASE_TEMPLATE % (destination_code, content_id) api_base_url = self._API_BASE_TEMPLATE % (destination_code, content_id)
content = self._download_json(api_base_url, content_id, query={ content = self._download_json(api_base_url, content_id, query={
'$include': '[Media,Season,ContentPackages]', '$include': '[Media.Name,Season,ContentPackages.Duration,ContentPackages.Id]',
}) })
title = content['Name'] title = content['Name']
if len(content['ContentPackages']) > 1:
raise ExtractorError('multiple content packages')
content_package = content['ContentPackages'][0] content_package = content['ContentPackages'][0]
package_id = content_package['Id'] package_id = content_package['Id']
content_package_url = api_base_url + 'contentpackages/%s/' % package_id content_package_url = api_base_url + 'contentpackages/%s/' % package_id
+119 -43
View File
@@ -1,52 +1,128 @@
# coding: utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
from .common import InfoExtractor import re
from ..utils import ExtractorError
from .youtube import YoutubeIE
from .zdf import ZDFBaseIE
from ..compat import compat_str
from ..utils import (
int_or_none,
merge_dicts,
unified_timestamp,
xpath_text,
)
class PhoenixIE(InfoExtractor): class PhoenixIE(ZDFBaseIE):
IE_NAME = 'phoenix.de' IE_NAME = 'phoenix.de'
_VALID_URL = r'''https?://(?:www\.)?phoenix.de/\D+(?P<id>\d+)\.html''' _VALID_URL = r'https?://(?:www\.)?phoenix\.de/(?:[^/]+/)*[^/?#&]*-a-(?P<id>\d+)\.html'
_TESTS = [ _TESTS = [{
{ # Same as https://www.zdf.de/politik/phoenix-sendungen/wohin-fuehrt-der-protest-in-der-pandemie-100.html
'url': 'https://www.phoenix.de/sendungen/dokumentationen/unsere-welt-in-zukunft---stadt-a-1283620.html', 'url': 'https://www.phoenix.de/sendungen/ereignisse/corona-nachgehakt/wohin-fuehrt-der-protest-in-der-pandemie-a-2050630.html',
'md5': '5e765e838aa3531c745a4f5b249ee3e3', 'md5': '34ec321e7eb34231fd88616c65c92db0',
'info_dict': { 'info_dict': {
'id': '0OB4HFc43Ns', 'id': '210222_phx_nachgehakt_corona_protest',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Unsere Welt in Zukunft - Stadt', 'title': 'Wohin führt der Protest in der Pandemie?',
'description': 'md5:9bfb6fd498814538f953b2dcad7ce044', 'description': 'md5:7d643fe7f565e53a24aac036b2122fbd',
'upload_date': '20190912', 'duration': 1691,
'uploader': 'phoenix', 'timestamp': 1613906100,
'uploader_id': 'phoenix', 'upload_date': '20210221',
} 'uploader': 'Phoenix',
'channel': 'corona nachgehakt',
}, },
{ }, {
'url': 'https://www.phoenix.de/drohnenangriffe-in-saudi-arabien-a-1286995.html?ref=aktuelles', # Youtube embed
'only_matching': True, 'url': 'https://www.phoenix.de/sendungen/gespraeche/phoenix-streitgut-brennglas-corona-a-1965505.html',
'info_dict': {
'id': 'hMQtqFYjomk',
'ext': 'mp4',
'title': 'phoenix streitgut: Brennglas Corona - Wie gerecht ist unsere Gesellschaft?',
'description': 'md5:ac7a02e2eb3cb17600bc372e4ab28fdd',
'duration': 3509,
'upload_date': '20201219',
'uploader': 'phoenix',
'uploader_id': 'phoenix',
}, },
# an older page: https://www.phoenix.de/sendungen/gespraeche/phoenix-persoenlich/im-dialog-a-177727.html 'params': {
# seems to not have an embedded video, even though it's uploaded on youtube: https://www.youtube.com/watch?v=4GxnoUHvOkM 'skip_download': True,
] },
}, {
def extract_from_json_api(self, video_id, api_url): 'url': 'https://www.phoenix.de/entwicklungen-in-russland-a-2044720.html',
doc = self._download_json( 'only_matching': True,
api_url, video_id, }, {
note="Downloading webpage metadata", # no media
errnote="Failed to load webpage metadata") 'url': 'https://www.phoenix.de/sendungen/dokumentationen/mit-dem-jumbo-durch-die-nacht-a-89625.html',
'only_matching': True,
for a in doc["absaetze"]: }, {
if a["typ"] == "video-youtube": # Same as https://www.zdf.de/politik/phoenix-sendungen/die-gesten-der-maechtigen-100.html
return { 'url': 'https://www.phoenix.de/sendungen/dokumentationen/gesten-der-maechtigen-i-a-89468.html?ref=suche',
'_type': 'url_transparent', 'only_matching': True,
'id': a["id"], }]
'title': doc["titel"],
'url': "https://www.youtube.com/watch?v=%s" % a["id"],
'ie_key': 'Youtube',
}
raise ExtractorError("No downloadable video found", expected=True)
def _real_extract(self, url): def _real_extract(self, url):
page_id = self._match_id(url) article_id = self._match_id(url)
api_url = 'https://www.phoenix.de/response/id/%s' % page_id
return self.extract_from_json_api(page_id, api_url) article = self._download_json(
'https://www.phoenix.de/response/id/%s' % article_id, article_id,
'Downloading article JSON')
video = article['absaetze'][0]
title = video.get('titel') or article.get('subtitel')
if video.get('typ') == 'video-youtube':
video_id = video['id']
return self.url_result(
video_id, ie=YoutubeIE.ie_key(), video_id=video_id,
video_title=title)
video_id = compat_str(video.get('basename') or video.get('content'))
details = self._download_xml(
'https://www.phoenix.de/php/mediaplayer/data/beitrags_details.php',
video_id, 'Downloading details XML', query={
'ak': 'web',
'ptmd': 'true',
'id': video_id,
'profile': 'player2',
})
title = title or xpath_text(
details, './/information/title', 'title', fatal=True)
content_id = xpath_text(
details, './/video/details/basename', 'content id', fatal=True)
info = self._extract_ptmd(
'https://tmd.phoenix.de/tmd/2/ngplayer_2_3/vod/ptmd/phoenix/%s' % content_id,
content_id, None, url)
timestamp = unified_timestamp(xpath_text(details, './/details/airtime'))
thumbnails = []
for node in details.findall('.//teaserimages/teaserimage'):
thumbnail_url = node.text
if not thumbnail_url:
continue
thumbnail = {
'url': thumbnail_url,
}
thumbnail_key = node.get('key')
if thumbnail_key:
m = re.match('^([0-9]+)x([0-9]+)$', thumbnail_key)
if m:
thumbnail['width'] = int(m.group(1))
thumbnail['height'] = int(m.group(2))
thumbnails.append(thumbnail)
return merge_dicts(info, {
'id': content_id,
'title': title,
'description': xpath_text(details, './/information/detail'),
'duration': int_or_none(xpath_text(details, './/details/lengthSec')),
'thumbnails': thumbnails,
'timestamp': timestamp,
'uploader': xpath_text(details, './/details/channel'),
'uploader_id': xpath_text(details, './/details/originChannelId'),
'channel': xpath_text(details, './/details/originChannelTitle'),
})
+9 -9
View File
@@ -15,17 +15,17 @@ class RDSIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?rds\.ca/vid(?:[eé]|%C3%A9)os/(?:[^/]+/)*(?P<id>[^/]+)-\d+\.\d+' _VALID_URL = r'https?://(?:www\.)?rds\.ca/vid(?:[eé]|%C3%A9)os/(?:[^/]+/)*(?P<id>[^/]+)-\d+\.\d+'
_TESTS = [{ _TESTS = [{
'url': 'http://www.rds.ca/videos/football/nfl/fowler-jr-prend-la-direction-de-jacksonville-3.1132799', # has two 9c9media ContentPackages, the web player selects the first ContentPackage
'url': 'https://www.rds.ca/videos/Hockey/NationalHockeyLeague/teams/9/forum-du-5-a-7-jesperi-kotkaniemi-de-retour-de-finlande-3.1377606',
'info_dict': { 'info_dict': {
'id': '604333', 'id': '2083309',
'display_id': 'fowler-jr-prend-la-direction-de-jacksonville', 'display_id': 'forum-du-5-a-7-jesperi-kotkaniemi-de-retour-de-finlande',
'ext': 'flv', 'ext': 'flv',
'title': 'Fowler Jr. prend la direction de Jacksonville', 'title': 'Forum du 5 à 7 : Kotkaniemi de retour de Finlande',
'description': 'Dante Fowler Jr. est le troisième choix du repêchage 2015 de la NFL. ', 'description': 'md5:83fa38ecc4a79b19e433433254077f25',
'timestamp': 1430397346, 'timestamp': 1606129030,
'upload_date': '20150430', 'upload_date': '20201123',
'duration': 154.354, 'duration': 773.039,
'age_limit': 0,
} }
}, { }, {
'url': 'http://www.rds.ca/vid%C3%A9os/un-voyage-positif-3.877934', 'url': 'http://www.rds.ca/vid%C3%A9os/un-voyage-positif-3.877934',
+10 -5
View File
@@ -6,11 +6,12 @@ import re
from .srgssr import SRGSSRIE from .srgssr import SRGSSRIE
from ..compat import compat_str from ..compat import compat_str
from ..utils import ( from ..utils import (
determine_ext,
int_or_none, int_or_none,
parse_duration, parse_duration,
parse_iso8601, parse_iso8601,
unescapeHTML, unescapeHTML,
determine_ext, urljoin,
) )
@@ -21,7 +22,7 @@ class RTSIE(SRGSSRIE):
_TESTS = [ _TESTS = [
{ {
'url': 'http://www.rts.ch/archives/tv/divers/3449373-les-enfants-terribles.html', 'url': 'http://www.rts.ch/archives/tv/divers/3449373-les-enfants-terribles.html',
'md5': 'ff7f8450a90cf58dacb64e29707b4a8e', 'md5': '753b877968ad8afaeddccc374d4256a5',
'info_dict': { 'info_dict': {
'id': '3449373', 'id': '3449373',
'display_id': 'les-enfants-terribles', 'display_id': 'les-enfants-terribles',
@@ -35,6 +36,7 @@ class RTSIE(SRGSSRIE):
'thumbnail': r're:^https?://.*\.image', 'thumbnail': r're:^https?://.*\.image',
'view_count': int, 'view_count': int,
}, },
'expected_warnings': ['Unable to download f4m manifest', 'Failed to download m3u8 information'],
}, },
{ {
'url': 'http://www.rts.ch/emissions/passe-moi-les-jumelles/5624067-entre-ciel-et-mer.html', 'url': 'http://www.rts.ch/emissions/passe-moi-les-jumelles/5624067-entre-ciel-et-mer.html',
@@ -63,11 +65,12 @@ class RTSIE(SRGSSRIE):
# m3u8 download # m3u8 download
'skip_download': True, 'skip_download': True,
}, },
'expected_warnings': ['Unable to download f4m manifest', 'Failed to download m3u8 information'],
'skip': 'Blocked outside Switzerland', 'skip': 'Blocked outside Switzerland',
}, },
{ {
'url': 'http://www.rts.ch/video/info/journal-continu/5745356-londres-cachee-par-un-epais-smog.html', 'url': 'http://www.rts.ch/video/info/journal-continu/5745356-londres-cachee-par-un-epais-smog.html',
'md5': '1bae984fe7b1f78e94abc74e802ed99f', 'md5': '9bb06503773c07ce83d3cbd793cebb91',
'info_dict': { 'info_dict': {
'id': '5745356', 'id': '5745356',
'display_id': 'londres-cachee-par-un-epais-smog', 'display_id': 'londres-cachee-par-un-epais-smog',
@@ -81,6 +84,7 @@ class RTSIE(SRGSSRIE):
'thumbnail': r're:^https?://.*\.image', 'thumbnail': r're:^https?://.*\.image',
'view_count': int, 'view_count': int,
}, },
'expected_warnings': ['Unable to download f4m manifest', 'Failed to download m3u8 information'],
}, },
{ {
'url': 'http://www.rts.ch/audio/couleur3/programmes/la-belle-video-de-stephane-laurenceau/5706148-urban-hippie-de-damien-krisl-03-04-2014.html', 'url': 'http://www.rts.ch/audio/couleur3/programmes/la-belle-video-de-stephane-laurenceau/5706148-urban-hippie-de-damien-krisl-03-04-2014.html',
@@ -160,7 +164,7 @@ class RTSIE(SRGSSRIE):
media_type = 'video' if 'video' in all_info else 'audio' media_type = 'video' if 'video' in all_info else 'audio'
# check for errors # check for errors
self.get_media_data('rts', media_type, media_id) self._get_media_data('rts', media_type, media_id)
info = all_info['video']['JSONinfo'] if 'video' in all_info else all_info['audio'] info = all_info['video']['JSONinfo'] if 'video' in all_info else all_info['audio']
@@ -194,6 +198,7 @@ class RTSIE(SRGSSRIE):
'tbr': extract_bitrate(format_url), 'tbr': extract_bitrate(format_url),
}) })
download_base = 'http://rtsww%s-d.rts.ch/' % ('-a' if media_type == 'audio' else '')
for media in info.get('media', []): for media in info.get('media', []):
media_url = media.get('url') media_url = media.get('url')
if not media_url or re.match(r'https?://', media_url): if not media_url or re.match(r'https?://', media_url):
@@ -205,7 +210,7 @@ class RTSIE(SRGSSRIE):
format_id += '-%dk' % rate format_id += '-%dk' % rate
formats.append({ formats.append({
'format_id': format_id, 'format_id': format_id,
'url': 'http://download-video.rts.ch/' + media_url, 'url': urljoin(download_base, media_url),
'tbr': rate or extract_bitrate(media_url), 'tbr': rate or extract_bitrate(media_url),
}) })
+133 -73
View File
@@ -4,16 +4,32 @@ from __future__ import unicode_literals
import re import re
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import compat_urllib_parse_urlparse
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
float_or_none,
int_or_none,
parse_iso8601, parse_iso8601,
qualities, qualities,
try_get,
) )
class SRGSSRIE(InfoExtractor): class SRGSSRIE(InfoExtractor):
_VALID_URL = r'(?:https?://tp\.srgssr\.ch/p(?:/[^/]+)+\?urn=urn|srgssr):(?P<bu>srf|rts|rsi|rtr|swi):(?:[^:]+:)?(?P<type>video|audio):(?P<id>[0-9a-f\-]{36}|\d+)' _VALID_URL = r'''(?x)
(?:
https?://tp\.srgssr\.ch/p(?:/[^/]+)+\?urn=urn|
srgssr
):
(?P<bu>
srf|rts|rsi|rtr|swi
):(?:[^:]+:)?
(?P<type>
video|audio
):
(?P<id>
[0-9a-f\-]{36}|\d+
)
'''
_GEO_BYPASS = False _GEO_BYPASS = False
_GEO_COUNTRIES = ['CH'] _GEO_COUNTRIES = ['CH']
@@ -25,25 +41,39 @@ class SRGSSRIE(InfoExtractor):
'LEGAL': 'The video cannot be transmitted for legal reasons.', 'LEGAL': 'The video cannot be transmitted for legal reasons.',
'STARTDATE': 'This video is not yet available. Please try again later.', 'STARTDATE': 'This video is not yet available. Please try again later.',
} }
_DEFAULT_LANGUAGE_CODES = {
'srf': 'de',
'rts': 'fr',
'rsi': 'it',
'rtr': 'rm',
'swi': 'en',
}
def _get_tokenized_src(self, url, video_id, format_id): def _get_tokenized_src(self, url, video_id, format_id):
sp = compat_urllib_parse_urlparse(url).path.split('/')
token = self._download_json( token = self._download_json(
'http://tp.srgssr.ch/akahd/token?acl=/%s/%s/*' % (sp[1], sp[2]), 'http://tp.srgssr.ch/akahd/token?acl=*',
video_id, 'Downloading %s token' % format_id, fatal=False) or {} video_id, 'Downloading %s token' % format_id, fatal=False) or {}
auth_params = token.get('token', {}).get('authparams') auth_params = try_get(token, lambda x: x['token']['authparams'])
if auth_params: if auth_params:
url += '?' + auth_params url += ('?' if '?' not in url else '&') + auth_params
return url return url
def get_media_data(self, bu, media_type, media_id): def _get_media_data(self, bu, media_type, media_id):
media_data = self._download_json( query = {'onlyChapters': True} if media_type == 'video' else {}
'http://il.srgssr.ch/integrationlayer/1.0/ue/%s/%s/play/%s.json' % (bu, media_type, media_id), full_media_data = self._download_json(
media_id)[media_type.capitalize()] 'https://il.srgssr.ch/integrationlayer/2.0/%s/mediaComposition/%s/%s.json'
% (bu, media_type, media_id),
media_id, query=query)['chapterList']
try:
media_data = next(
x for x in full_media_data if x.get('id') == media_id)
except StopIteration:
raise ExtractorError('No media information found')
if media_data.get('block') and media_data['block'] in self._ERRORS: block_reason = media_data.get('blockReason')
message = self._ERRORS[media_data['block']] if block_reason and block_reason in self._ERRORS:
if media_data['block'] == 'GEOBLOCK': message = self._ERRORS[block_reason]
if block_reason == 'GEOBLOCK':
self.raise_geo_restricted( self.raise_geo_restricted(
msg=message, countries=self._GEO_COUNTRIES) msg=message, countries=self._GEO_COUNTRIES)
raise ExtractorError( raise ExtractorError(
@@ -53,53 +83,75 @@ class SRGSSRIE(InfoExtractor):
def _real_extract(self, url): def _real_extract(self, url):
bu, media_type, media_id = re.match(self._VALID_URL, url).groups() bu, media_type, media_id = re.match(self._VALID_URL, url).groups()
media_data = self._get_media_data(bu, media_type, media_id)
title = media_data['title']
media_data = self.get_media_data(bu, media_type, media_id)
metadata = media_data['AssetMetadatas']['AssetMetadata'][0]
title = metadata['title']
description = metadata.get('description')
created_date = media_data.get('createdDate') or metadata.get('createdDate')
timestamp = parse_iso8601(created_date)
thumbnails = [{
'id': image.get('id'),
'url': image['url'],
} for image in media_data.get('Image', {}).get('ImageRepresentations', {}).get('ImageRepresentation', [])]
preference = qualities(['LQ', 'MQ', 'SD', 'HQ', 'HD'])
formats = [] formats = []
for source in media_data.get('Playlists', {}).get('Playlist', []) + media_data.get('Downloads', {}).get('Download', []): q = qualities(['SD', 'HD'])
protocol = source.get('@protocol') for source in (media_data.get('resourceList') or []):
for asset in source['url']: format_url = source.get('url')
asset_url = asset['text'] if not format_url:
quality = asset['@quality'] continue
format_id = '%s-%s' % (protocol, quality) protocol = source.get('protocol')
if protocol.startswith('HTTP-HDS') or protocol.startswith('HTTP-HLS'): quality = source.get('quality')
asset_url = self._get_tokenized_src(asset_url, media_id, format_id) format_id = []
if protocol.startswith('HTTP-HDS'): for e in (protocol, source.get('encoding'), quality):
formats.extend(self._extract_f4m_formats( if e:
asset_url + ('?' if '?' not in asset_url else '&') + 'hdcore=3.4.0', format_id.append(e)
media_id, f4m_id=format_id, fatal=False)) format_id = '-'.join(format_id)
elif protocol.startswith('HTTP-HLS'):
formats.extend(self._extract_m3u8_formats( if protocol in ('HDS', 'HLS'):
asset_url, media_id, 'mp4', 'm3u8_native', if source.get('tokenType') == 'AKAMAI':
m3u8_id=format_id, fatal=False)) format_url = self._get_tokenized_src(
else: format_url, media_id, format_id)
formats.append({ formats.extend(self._extract_akamai_formats(
'format_id': format_id, format_url, media_id))
'url': asset_url, elif protocol == 'HLS':
'quality': preference(quality), formats.extend(self._extract_m3u8_formats(
'ext': 'flv' if protocol == 'RTMP' else None, format_url, media_id, 'mp4', 'm3u8_native',
}) m3u8_id=format_id, fatal=False))
elif protocol in ('HTTP', 'HTTPS'):
formats.append({
'format_id': format_id,
'url': format_url,
'quality': q(quality),
})
# This is needed because for audio medias the podcast url is usually
# always included, even if is only an audio segment and not the
# whole episode.
if int_or_none(media_data.get('position')) == 0:
for p in ('S', 'H'):
podcast_url = media_data.get('podcast%sdUrl' % p)
if not podcast_url:
continue
quality = p + 'D'
formats.append({
'format_id': 'PODCAST-' + quality,
'url': podcast_url,
'quality': q(quality),
})
self._sort_formats(formats) self._sort_formats(formats)
subtitles = {}
if media_type == 'video':
for sub in (media_data.get('subtitleList') or []):
sub_url = sub.get('url')
if not sub_url:
continue
lang = sub.get('locale') or self._DEFAULT_LANGUAGE_CODES[bu]
subtitles.setdefault(lang, []).append({
'url': sub_url,
})
return { return {
'id': media_id, 'id': media_id,
'title': title, 'title': title,
'description': description, 'description': media_data.get('description'),
'timestamp': timestamp, 'timestamp': parse_iso8601(media_data.get('date')),
'thumbnails': thumbnails, 'thumbnail': media_data.get('imageUrl'),
'duration': float_or_none(media_data.get('duration'), 1000),
'subtitles': subtitles,
'formats': formats, 'formats': formats,
} }
@@ -119,26 +171,17 @@ class SRGSSRPlayIE(InfoExtractor):
_TESTS = [{ _TESTS = [{
'url': 'http://www.srf.ch/play/tv/10vor10/video/snowden-beantragt-asyl-in-russland?id=28e1a57d-5b76-4399-8ab3-9097f071e6c5', 'url': 'http://www.srf.ch/play/tv/10vor10/video/snowden-beantragt-asyl-in-russland?id=28e1a57d-5b76-4399-8ab3-9097f071e6c5',
'md5': 'da6b5b3ac9fa4761a942331cef20fcb3', 'md5': '6db2226ba97f62ad42ce09783680046c',
'info_dict': { 'info_dict': {
'id': '28e1a57d-5b76-4399-8ab3-9097f071e6c5', 'id': '28e1a57d-5b76-4399-8ab3-9097f071e6c5',
'ext': 'mp4', 'ext': 'mp4',
'upload_date': '20130701', 'upload_date': '20130701',
'title': 'Snowden beantragt Asyl in Russland', 'title': 'Snowden beantragt Asyl in Russland',
'timestamp': 1372713995, 'timestamp': 1372708215,
} 'duration': 113.827,
}, { 'thumbnail': r're:^https?://.*1383719781\.png$',
# No Speichern (Save) button
'url': 'http://www.srf.ch/play/tv/top-gear/video/jaguar-xk120-shadow-und-tornado-dampflokomotive?id=677f5829-e473-4823-ac83-a1087fe97faa',
'md5': '0a274ce38fda48c53c01890651985bc6',
'info_dict': {
'id': '677f5829-e473-4823-ac83-a1087fe97faa',
'ext': 'flv',
'upload_date': '20130710',
'title': 'Jaguar XK120, Shadow und Tornado-Dampflokomotive',
'description': 'md5:88604432b60d5a38787f152dec89cd56',
'timestamp': 1373493600,
}, },
'expected_warnings': ['Unable to download f4m manifest'],
}, { }, {
'url': 'http://www.rtr.ch/play/radio/actualitad/audio/saira-tujetsch-tuttina-cuntinuar-cun-sedrun-muster-turissem?id=63cb0778-27f8-49af-9284-8c7a8c6d15fc', 'url': 'http://www.rtr.ch/play/radio/actualitad/audio/saira-tujetsch-tuttina-cuntinuar-cun-sedrun-muster-turissem?id=63cb0778-27f8-49af-9284-8c7a8c6d15fc',
'info_dict': { 'info_dict': {
@@ -146,7 +189,8 @@ class SRGSSRPlayIE(InfoExtractor):
'ext': 'mp3', 'ext': 'mp3',
'upload_date': '20151013', 'upload_date': '20151013',
'title': 'Saira: Tujetsch - tuttina cuntinuar cun Sedrun Mustér Turissem', 'title': 'Saira: Tujetsch - tuttina cuntinuar cun Sedrun Mustér Turissem',
'timestamp': 1444750398, 'timestamp': 1444709160,
'duration': 336.816,
}, },
'params': { 'params': {
# rtmp download # rtmp download
@@ -159,19 +203,32 @@ class SRGSSRPlayIE(InfoExtractor):
'id': '6348260', 'id': '6348260',
'display_id': '6348260', 'display_id': '6348260',
'ext': 'mp4', 'ext': 'mp4',
'duration': 1796, 'duration': 1796.76,
'title': 'Le 19h30', 'title': 'Le 19h30',
'description': '',
'uploader': '19h30',
'upload_date': '20141201', 'upload_date': '20141201',
'timestamp': 1417458600, 'timestamp': 1417458600,
'thumbnail': r're:^https?://.*\.image', 'thumbnail': r're:^https?://.*\.image',
'view_count': int,
}, },
'params': { 'params': {
# m3u8 download # m3u8 download
'skip_download': True, 'skip_download': True,
} }
}, {
'url': 'http://play.swissinfo.ch/play/tv/business/video/why-people-were-against-tax-reforms?id=42960270',
'info_dict': {
'id': '42960270',
'ext': 'mp4',
'title': 'Why people were against tax reforms',
'description': 'md5:7ac442c558e9630e947427469c4b824d',
'duration': 94.0,
'upload_date': '20170215',
'timestamp': 1487173560,
'thumbnail': r're:https?://www\.swissinfo\.ch/srgscalableimage/42961964',
'subtitles': 'count:9',
},
'params': {
'skip_download': True,
}
}, { }, {
'url': 'https://www.srf.ch/play/tv/popupvideoplayer?id=c4dba0ca-e75b-43b2-a34f-f708a4932e01', 'url': 'https://www.srf.ch/play/tv/popupvideoplayer?id=c4dba0ca-e75b-43b2-a34f-f708a4932e01',
'only_matching': True, 'only_matching': True,
@@ -181,6 +238,10 @@ class SRGSSRPlayIE(InfoExtractor):
}, { }, {
'url': 'https://www.rts.ch/play/tv/19h30/video/le-19h30?urn=urn:rts:video:6348260', 'url': 'https://www.rts.ch/play/tv/19h30/video/le-19h30?urn=urn:rts:video:6348260',
'only_matching': True, 'only_matching': True,
}, {
# audio segment, has podcastSdUrl of the full episode
'url': 'https://www.srf.ch/play/radio/popupaudioplayer?id=50b20dc8-f05b-4972-bf03-e438ff2833eb',
'only_matching': True,
}] }]
def _real_extract(self, url): def _real_extract(self, url):
@@ -188,5 +249,4 @@ class SRGSSRPlayIE(InfoExtractor):
bu = mobj.group('bu') bu = mobj.group('bu')
media_type = mobj.group('type') or mobj.group('type_2') media_type = mobj.group('type') or mobj.group('type_2')
media_id = mobj.group('id') media_id = mobj.group('id')
# other info can be extracted from url + '&layout=json'
return self.url_result('srgssr:%s:%s:%s' % (bu[:3], media_type, media_id), 'SRGSSR') return self.url_result('srgssr:%s:%s:%s' % (bu[:3], media_type, media_id), 'SRGSSR')
+13 -8
View File
@@ -1,7 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import int_or_none
class StretchInternetIE(InfoExtractor): class StretchInternetIE(InfoExtractor):
@@ -11,22 +10,28 @@ class StretchInternetIE(InfoExtractor):
'info_dict': { 'info_dict': {
'id': '573272', 'id': '573272',
'ext': 'mp4', 'ext': 'mp4',
'title': 'University of Mary Wrestling vs. Upper Iowa', 'title': 'UNIVERSITY OF MARY WRESTLING VS UPPER IOWA',
'timestamp': 1575668361, # 'timestamp': 1575668361,
'upload_date': '20191206', # 'upload_date': '20191206',
'uploader_id': '99997',
} }
} }
def _real_extract(self, url): def _real_extract(self, url):
video_id = self._match_id(url) video_id = self._match_id(url)
media_url = self._download_json(
'https://core.stretchlive.com/trinity/event/tcg/' + video_id,
video_id)[0]['media'][0]['url']
event = self._download_json( event = self._download_json(
'https://api.stretchinternet.com/trinity/event/tcg/' + video_id, 'https://neo-client.stretchinternet.com/portal-ws/getEvent.json',
video_id)[0] video_id, query={'eventID': video_id, 'token': 'asdf'})['event']
return { return {
'id': video_id, 'id': video_id,
'title': event['title'], 'title': event['title'],
'timestamp': int_or_none(event.get('dateCreated'), 1000), # TODO: parse US timezone abbreviations
'url': 'https://' + event['media'][0]['url'], # 'timestamp': event.get('dateTimeString'),
'url': 'https://' + media_url,
'uploader_id': event.get('ownerID'),
} }
+1
View File
@@ -86,6 +86,7 @@ class TennisTVIE(InfoExtractor):
'https://www.tennistv.com/api/users/v1/entitlementchecknondiva', 'https://www.tennistv.com/api/users/v1/entitlementchecknondiva',
video_id, note='Checking video authorization', headers=headers, data=check_json) video_id, note='Checking video authorization', headers=headers, data=check_json)
formats = self._extract_m3u8_formats(check_result['contentUrl'], video_id, ext='mp4') formats = self._extract_m3u8_formats(check_result['contentUrl'], video_id, ext='mp4')
self._sort_formats(formats)
vdata = self._download_json( vdata = self._download_json(
'https://www.tennistv.com/api/en/v2/none/common/video/%s' % video_id, 'https://www.tennistv.com/api/en/v2/none/common/video/%s' % video_id,
+13 -2
View File
@@ -21,6 +21,11 @@ class URPlayIE(InfoExtractor):
'description': 'md5:5344508a52aa78c1ced6c1b8b9e44e9a', 'description': 'md5:5344508a52aa78c1ced6c1b8b9e44e9a',
'timestamp': 1513292400, 'timestamp': 1513292400,
'upload_date': '20171214', 'upload_date': '20171214',
'series': 'UR Samtiden - Livet, universum och rymdens märkliga musik',
'duration': 2269,
'categories': ['Kultur & historia'],
'tags': ['Kritiskt tänkande', 'Vetenskap', 'Vetenskaplig verksamhet'],
'episode': 'Om vetenskap, kritiskt tänkande och motstånd',
}, },
}, { }, {
'url': 'https://urskola.se/Produkter/190031-Tripp-Trapp-Trad-Sovkudde', 'url': 'https://urskola.se/Produkter/190031-Tripp-Trapp-Trad-Sovkudde',
@@ -31,6 +36,10 @@ class URPlayIE(InfoExtractor):
'description': 'md5:b86bffdae04a7e9379d1d7e5947df1d1', 'description': 'md5:b86bffdae04a7e9379d1d7e5947df1d1',
'timestamp': 1440086400, 'timestamp': 1440086400,
'upload_date': '20150820', 'upload_date': '20150820',
'series': 'Tripp, Trapp, Träd',
'duration': 865,
'tags': ['Sova'],
'episode': 'Sovkudde',
}, },
}, { }, {
'url': 'http://urskola.se/Produkter/155794-Smasagor-meankieli-Grodan-i-vida-varlden', 'url': 'http://urskola.se/Produkter/155794-Smasagor-meankieli-Grodan-i-vida-varlden',
@@ -41,9 +50,11 @@ class URPlayIE(InfoExtractor):
video_id = self._match_id(url) video_id = self._match_id(url)
url = url.replace('skola.se/Produkter', 'play.se/program') url = url.replace('skola.se/Produkter', 'play.se/program')
webpage = self._download_webpage(url, video_id) webpage = self._download_webpage(url, video_id)
urplayer_data = self._parse_json(self._html_search_regex( vid = int(video_id)
accessible_episodes = self._parse_json(self._html_search_regex(
r'data-react-class="routes/Product/components/ProgramContainer/ProgramContainer"[^>]+data-react-props="({.+?})"', r'data-react-class="routes/Product/components/ProgramContainer/ProgramContainer"[^>]+data-react-props="({.+?})"',
webpage, 'urplayer data'), video_id)['accessibleEpisodes'][0] webpage, 'urplayer data'), video_id)['accessibleEpisodes']
urplayer_data = next(e for e in accessible_episodes if e.get('id') == vid)
episode = urplayer_data['title'] episode = urplayer_data['title']
host = self._download_json('http://streaming-loadbalancer.ur.se/loadbalancer.json', video_id)['redirect'] host = self._download_json('http://streaming-loadbalancer.ur.se/loadbalancer.json', video_id)['redirect']
+20 -48
View File
@@ -255,15 +255,8 @@ class VikiIE(VikiBaseIE):
def _real_extract(self, url): def _real_extract(self, url):
video_id = self._match_id(url) video_id = self._match_id(url)
resp = self._download_json( video = self._call_api(
'https://www.viki.com/api/videos/' + video_id, 'videos/%s.json' % video_id, video_id, 'Downloading video JSON')
video_id, 'Downloading video JSON', headers={
'x-client-user-agent': std_headers['User-Agent'],
'x-viki-as-id': self._APP,
'x-viki-app-ver': self._APP_VERSION,
})
video = resp['video']
self._check_errors(video) self._check_errors(video)
title = self.dict_selection(video.get('titles', {}), 'en', allow_fallback=False) title = self.dict_selection(video.get('titles', {}), 'en', allow_fallback=False)
@@ -286,30 +279,12 @@ class VikiIE(VikiBaseIE):
}) })
subtitles = {} subtitles = {}
try: for subtitle_lang, _ in (video.get('subtitle_completions') or {}).items():
# New way to fetch subtitles subtitles[subtitle_lang] = [{
new_video = self._download_json( 'ext': subtitles_format,
'https://www.viki.com/api/videos/%s' % video_id, video_id, 'url': self._prepare_call(
'Downloading new video JSON to get subtitles', fatal=False, 'videos/%s/subtitles/%s.%s' % (video_id, subtitle_lang, subtitles_format)),
headers={ } for subtitles_format in ('srt', 'vtt')]
'x-client-user-agent': std_headers['User-Agent'],
'x-viki-as-id': self._APP,
'x-viki-app-ver': self._APP_VERSION,
})
for sub in new_video.get('streamSubtitles').get('dash'):
subtitles[sub.get('srclang')] = [{
'ext': 'vtt',
'url': sub.get('src'),
'completion': sub.get('percentage'),
}]
except AttributeError:
# fall-back to the old way if there isn't a streamSubtitles attribute
for subtitle_lang, _ in (video.get('subtitle_completions') or {}).items():
subtitles[subtitle_lang] = [{
'ext': subtitles_format,
'url': self._prepare_call(
'videos/%s/subtitles/%s.%s' % (video_id, subtitle_lang, subtitles_format)),
} for subtitles_format in ('srt', 'vtt')]
result = { result = {
'id': video_id, 'id': video_id,
@@ -386,23 +361,20 @@ class VikiIE(VikiBaseIE):
'filesize': int_or_none(urlh.headers.get('Content-Length')), 'filesize': int_or_none(urlh.headers.get('Content-Length')),
}) })
for format_id, format_dict in (resp.get('streams') or {}).items(): streams = self._call_api(
add_format(format_id, format_dict) 'videos/%s/streams.json' % video_id, video_id,
if not formats: 'Downloading video streams JSON')
streams = self._call_api(
'videos/%s/streams.json' % video_id, video_id,
'Downloading video streams JSON')
if 'external' in streams: if 'external' in streams:
result.update({ result.update({
'_type': 'url_transparent', '_type': 'url_transparent',
'url': streams['external']['url'], 'url': streams['external']['url'],
}) })
return result return result
for format_id, stream_dict in streams.items(): for format_id, stream_dict in streams.items():
for protocol, format_dict in stream_dict.items(): for protocol, format_dict in stream_dict.items():
add_format(format_id, format_dict, protocol) add_format(format_id, format_dict, protocol)
self._sort_formats(formats) self._sort_formats(formats)
result['formats'] = formats result['formats'] = formats
+15 -6
View File
@@ -75,12 +75,15 @@ class VVVVIDIE(InfoExtractor):
'https://www.vvvvid.it/user/login', 'https://www.vvvvid.it/user/login',
None, headers=self.geo_verification_headers())['data']['conn_id'] None, headers=self.geo_verification_headers())['data']['conn_id']
def _download_info(self, show_id, path, video_id, fatal=True): def _download_info(self, show_id, path, video_id, fatal=True, query=None):
q = {
'conn_id': self._conn_id,
}
if query:
q.update(query)
response = self._download_json( response = self._download_json(
'https://www.vvvvid.it/vvvvid/ondemand/%s/%s' % (show_id, path), 'https://www.vvvvid.it/vvvvid/ondemand/%s/%s' % (show_id, path),
video_id, headers=self.geo_verification_headers(), query={ video_id, headers=self.geo_verification_headers(), query=q, fatal=fatal)
'conn_id': self._conn_id,
}, fatal=fatal)
if not (response or fatal): if not (response or fatal):
return return
if response.get('result') == 'error': if response.get('result') == 'error':
@@ -98,7 +101,8 @@ class VVVVIDIE(InfoExtractor):
show_id, season_id, video_id = re.match(self._VALID_URL, url).groups() show_id, season_id, video_id = re.match(self._VALID_URL, url).groups()
response = self._download_info( response = self._download_info(
show_id, 'season/%s' % season_id, video_id) show_id, 'season/%s' % season_id,
video_id, query={'video_id': video_id})
vid = int(video_id) vid = int(video_id)
video_data = list(filter( video_data = list(filter(
@@ -247,9 +251,13 @@ class VVVVIDShowIE(VVVVIDIE):
show_info = self._download_info( show_info = self._download_info(
show_id, 'info/', show_title, fatal=False) show_id, 'info/', show_title, fatal=False)
if not show_title:
base_url += "/title"
entries = [] entries = []
for season in (seasons or []): for season in (seasons or []):
episodes = season.get('episodes') or [] episodes = season.get('episodes') or []
playlist_title = season.get('name') or show_info.get('title')
for episode in episodes: for episode in episodes:
if episode.get('playable') is False: if episode.get('playable') is False:
continue continue
@@ -259,12 +267,13 @@ class VVVVIDShowIE(VVVVIDIE):
continue continue
info = self._extract_common_video_info(episode) info = self._extract_common_video_info(episode)
info.update({ info.update({
'_type': 'url', '_type': 'url_transparent',
'ie_key': VVVVIDIE.ie_key(), 'ie_key': VVVVIDIE.ie_key(),
'url': '/'.join([base_url, season_id, video_id]), 'url': '/'.join([base_url, season_id, video_id]),
'title': episode.get('title'), 'title': episode.get('title'),
'description': episode.get('description'), 'description': episode.get('description'),
'season_id': season_id, 'season_id': season_id,
'playlist_title': playlist_title,
}) })
entries.append(info) entries.append(info)
+100 -64
View File
@@ -2,6 +2,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import hashlib
import itertools import itertools
import json import json
import os.path import os.path
@@ -274,7 +275,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
'context': { 'context': {
'client': { 'client': {
'clientName': 'WEB', 'clientName': 'WEB',
'clientVersion': '2.20201021.03.00', 'clientVersion': '2.20210301.08.00',
} }
}, },
} }
@@ -283,15 +284,28 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
_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)'
def _call_api(self, ep, query, video_id, fatal=True): def _generate_sapisidhash_header(self):
sapisid_cookie = self._get_cookies('https://www.youtube.com').get('SAPISID')
if sapisid_cookie is None:
return
time_now = round(time.time())
sapisidhash = hashlib.sha1((str(time_now) + " " + sapisid_cookie.value + " " + "https://www.youtube.com").encode("utf-8")).hexdigest()
return "SAPISIDHASH %s_%s" % (time_now, sapisidhash)
def _call_api(self, ep, query, video_id, fatal=True, headers=None,
note='Downloading API JSON', errnote='Unable to download API page'):
data = self._DEFAULT_API_DATA.copy() data = self._DEFAULT_API_DATA.copy()
data.update(query) data.update(query)
headers = headers or {}
headers.update({'content-type': 'application/json'})
auth = self._generate_sapisidhash_header()
if auth is not None:
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, video_id=video_id, 'https://www.youtube.com/youtubei/v1/%s' % ep,
note='Downloading API JSON', errnote='Unable to download API page', video_id=video_id, fatal=fatal, note=note, errnote=errnote,
data=json.dumps(data).encode('utf8'), fatal=fatal, data=json.dumps(data).encode('utf8'), headers=headers,
headers={'content-type': 'application/json'},
query={'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'}) query={'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'})
def _extract_yt_initial_data(self, video_id, webpage): def _extract_yt_initial_data(self, video_id, webpage):
@@ -1452,8 +1466,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
url, smuggled_data = unsmuggle_url(url, {}) url, smuggled_data = unsmuggle_url(url, {})
video_id = self._match_id(url) video_id = self._match_id(url)
base_url = self.http_scheme() + '//www.youtube.com/' base_url = self.http_scheme() + '//www.youtube.com/'
webpage_url = base_url + 'watch?v=' + video_id + '&has_verified=1&bpctr=9999999999' webpage_url = base_url + 'watch?v=' + video_id
webpage = self._download_webpage(webpage_url, video_id, fatal=False) webpage = self._download_webpage(
webpage_url + '&has_verified=1&bpctr=9999999999',
video_id, fatal=False)
player_response = None player_response = None
if webpage: if webpage:
@@ -2010,9 +2026,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
# Get comments # Get comments
# TODO: Refactor and move to seperate function # TODO: Refactor and move to seperate function
if get_comments: def extract_comments():
expected_video_comment_count = 0 expected_video_comment_count = 0
video_comments = [] video_comments = []
comment_xsrf = xsrf_token
def find_value(html, key, num_chars=2, separator='"'): def find_value(html, key, num_chars=2, separator='"'):
pos_begin = html.find(key) + len(key) + num_chars pos_begin = html.find(key) + len(key) + num_chars
@@ -2081,7 +2098,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
self.to_screen('Downloading comments') self.to_screen('Downloading comments')
while continuations: while continuations:
continuation = continuations.pop() continuation = continuations.pop()
comment_response = get_continuation(continuation, xsrf_token) comment_response = get_continuation(continuation, comment_xsrf)
if not comment_response: if not comment_response:
continue continue
if list(search_dict(comment_response, 'externalErrorMessage')): if list(search_dict(comment_response, 'externalErrorMessage')):
@@ -2092,7 +2109,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
continue continue
# not sure if this actually helps # not sure if this actually helps
if 'xsrf_token' in comment_response: if 'xsrf_token' in comment_response:
xsrf_token = comment_response['xsrf_token'] comment_xsrf = comment_response['xsrf_token']
item_section = comment_response['response']['continuationContents']['itemSectionContinuation'] item_section = comment_response['response']['continuationContents']['itemSectionContinuation']
if first_continuation: if first_continuation:
@@ -2121,7 +2138,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
while reply_continuations: while reply_continuations:
time.sleep(1) time.sleep(1)
continuation = reply_continuations.pop() continuation = reply_continuations.pop()
replies_data = get_continuation(continuation, xsrf_token, True) replies_data = get_continuation(continuation, comment_xsrf, True)
if not replies_data or 'continuationContents' not in replies_data[1]['response']: if not replies_data or 'continuationContents' not in replies_data[1]['response']:
continue continue
@@ -2150,10 +2167,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
time.sleep(1) time.sleep(1)
self.to_screen('Total comments downloaded: %d of ~%d' % (len(video_comments), expected_video_comment_count)) self.to_screen('Total comments downloaded: %d of ~%d' % (len(video_comments), expected_video_comment_count))
info.update({ return {
'comments': video_comments, 'comments': video_comments,
'comment_count': expected_video_comment_count 'comment_count': expected_video_comment_count
}) }
if get_comments:
info['__post_extractor'] = extract_comments
self.mark_watched(video_id, player_response) self.mark_watched(video_id, player_response)
@@ -2693,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, identity_token): 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 []
@@ -2756,27 +2776,39 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
for page_num in itertools.count(1): for page_num in itertools.count(1):
if not continuation: if not continuation:
break break
count = 0 retries = self._downloader.params.get('extractor_retries', 3)
retries = 3 count = -1
while count <= retries: last_error = None
while count < retries:
count += 1
if last_error:
self.report_warning('%s. Retrying ...' % last_error)
try: try:
# Downloading page may result in intermittent 5xx HTTP error response = self._call_api(
# that is usually worked around with a retry ep="browse", fatal=True, headers=headers,
browse = self._download_json( video_id='%s page %s' % (item_id, page_num),
'https://www.youtube.com/browse_ajax', None, query={
'Downloading page %d%s' 'continuation': continuation['continuation'],
% (page_num, ' (retry #%d)' % count if count else ''), 'clickTracking': {'clickTrackingParams': continuation['itct']},
headers=headers, query=continuation) },
break note='Downloading API JSON%s' % (' (retry #%d)' % count if count else ''))
except ExtractorError as e: except ExtractorError as e:
if isinstance(e.cause, compat_HTTPError) and e.cause.code in (500, 503): if isinstance(e.cause, compat_HTTPError) and e.cause.code in (500, 503, 404):
count += 1 # Downloading page may result in intermittent 5xx HTTP error
if count <= retries: # 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 count < retries:
continue continue
raise raise
if not browse: else:
break # Youtube sometimes sends incomplete data
response = try_get(browse, lambda x: x[1]['response'], dict) # See: https://github.com/ytdl-org/youtube-dl/issues/28194
if response.get('continuationContents') or response.get('onResponseReceivedActions'):
break
last_error = 'Incomplete data recieved'
if count >= retries:
self._downloader.report_error(last_error)
if not response: if not response:
break break
@@ -2920,7 +2952,7 @@ 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(selected_tab, identity_token), self._entries(selected_tab, identity_token, playlist_id),
**metadata) **metadata)
def _extract_from_playlist(self, item_id, url, data, playlist): def _extract_from_playlist(self, item_id, url, data, playlist):
@@ -2998,19 +3030,35 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
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)
self.to_screen('Downloading playlist %s - add --no-playlist to just download video %s' % (playlist_id, video_id)) self.to_screen('Downloading playlist %s - add --no-playlist to just download video %s' % (playlist_id, video_id))
webpage = self._download_webpage(url, item_id) retries = self._downloader.params.get('extractor_retries', 3)
identity_token = self._extract_identity_token(webpage, item_id) count = -1
data = self._extract_yt_initial_data(item_id, webpage) last_error = 'Incomplete yt initial data recieved'
err_msg = None while count < retries:
for alert_type, alert_message in self._extract_alerts(data): count += 1
if alert_type.lower() == 'error': # Sometimes youtube returns a webpage with incomplete ytInitialData
if err_msg: # See: https://github.com/yt-dlp/yt-dlp/issues/116
self._downloader.report_warning('YouTube said: %s - %s' % ('ERROR', err_msg)) if count:
err_msg = alert_message self.report_warning('%s. Retrying ...' % last_error)
else: webpage = self._download_webpage(
self._downloader.report_warning('YouTube said: %s - %s' % (alert_type, alert_message)) url, item_id,
if err_msg: 'Downloading webpage%s' % ' (retry #%d)' % count if count else '')
raise ExtractorError('YouTube said: %s' % err_msg, expected=True) identity_token = self._extract_identity_token(webpage, item_id)
data = self._extract_yt_initial_data(item_id, webpage)
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'):
break
if count >= retries:
self._downloader.report_error(last_error)
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:
@@ -3191,26 +3239,14 @@ class YoutubeSearchIE(SearchInfoExtractor, YoutubeBaseInfoExtractor):
_TESTS = [] _TESTS = []
def _entries(self, query, n): def _entries(self, query, n):
data = { data = {'query': query}
'context': {
'client': {
'clientName': 'WEB',
'clientVersion': '2.20201021.03.00',
}
},
'query': query,
}
if self._SEARCH_PARAMS: if self._SEARCH_PARAMS:
data['params'] = self._SEARCH_PARAMS data['params'] = self._SEARCH_PARAMS
total = 0 total = 0
for page_num in itertools.count(1): for page_num in itertools.count(1):
search = self._download_json( search = self._call_api(
'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', ep='search', video_id='query "%s"' % query, fatal=False,
video_id='query "%s"' % query, note='Downloading page %s' % page_num, query=data)
note='Downloading page %s' % page_num,
errnote='Unable to download API page', fatal=False,
data=json.dumps(data).encode('utf8'),
headers={'content-type': 'application/json'})
if not search: if not search:
break break
slr_contents = try_get( slr_contents = try_get(
@@ -3362,8 +3398,8 @@ class YoutubeSubscriptionsIE(YoutubeFeedsInfoExtractor):
class YoutubeHistoryIE(YoutubeFeedsInfoExtractor): class YoutubeHistoryIE(YoutubeFeedsInfoExtractor):
IE_DESC = 'Youtube watch history, ":ythistory" for short (requires authentication)' IE_DESC = 'Youtube watch history, ":ythis" for short (requires authentication)'
_VALID_URL = r':ythistory' _VALID_URL = r':ythis(?:tory)?'
_FEED_NAME = 'history' _FEED_NAME = 'history'
_TESTS = [{ _TESTS = [{
'url': ':ythistory', 'url': ':ythistory',
+125 -79
View File
@@ -7,7 +7,9 @@ from .common import InfoExtractor
from ..compat import compat_str from ..compat import compat_str
from ..utils import ( from ..utils import (
determine_ext, determine_ext,
float_or_none,
int_or_none, int_or_none,
merge_dicts,
NO_DEFAULT, NO_DEFAULT,
orderedSet, orderedSet,
parse_codecs, parse_codecs,
@@ -21,61 +23,17 @@ from ..utils import (
class ZDFBaseIE(InfoExtractor): class ZDFBaseIE(InfoExtractor):
def _call_api(self, url, player, referrer, video_id, item):
return self._download_json(
url, video_id, 'Downloading JSON %s' % item,
headers={
'Referer': referrer,
'Api-Auth': 'Bearer %s' % player['apiToken'],
})
def _extract_player(self, webpage, video_id, fatal=True):
return self._parse_json(
self._search_regex(
r'(?s)data-zdfplayer-jsb=(["\'])(?P<json>{.+?})\1', webpage,
'player JSON', default='{}' if not fatal else NO_DEFAULT,
group='json'),
video_id)
class ZDFIE(ZDFBaseIE):
IE_NAME = "ZDF-3sat"
_VALID_URL = r'https?://www\.(zdf|3sat)\.de/(?:[^/]+/)*(?P<id>[^/?]+)\.html'
_QUALITIES = ('auto', 'low', 'med', 'high', 'veryhigh', 'hd')
_GEO_COUNTRIES = ['DE'] _GEO_COUNTRIES = ['DE']
_QUALITIES = ('auto', 'low', 'med', 'high', 'veryhigh', 'hd')
_TESTS = [{ def _call_api(self, url, video_id, item, api_token=None, referrer=None):
'url': 'https://www.3sat.de/wissen/wissenschaftsdoku/luxusgut-lebensraum-100.html', headers = {}
'info_dict': { if api_token:
'id': 'luxusgut-lebensraum-100', headers['Api-Auth'] = 'Bearer %s' % api_token
'ext': 'mp4', if referrer:
'title': 'Luxusgut Lebensraum', headers['Referer'] = referrer
'description': 'md5:5c09b2f45ac3bc5233d1b50fc543d061', return self._download_json(
'duration': 2601, url, video_id, 'Downloading JSON %s' % item, headers=headers)
'timestamp': 1566497700,
'upload_date': '20190822',
}
}, {
'url': 'https://www.zdf.de/dokumentation/terra-x/die-magie-der-farben-von-koenigspurpur-und-jeansblau-100.html',
'info_dict': {
'id': 'die-magie-der-farben-von-koenigspurpur-und-jeansblau-100',
'ext': 'mp4',
'title': 'Die Magie der Farben (2/2)',
'description': 'md5:a89da10c928c6235401066b60a6d5c1a',
'duration': 2615,
'timestamp': 1465021200,
'upload_date': '20160604',
},
}, {
'url': 'https://www.zdf.de/service-und-hilfe/die-neue-zdf-mediathek/zdfmediathek-trailer-100.html',
'only_matching': True,
}, {
'url': 'https://www.zdf.de/filme/taunuskrimi/die-lebenden-und-die-toten-1---ein-taunuskrimi-100.html',
'only_matching': True,
}, {
'url': 'https://www.zdf.de/dokumentation/planet-e/planet-e-uebersichtsseite-weitere-dokumentationen-von-planet-e-100.html',
'only_matching': True,
}]
@staticmethod @staticmethod
def _extract_subtitles(src): def _extract_subtitles(src):
@@ -121,20 +79,11 @@ class ZDFIE(ZDFBaseIE):
}) })
formats.append(f) formats.append(f)
def _extract_entry(self, url, player, content, video_id): def _extract_ptmd(self, ptmd_url, video_id, api_token, referrer):
title = content.get('title') or content['teaserHeadline']
t = content['mainVideoContent']['http://zdf.de/rels/target']
ptmd_path = t.get('http://zdf.de/rels/streams/ptmd')
if not ptmd_path:
ptmd_path = t[
'http://zdf.de/rels/streams/ptmd-template'].replace(
'{playerId}', 'ngplayer_2_4')
ptmd = self._call_api( ptmd = self._call_api(
urljoin(url, ptmd_path), player, url, video_id, 'metadata') ptmd_url, video_id, 'metadata', api_token, referrer)
content_id = ptmd.get('basename') or ptmd_url.split('/')[-1]
formats = [] formats = []
track_uris = set() track_uris = set()
@@ -152,7 +101,7 @@ class ZDFIE(ZDFBaseIE):
continue continue
for track in tracks: for track in tracks:
self._extract_format( self._extract_format(
video_id, formats, track_uris, { content_id, formats, track_uris, {
'url': track.get('uri'), 'url': track.get('uri'),
'type': f.get('type'), 'type': f.get('type'),
'mimeType': f.get('mimeType'), 'mimeType': f.get('mimeType'),
@@ -161,6 +110,103 @@ class ZDFIE(ZDFBaseIE):
}) })
self._sort_formats(formats) self._sort_formats(formats)
duration = float_or_none(try_get(
ptmd, lambda x: x['attributes']['duration']['value']), scale=1000)
return {
'extractor_key': ZDFIE.ie_key(),
'id': content_id,
'duration': duration,
'formats': formats,
'subtitles': self._extract_subtitles(ptmd),
}
def _extract_player(self, webpage, video_id, fatal=True):
return self._parse_json(
self._search_regex(
r'(?s)data-zdfplayer-jsb=(["\'])(?P<json>{.+?})\1', webpage,
'player JSON', default='{}' if not fatal else NO_DEFAULT,
group='json'),
video_id)
class ZDFIE(ZDFBaseIE):
_VALID_URL = r'https?://www\.zdf\.de/(?:[^/]+/)*(?P<id>[^/?#&]+)\.html'
_TESTS = [{
# Same as https://www.phoenix.de/sendungen/ereignisse/corona-nachgehakt/wohin-fuehrt-der-protest-in-der-pandemie-a-2050630.html
'url': 'https://www.zdf.de/politik/phoenix-sendungen/wohin-fuehrt-der-protest-in-der-pandemie-100.html',
'md5': '34ec321e7eb34231fd88616c65c92db0',
'info_dict': {
'id': '210222_phx_nachgehakt_corona_protest',
'ext': 'mp4',
'title': 'Wohin führt der Protest in der Pandemie?',
'description': 'md5:7d643fe7f565e53a24aac036b2122fbd',
'duration': 1691,
'timestamp': 1613948400,
'upload_date': '20210221',
},
}, {
# Same as https://www.3sat.de/film/ab-18/10-wochen-sommer-108.html
'url': 'https://www.zdf.de/dokumentation/ab-18/10-wochen-sommer-102.html',
'md5': '0aff3e7bc72c8813f5e0fae333316a1d',
'info_dict': {
'id': '141007_ab18_10wochensommer_film',
'ext': 'mp4',
'title': 'Ab 18! - 10 Wochen Sommer',
'description': 'md5:8253f41dc99ce2c3ff892dac2d65fe26',
'duration': 2660,
'timestamp': 1608604200,
'upload_date': '20201222',
},
}, {
'url': 'https://www.zdf.de/dokumentation/terra-x/die-magie-der-farben-von-koenigspurpur-und-jeansblau-100.html',
'info_dict': {
'id': '151025_magie_farben2_tex',
'ext': 'mp4',
'title': 'Die Magie der Farben (2/2)',
'description': 'md5:a89da10c928c6235401066b60a6d5c1a',
'duration': 2615,
'timestamp': 1465021200,
'upload_date': '20160604',
},
}, {
# Same as https://www.phoenix.de/sendungen/dokumentationen/gesten-der-maechtigen-i-a-89468.html?ref=suche
'url': 'https://www.zdf.de/politik/phoenix-sendungen/die-gesten-der-maechtigen-100.html',
'only_matching': True,
}, {
# Same as https://www.3sat.de/film/spielfilm/der-hauptmann-100.html
'url': 'https://www.zdf.de/filme/filme-sonstige/der-hauptmann-112.html',
'only_matching': True,
}, {
# Same as https://www.3sat.de/wissen/nano/nano-21-mai-2019-102.html, equal media ids
'url': 'https://www.zdf.de/wissen/nano/nano-21-mai-2019-102.html',
'only_matching': True,
}, {
'url': 'https://www.zdf.de/service-und-hilfe/die-neue-zdf-mediathek/zdfmediathek-trailer-100.html',
'only_matching': True,
}, {
'url': 'https://www.zdf.de/filme/taunuskrimi/die-lebenden-und-die-toten-1---ein-taunuskrimi-100.html',
'only_matching': True,
}, {
'url': 'https://www.zdf.de/dokumentation/planet-e/planet-e-uebersichtsseite-weitere-dokumentationen-von-planet-e-100.html',
'only_matching': True,
}]
def _extract_entry(self, url, player, content, video_id):
title = content.get('title') or content['teaserHeadline']
t = content['mainVideoContent']['http://zdf.de/rels/target']
ptmd_path = t.get('http://zdf.de/rels/streams/ptmd')
if not ptmd_path:
ptmd_path = t[
'http://zdf.de/rels/streams/ptmd-template'].replace(
'{playerId}', 'ngplayer_2_4')
info = self._extract_ptmd(
urljoin(url, ptmd_path), video_id, player['apiToken'], url)
thumbnails = [] thumbnails = []
layouts = try_get( layouts = try_get(
content, lambda x: x['teaserImageRef']['layouts'], dict) content, lambda x: x['teaserImageRef']['layouts'], dict)
@@ -181,33 +227,33 @@ class ZDFIE(ZDFBaseIE):
}) })
thumbnails.append(thumbnail) thumbnails.append(thumbnail)
return { return merge_dicts(info, {
'id': video_id,
'title': title, 'title': title,
'description': content.get('leadParagraph') or content.get('teasertext'), 'description': content.get('leadParagraph') or content.get('teasertext'),
'duration': int_or_none(t.get('duration')), 'duration': int_or_none(t.get('duration')),
'timestamp': unified_timestamp(content.get('editorialDate')), 'timestamp': unified_timestamp(content.get('editorialDate')),
'thumbnails': thumbnails, 'thumbnails': thumbnails,
'subtitles': self._extract_subtitles(ptmd), })
'formats': formats,
}
def _extract_regular(self, url, player, video_id): def _extract_regular(self, url, player, video_id):
content = self._call_api( content = self._call_api(
player['content'], player, url, video_id, 'content') player['content'], video_id, 'content', player['apiToken'], url)
return self._extract_entry(player['content'], player, content, video_id) return self._extract_entry(player['content'], player, content, video_id)
def _extract_mobile(self, video_id): def _extract_mobile(self, video_id):
document = self._download_json( video = self._download_json(
'https://zdf-cdn.live.cellular.de/mediathekV2/document/%s' % video_id, 'https://zdf-cdn.live.cellular.de/mediathekV2/document/%s' % video_id,
video_id)['document'] video_id)
document = video['document']
title = document['titel'] title = document['titel']
content_id = document['basename']
formats = [] formats = []
format_urls = set() format_urls = set()
for f in document['formitaeten']: for f in document['formitaeten']:
self._extract_format(video_id, formats, format_urls, f) self._extract_format(content_id, formats, format_urls, f)
self._sort_formats(formats) self._sort_formats(formats)
thumbnails = [] thumbnails = []
@@ -225,12 +271,12 @@ class ZDFIE(ZDFBaseIE):
}) })
return { return {
'id': video_id, 'id': content_id,
'title': title, 'title': title,
'description': document.get('beschreibung'), 'description': document.get('beschreibung'),
'duration': int_or_none(document.get('length')), 'duration': int_or_none(document.get('length')),
'timestamp': unified_timestamp(try_get( 'timestamp': unified_timestamp(document.get('date')) or unified_timestamp(
document, lambda x: x['meta']['editorialDate'], compat_str)), try_get(video, lambda x: x['meta']['editorialDate'], compat_str)),
'thumbnails': thumbnails, 'thumbnails': thumbnails,
'subtitles': self._extract_subtitles(document), 'subtitles': self._extract_subtitles(document),
'formats': formats, 'formats': formats,
+28 -10
View File
@@ -347,7 +347,7 @@ def parseOpts(overrideArguments=None):
'Specify any key (see "OUTPUT TEMPLATE" for a list of available keys) to ' 'Specify any key (see "OUTPUT TEMPLATE" for a list of available keys) to '
'match if the key is present, ' 'match if the key is present, '
'!key to check if the key is not present, ' '!key to check if the key is not present, '
'key>NUMBER (like "comment_count > 12", also works with ' 'key>NUMBER (like "view_count > 12", also works with '
'>=, <, <=, !=, =) to compare against a number, ' '>=, <, <=, !=, =) to compare against a number, '
'key = \'LITERAL\' (like "uploader = \'Mike Smith\'", also works with !=) ' 'key = \'LITERAL\' (like "uploader = \'Mike Smith\'", also works with !=) '
'to match against a string literal ' 'to match against a string literal '
@@ -369,7 +369,7 @@ def parseOpts(overrideArguments=None):
help='Download only the video, if the URL refers to a video and a playlist') help='Download only the video, if the URL refers to a video and a playlist')
selection.add_option( selection.add_option(
'--yes-playlist', '--yes-playlist',
action='store_false', dest='noplaylist', default=False, action='store_false', dest='noplaylist',
help='Download the playlist, if the URL refers to a video and a playlist') help='Download the playlist, if the URL refers to a video and a playlist')
selection.add_option( selection.add_option(
'--age-limit', '--age-limit',
@@ -634,16 +634,24 @@ def parseOpts(overrideArguments=None):
help='Use ffmpeg instead of the native HLS downloader') help='Use ffmpeg instead of the native HLS downloader')
downloader.add_option( downloader.add_option(
'--hls-use-mpegts', '--hls-use-mpegts',
dest='hls_use_mpegts', action='store_true', dest='hls_use_mpegts', action='store_true', default=None,
help=( help=(
'Use the mpegts container for HLS videos, allowing to play the ' 'Use the mpegts container for HLS videos; '
'video while downloading (some players may not be able to play it)')) 'allowing some players to play the video while downloading, '
'and reducing the chance of file corruption if download is interrupted. '
'This is enabled by default for live streams'))
downloader.add_option(
'--no-hls-use-mpegts',
dest='hls_use_mpegts', action='store_false',
help=(
'Do not use the mpegts container for HLS videos. '
'This is default when not downloading live streams'))
downloader.add_option( downloader.add_option(
'--external-downloader', '--external-downloader',
dest='external_downloader', metavar='NAME', dest='external_downloader', metavar='NAME',
help=( help=(
'Use the specified external downloader. ' 'Name or path of the external downloader to use. '
'Currently supports %s' % ', '.join(list_external_downloaders()))) 'Currently supports %s (Recommended: aria2c)' % ', '.join(list_external_downloaders())))
downloader.add_option( downloader.add_option(
'--downloader-args', '--external-downloader-args', '--downloader-args', '--external-downloader-args',
metavar='NAME:ARGS', dest='external_downloader_args', default={}, type='str', metavar='NAME:ARGS', dest='external_downloader_args', default={}, type='str',
@@ -688,6 +696,10 @@ def parseOpts(overrideArguments=None):
'--bidi-workaround', '--bidi-workaround',
dest='bidi_workaround', action='store_true', dest='bidi_workaround', action='store_true',
help='Work around terminals that lack bidirectional text support. Requires bidiv or fribidi executable in PATH') help='Work around terminals that lack bidirectional text support. Requires bidiv or fribidi executable in PATH')
workarounds.add_option(
'--sleep-requests', metavar='SECONDS',
dest='sleep_interval_requests', type=float,
help='Number of seconds to sleep between requests during data extraction')
workarounds.add_option( workarounds.add_option(
'--sleep-interval', '--min-sleep-interval', metavar='SECONDS', '--sleep-interval', '--min-sleep-interval', metavar='SECONDS',
dest='sleep_interval', type=float, dest='sleep_interval', type=float,
@@ -706,7 +718,7 @@ def parseOpts(overrideArguments=None):
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,
help='Enforce sleep interval on subtitles as well') help='Number of seconds to sleep before each subtitle download')
verbosity = optparse.OptionGroup(parser, 'Verbosity and Simulation Options') verbosity = optparse.OptionGroup(parser, 'Verbosity and Simulation Options')
verbosity.add_option( verbosity.add_option(
@@ -973,7 +985,9 @@ def parseOpts(overrideArguments=None):
filesystem.add_option( filesystem.add_option(
'--get-comments', '--get-comments',
action='store_true', dest='getcomments', default=False, action='store_true', dest='getcomments', default=False,
help='Retrieve video comments to be placed in the .info.json file') help=(
'Retrieve video comments to be placed in the .info.json file. '
'The comments are fetched even without this option if the extraction is known to be quick'))
filesystem.add_option( filesystem.add_option(
'--load-info-json', '--load-info', '--load-info-json', '--load-info',
dest='load_info_filename', metavar='FILE', dest='load_info_filename', metavar='FILE',
@@ -1129,7 +1143,7 @@ def parseOpts(overrideArguments=None):
'Give field name to extract data from, and format of the field seperated by a ":". ' 'Give field name to extract data from, and format of the field 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 also be used. ' 'similar syntax to the output template can also be used. '
'The parsed parameters replace any existing values and can be use in output template' 'The parsed parameters replace any existing values and can be use in output template. '
'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". '
@@ -1204,6 +1218,10 @@ def parseOpts(overrideArguments=None):
help=optparse.SUPPRESS_HELP) help=optparse.SUPPRESS_HELP)
extractor = optparse.OptionGroup(parser, 'Extractor Options') extractor = optparse.OptionGroup(parser, 'Extractor Options')
extractor.add_option(
'--extractor-retries',
dest='extractor_retries', metavar='RETRIES', default=3,
help='Number of retries for known extractor errors (default is %default), or "infinite"')
extractor.add_option( extractor.add_option(
'--allow-dynamic-mpd', '--no-ignore-dynamic-mpd', '--allow-dynamic-mpd', '--no-ignore-dynamic-mpd',
action='store_true', dest='dynamic_mpd', default=True, action='store_true', dest='dynamic_mpd', default=True,
+2
View File
@@ -193,4 +193,6 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
info['__thumbnail_filename'], os.path.splitext(original_thumbnail)[1][1:]) info['__thumbnail_filename'], 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:
files_to_delete.append(original_thumbnail)
return files_to_delete, info return files_to_delete, info
+7 -3
View File
@@ -5945,9 +5945,13 @@ def make_dir(path, to_screen=None):
def get_executable_path(): def get_executable_path():
path = os.path.dirname(sys.argv[0]) from zipimport import zipimporter
if os.path.basename(sys.argv[0]) == '__main__': # Running from source if hasattr(sys, 'frozen'): # Running from PyInstaller
path = os.path.join(path, '..') path = os.path.dirname(sys.executable)
elif isinstance(globals().get('__loader__'), zipimporter): # Running from ZIP
path = os.path.join(os.path.dirname(__file__), '../..')
else:
path = os.path.join(os.path.dirname(__file__), '..')
return os.path.abspath(path) return os.path.abspath(path)
+1 -1
View File
@@ -1,3 +1,3 @@
from __future__ import unicode_literals from __future__ import unicode_literals
__version__ = '2021.02.19' __version__ = '2021.03.01'