mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2025-12-08 23:22:49 +01:00
Compare commits
74 Commits
2021.02.24
...
2021.03.15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
597c18665e | ||
|
|
10db0d2f57 | ||
|
|
7275535116 | ||
|
|
a1c5d2ca64 | ||
|
|
ca87974543 | ||
|
|
e92caff5d5 | ||
|
|
ea3a012d2a | ||
|
|
5b8917fb52 | ||
|
|
8eec0120a2 | ||
|
|
4cf1e5d2f9 | ||
|
|
0a473f2f0f | ||
|
|
e4edeb6226 | ||
|
|
d488e254d9 | ||
|
|
d7009caa03 | ||
|
|
54759df586 | ||
|
|
605b684c2d | ||
|
|
994443d24d | ||
|
|
c5640c4508 | ||
|
|
1f52a09e2e | ||
|
|
fc21af505c | ||
|
|
015f3b3120 | ||
|
|
5ba4a0b69c | ||
|
|
0852947fcc | ||
|
|
99594a11ce | ||
|
|
2be71994c0 | ||
|
|
26fe8ffed0 | ||
|
|
feee67ae88 | ||
|
|
1caaf92d47 | ||
|
|
d069eca7a3 | ||
|
|
f3eaa8dd1c | ||
|
|
9e631877f8 | ||
|
|
36147a63e3 | ||
|
|
57db6a87ef | ||
|
|
cd7c66cf01 | ||
|
|
2c736b4f61 | ||
|
|
c4a508ab31 | ||
|
|
7815e55572 | ||
|
|
162e6f0000 | ||
|
|
a8278ababd | ||
|
|
bd9ed42387 | ||
|
|
5f7514957f | ||
|
|
3721515bde | ||
|
|
a5c5623470 | ||
|
|
c705177da2 | ||
|
|
d6e51845b7 | ||
|
|
da7f321e93 | ||
|
|
097b056c5a | ||
|
|
f3b737ed19 | ||
|
|
ee1e05581e | ||
|
|
ec5e77c558 | ||
|
|
b3b30a4bca | ||
|
|
5372545ddb | ||
|
|
5ef7d9bdd8 | ||
|
|
62bff2c170 | ||
|
|
f0884c8b3f | ||
|
|
277d6ff5f2 | ||
|
|
1cf376f55a | ||
|
|
7f7de7f94d | ||
|
|
86878b6cd9 | ||
|
|
b3d1242534 | ||
|
|
9bd2020476 | ||
|
|
ed9b7e3dd3 | ||
|
|
c552ae8838 | ||
|
|
31a5e037a7 | ||
|
|
3638226215 | ||
|
|
14fdfea973 | ||
|
|
b45d4e4a8e | ||
|
|
3e39273418 | ||
|
|
b965087396 | ||
|
|
359d6d8650 | ||
|
|
0e0040519b | ||
|
|
127d075955 | ||
|
|
bce8cbb089 | ||
|
|
aae273ded8 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Makefile* text whitespace=-tab-in-indent
|
||||||
6
.github/ISSUE_TEMPLATE/1_broken_site.md
vendored
6
.github/ISSUE_TEMPLATE/1_broken_site.md
vendored
@@ -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.07. 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.07**
|
||||||
- [ ] 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.07
|
||||||
[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.07. 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.07**
|
||||||
- [ ] 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.07. 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.07**
|
||||||
- [ ] 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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/4_bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/4_bug_report.md
vendored
@@ -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.07. 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.07**
|
||||||
- [ ] 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.07
|
||||||
[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: {}
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/5_feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/5_feature_request.md
vendored
@@ -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.07. 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.07**
|
||||||
- [ ] I've searched the bugtracker for similar feature requests including closed ones
|
- [ ] I've searched the bugtracker for similar feature requests including closed ones
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/core.yml
vendored
2
.github/workflows/core.yml
vendored
@@ -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
|
||||||
|
|||||||
2
.github/workflows/download.yml
vendored
2
.github/workflows/download.yml
vendored
@@ -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
|
||||||
|
|||||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -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,15 @@ yt-dlp.zip
|
|||||||
*.m4v
|
*.m4v
|
||||||
*.mp3
|
*.mp3
|
||||||
*.3gp
|
*.3gp
|
||||||
|
*.webm
|
||||||
*.wav
|
*.wav
|
||||||
*.ape
|
*.ape
|
||||||
*.mkv
|
*.mkv
|
||||||
*.swf
|
*.swf
|
||||||
*.part
|
*.part
|
||||||
|
*.part-*
|
||||||
*.ytdl
|
*.ytdl
|
||||||
|
*.dump
|
||||||
*.frag
|
*.frag
|
||||||
*.frag.urls
|
*.frag.urls
|
||||||
*.aria2
|
*.aria2
|
||||||
|
|||||||
@@ -20,4 +20,3 @@ python:
|
|||||||
version: 3
|
version: 3
|
||||||
install:
|
install:
|
||||||
- requirements: docs/requirements.txt
|
- requirements: docs/requirements.txt
|
||||||
- requirements: requirements.txt
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -21,5 +21,12 @@ nao20010128nao
|
|||||||
kurumigi
|
kurumigi
|
||||||
tsukumi
|
tsukumi
|
||||||
bbepis
|
bbepis
|
||||||
|
animelover1984
|
||||||
Pccode66
|
Pccode66
|
||||||
Ashish
|
Ashish
|
||||||
|
RobinD42
|
||||||
|
hseg
|
||||||
|
colethedj
|
||||||
|
DennyDai
|
||||||
|
codeasashu
|
||||||
|
teesid
|
||||||
|
|||||||
148
Changelog.md
148
Changelog.md
@@ -17,10 +17,87 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
### 2021.03.15
|
||||||
|
* **Split video by chapters**: using option `--split-chapters`
|
||||||
|
* The output file of the split files can be set with `-o`/`-P` using the prefix `chapter:`
|
||||||
|
* Additional keys `section_title`, `section_number`, `section_start`, `section_end` are available in the output template
|
||||||
|
* **Parallel fragment downloads** by [shirt](https://github.com/shirt-dev)
|
||||||
|
* Use option `--concurrent-fragments` (`-N`) to set the number of threads (default 1)
|
||||||
|
* Merge youtube-dl: Upto [commit/3be0980](https://github.com/ytdl-org/youtube-dl/commit/3be098010f667b14075e3dfad1e74e5e2becc8ea)
|
||||||
|
* [Zee5] Add Show Extractor by [Ashish](https://github.com/Ashish) and [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [rai] fix drm check [nixxo](https://github.com/nixxo)
|
||||||
|
* [zee5] Support zee5originals
|
||||||
|
* [wimtv] Add extractor by [nixxo](https://github.com/nixxo)
|
||||||
|
* [mtv] Add mtv.it and extract series metadata by [nixxo](https://github.com/nixxo)
|
||||||
|
* [pluto.tv] Add extractor by [kevinoconnor7](https://github.com/kevinoconnor7)
|
||||||
|
* [Youtube] Rewrite comment extraction by [colethedj](https://github.com/colethedj)
|
||||||
|
* [embedthumbnail] Set mtime correctly
|
||||||
|
* Refactor some postprocessor/downloader code by [pukkandan](https://github.com/pukkandan) and [shirt](https://github.com/shirt-dev)
|
||||||
|
|
||||||
|
|
||||||
|
### 2021.03.07
|
||||||
|
* [youtube] Fix history, mixes, community pages and trending by [pukkandan](https://github.com/pukkandan) and [colethedj](https://github.com/colethedj)
|
||||||
|
* [youtube] Fix private feeds/playlists on multi-channel accounts by [colethedj](https://github.com/colethedj)
|
||||||
|
* [youtube] Extract alerts from continuation by [colethedj](https://github.com/colethedj)
|
||||||
|
* [cbs] Add support for ParamountPlus by [shirt](https://github.com/shirt-dev)
|
||||||
|
* [mxplayer] Rewrite extractor with show support by [pukkandan](https://github.com/pukkandan) and [Ashish](https://github.com/Ashish)
|
||||||
|
* [gedi] Improvements from youtube-dl by [nixxo](https://github.com/nixxo)
|
||||||
|
* [vimeo] Fix videos with password by [teesid](https://github.com/teesid)
|
||||||
|
* [lbry] Support `lbry://` url by [nixxo](https://github.com/nixxo)
|
||||||
|
* [bilibili] Change `Accept` header by [pukkandan](https://github.com/pukkandan) and [animelover1984](https://github.com/animelover1984)
|
||||||
|
* [trovo] Pass origin header
|
||||||
|
* [rai] Check for DRM by [nixxo](https://github.com/nixxo)
|
||||||
|
* [downloader] Fix bug for `ffmpeg`/`httpie`
|
||||||
|
* [update] Fix updater removing the executable bit on some UNIX distros
|
||||||
|
* [update] Fix current build hash for UNIX
|
||||||
|
* [documentation] Include wget/curl/aria2c install instructions for Unix by [Ashish](https://github.com/Ashish)
|
||||||
|
* Fix some videos downloading with `m3u8` extension
|
||||||
|
* Remove "fixup is ignored" warning when fixup wasn't passed by user
|
||||||
|
|
||||||
|
|
||||||
|
### 2021.03.03.2
|
||||||
|
* [build] Fix bug
|
||||||
|
|
||||||
|
|
||||||
|
### 2021.03.03
|
||||||
|
* [youtube] Use new browse API for continuation page extraction by [colethedj](https://github.com/colethedj) and [pukkandan](https://github.com/pukkandan)
|
||||||
|
* Fix HLS playlist downloading by [shirt](https://github.com/shirt-dev)
|
||||||
|
* 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](https://github.com/DennyDai)
|
||||||
|
* [mxplayer] Add new extractor by [codeasashu](https://github.com/codeasashu)
|
||||||
|
* [youtube] Throw error when `--extractor-retries` are exhausted
|
||||||
|
* Reduce default of `--extractor-retries` to 3
|
||||||
|
* Fix packaging bugs by [hseg](https://github.com/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)
|
||||||
* **Merge youtube-dl:** Upto [commit/4460329](https://github.com/ytdl-org/youtube-dl/commit/44603290e5002153f3ebad6230cc73aef42cc2cd) (except tmz, gedi)
|
* 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)
|
||||||
* [Readthedocs](https://yt-dlp.readthedocs.io) support by [shirt](https://github.com/shirt-dev)
|
* [Readthedocs](https://yt-dlp.readthedocs.io) support by [shirt](https://github.com/shirt-dev)
|
||||||
* [youtube] Show if video was a live stream in info (`was_live`)
|
* [youtube] Show if video was a live stream in info (`was_live`)
|
||||||
* [Zee5] Add new extractor by [Ashish](https://github.com/Ashish) and [pukkandan](https://github.com/pukkandan)
|
* [Zee5] Add new extractor by [Ashish](https://github.com/Ashish) and [pukkandan](https://github.com/pukkandan)
|
||||||
@@ -28,17 +105,17 @@
|
|||||||
* [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
|
||||||
|
|
||||||
|
|
||||||
### 2021.02.19
|
### 2021.02.19
|
||||||
* **Merge youtube-dl:** Upto [commit/cf2dbec](https://github.com/ytdl-org/youtube-dl/commit/cf2dbec6301177a1fddf72862de05fa912d9869d) (except kakao)
|
* Merge youtube-dl: Upto [commit/cf2dbec](https://github.com/ytdl-org/youtube-dl/commit/cf2dbec6301177a1fddf72862de05fa912d9869d) (except kakao)
|
||||||
* [viki] Fix extractor
|
* [viki] Fix extractor
|
||||||
* [niconico] Extract `channel` and `channel_id` by [kurumigi](https://github.com/kurumigi)
|
* [niconico] Extract `channel` and `channel_id` by [kurumigi](https://github.com/kurumigi)
|
||||||
* [youtube] Multiple page support for hashtag URLs
|
* [youtube] Multiple page support for hashtag URLs
|
||||||
@@ -63,7 +140,7 @@
|
|||||||
|
|
||||||
|
|
||||||
### 2021.02.15
|
### 2021.02.15
|
||||||
* **Merge youtube-dl:** Upto [2021.02.10](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.02.10) (except archive.org)
|
* Merge youtube-dl: Upto [2021.02.10](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.02.10) (except archive.org)
|
||||||
* [niconico] Improved extraction and support encrypted/SMILE movies by [kurumigi](https://github.com/kurumigi), [tsukumi](https://github.com/tsukumi), [bbepis](https://github.com/bbepis), [pukkandan](https://github.com/pukkandan)
|
* [niconico] Improved extraction and support encrypted/SMILE movies by [kurumigi](https://github.com/kurumigi), [tsukumi](https://github.com/tsukumi), [bbepis](https://github.com/bbepis), [pukkandan](https://github.com/pukkandan)
|
||||||
* Fix HLS AES-128 with multiple keys in external downloaders by [shirt](https://github.com/shirt-dev)
|
* Fix HLS AES-128 with multiple keys in external downloaders by [shirt](https://github.com/shirt-dev)
|
||||||
* [youtube_live_chat] Fix by using POST API by [siikamiika](https://github.com/siikamiika)
|
* [youtube_live_chat] Fix by using POST API by [siikamiika](https://github.com/siikamiika)
|
||||||
@@ -106,7 +183,7 @@
|
|||||||
|
|
||||||
|
|
||||||
### 2021.02.04
|
### 2021.02.04
|
||||||
* **Merge youtube-dl:** Upto [2021.02.04.1](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.02.04.1)
|
* Merge youtube-dl: Upto [2021.02.04.1](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.02.04.1)
|
||||||
* **Date/time formatting in output template:**
|
* **Date/time formatting in output template:**
|
||||||
* You can use [`strftime`](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) to format date/time fields. Example: `%(upload_date>%Y-%m-%d)s`
|
* You can use [`strftime`](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) to format date/time fields. Example: `%(upload_date>%Y-%m-%d)s`
|
||||||
* **Multiple output templates:**
|
* **Multiple output templates:**
|
||||||
@@ -160,7 +237,7 @@
|
|||||||
|
|
||||||
|
|
||||||
### 2021.01.24
|
### 2021.01.24
|
||||||
* **Merge youtube-dl:** Upto [2021.01.24](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.01.16)
|
* Merge youtube-dl: Upto [2021.01.24](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.01.16)
|
||||||
* Plugin support ([documentation](https://github.com/yt-dlp/yt-dlp#plugins))
|
* Plugin support ([documentation](https://github.com/yt-dlp/yt-dlp#plugins))
|
||||||
* **Multiple paths**: New option `-P`/`--paths` to give different paths for different types of files
|
* **Multiple paths**: New option `-P`/`--paths` to give different paths for different types of files
|
||||||
* The syntax is `-P "type:path" -P "type:path"` ([documentation](https://github.com/yt-dlp/yt-dlp#:~:text=-P,%20--paths%20TYPE:PATH))
|
* The syntax is `-P "type:path" -P "type:path"` ([documentation](https://github.com/yt-dlp/yt-dlp#:~:text=-P,%20--paths%20TYPE:PATH))
|
||||||
@@ -189,7 +266,7 @@
|
|||||||
|
|
||||||
|
|
||||||
### 2021.01.16
|
### 2021.01.16
|
||||||
* **Merge youtube-dl:** Upto [2021.01.16](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.01.16)
|
* Merge youtube-dl: Upto [2021.01.16](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.01.16)
|
||||||
* **Configuration files:**
|
* **Configuration files:**
|
||||||
* Portable configuration file: `./yt-dlp.conf`
|
* Portable configuration file: `./yt-dlp.conf`
|
||||||
* Allow the configuration files to be named `yt-dlp` instead of `youtube-dlc`. See [this](https://github.com/yt-dlp/yt-dlp#configuration) for details
|
* Allow the configuration files to be named `yt-dlp` instead of `youtube-dlc`. See [this](https://github.com/yt-dlp/yt-dlp#configuration) for details
|
||||||
@@ -235,8 +312,7 @@
|
|||||||
|
|
||||||
|
|
||||||
### 2021.01.08
|
### 2021.01.08
|
||||||
* **Merge youtube-dl:** Upto [2021.01.08](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.01.08)
|
* Merge youtube-dl: Upto [2021.01.08](https://github.com/ytdl-org/youtube-dl/releases/tag/2021.01.08) except stitcher ([1](https://github.com/ytdl-org/youtube-dl/commit/bb38a1215718cdf36d73ff0a7830a64cd9fa37cc), [2](https://github.com/ytdl-org/youtube-dl/commit/a563c97c5cddf55f8989ed7ea8314ef78e30107f))
|
||||||
* Extractor stitcher ([1](https://github.com/ytdl-org/youtube-dl/commit/bb38a1215718cdf36d73ff0a7830a64cd9fa37cc), [2](https://github.com/ytdl-org/youtube-dl/commit/a563c97c5cddf55f8989ed7ea8314ef78e30107f)) have not been merged
|
|
||||||
* Moved changelog to seperate file
|
* Moved changelog to seperate file
|
||||||
|
|
||||||
|
|
||||||
@@ -275,8 +351,8 @@
|
|||||||
* Changed video format sorting to show video only files and video+audio files together.
|
* Changed video format sorting to show video only files and video+audio files together.
|
||||||
* Added `--video-multistreams`, `--no-video-multistreams`, `--audio-multistreams`, `--no-audio-multistreams`
|
* Added `--video-multistreams`, `--no-video-multistreams`, `--audio-multistreams`, `--no-audio-multistreams`
|
||||||
* Added `b`,`w`,`v`,`a` as alias for `best`, `worst`, `video` and `audio` respectively
|
* Added `b`,`w`,`v`,`a` as alias for `best`, `worst`, `video` and `audio` respectively
|
||||||
* **Shortcut Options:** Added `--write-link`, `--write-url-link`, `--write-webloc-link`, `--write-desktop-link` by [h-h-h-h](https://github.com/h-h-h-h) - See [Internet Shortcut Options](README.md#internet-shortcut-options) for details
|
* Shortcut Options: Added `--write-link`, `--write-url-link`, `--write-webloc-link`, `--write-desktop-link` by [h-h-h-h](https://github.com/h-h-h-h) - See [Internet Shortcut Options](README.md#internet-shortcut-options) for details
|
||||||
* **Sponskrub integration:** Added `--sponskrub`, `--sponskrub-cut`, `--sponskrub-force`, `--sponskrub-location`, `--sponskrub-args` - See [SponSkrub Options](README.md#sponskrub-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
|
||||||
@@ -285,36 +361,38 @@
|
|||||||
* Relaxed validation for format filters so that any arbitrary field can be used
|
* Relaxed validation for format filters so that any arbitrary field can be used
|
||||||
* Fix for embedding thumbnail in mp3 by [pauldubois98](https://github.com/pauldubois98) ([ytdl-org/youtube-dl#21569](https://github.com/ytdl-org/youtube-dl/pull/21569))
|
* Fix for embedding thumbnail in mp3 by [pauldubois98](https://github.com/pauldubois98) ([ytdl-org/youtube-dl#21569](https://github.com/ytdl-org/youtube-dl/pull/21569))
|
||||||
* Make Twitch Video ID output from Playlist and VOD extractor same. This is only a temporary fix
|
* Make Twitch Video ID output from Playlist and VOD extractor same. This is only a temporary fix
|
||||||
* **Merge youtube-dl:** Upto [2021.01.03](https://github.com/ytdl-org/youtube-dl/commit/8e953dcbb10a1a42f4e12e4e132657cb0100a1f8) - See [blackjack4494/yt-dlc#280](https://github.com/blackjack4494/yt-dlc/pull/280) for details
|
* Merge youtube-dl: Upto [2021.01.03](https://github.com/ytdl-org/youtube-dl/commit/8e953dcbb10a1a42f4e12e4e132657cb0100a1f8) - See [blackjack4494/yt-dlc#280](https://github.com/blackjack4494/yt-dlc/pull/280) for details
|
||||||
* Extractors [tiktok](https://github.com/ytdl-org/youtube-dl/commit/fb626c05867deab04425bad0c0b16b55473841a2) and [hotstar](https://github.com/ytdl-org/youtube-dl/commit/bb38a1215718cdf36d73ff0a7830a64cd9fa37cc) have not been merged
|
* Extractors [tiktok](https://github.com/ytdl-org/youtube-dl/commit/fb626c05867deab04425bad0c0b16b55473841a2) and [hotstar](https://github.com/ytdl-org/youtube-dl/commit/bb38a1215718cdf36d73ff0a7830a64cd9fa37cc) have not been merged
|
||||||
* Cleaned up the fork for public use
|
* Cleaned up the fork for public use
|
||||||
|
|
||||||
|
|
||||||
|
**PS**: All uncredited changes above this point are authored by [pukkandan](https://github.com/pukkandan)
|
||||||
|
|
||||||
### Unreleased changes in [blackjack4494/yt-dlc](https://github.com/blackjack4494/yt-dlc)
|
### Unreleased changes in [blackjack4494/yt-dlc](https://github.com/blackjack4494/yt-dlc)
|
||||||
* Updated to youtube-dl release 2020.11.26
|
* Updated to youtube-dl release 2020.11.26 by [pukkandan](https://github.com/pukkandan)
|
||||||
* [youtube]
|
* Youtube improvements by [pukkandan](https://github.com/pukkandan)
|
||||||
* Implemented all Youtube Feeds (ytfav, ytwatchlater, ytsubs, ythistory, ytrec) and SearchURL
|
* Implemented all Youtube Feeds (ytfav, ytwatchlater, ytsubs, ythistory, ytrec) and SearchURL
|
||||||
* Fix ytsearch not returning results sometimes due to promoted content
|
|
||||||
* Temporary fix for automatic captions - disable json3
|
|
||||||
* Fix some improper Youtube URLs
|
* Fix some improper Youtube URLs
|
||||||
* Redirect channel home to /video
|
* Redirect channel home to /video
|
||||||
* Print youtube's warning message
|
* Print youtube's warning message
|
||||||
* Multiple pages are handled better for feeds
|
* Handle Multiple pages for feeds better
|
||||||
|
* [youtube] Fix ytsearch not returning results sometimes due to promoted content by [colethedj](https://github.com/colethedj)
|
||||||
|
* [youtube] Temporary fix for automatic captions - disable json3 by [blackjack4494](https://github.com/blackjack4494)
|
||||||
* Add --break-on-existing by [gergesh](https://github.com/gergesh)
|
* Add --break-on-existing by [gergesh](https://github.com/gergesh)
|
||||||
* Pre-check video IDs in the archive before downloading
|
* Pre-check video IDs in the archive before downloading by [pukkandan](https://github.com/pukkandan)
|
||||||
* [bitwave.tv] New extractor
|
* [bitwave.tv] New extractor by [lorpus](https://github.com/lorpus)
|
||||||
* [Gedi] Add extractor
|
* [Gedi] Add extractor by [nixxo](https://github.com/nixxo)
|
||||||
* [Rcs] Add new extractor
|
* [Rcs] Add new extractor by [nixxo](https://github.com/nixxo)
|
||||||
* [skyit] Add support for multiple Sky Italia website and removed old skyitalia extractor
|
* [skyit] New skyitalia extractor by [nixxo](https://github.com/nixxo)
|
||||||
* [france.tv] Fix thumbnail URL
|
* [france.tv] Fix thumbnail URL by [renalid](https://github.com/renalid)
|
||||||
* [ina] support mobile links
|
* [ina] support mobile links by [B0pol](https://github.com/B0pol)
|
||||||
* [instagram] Fix extractor
|
* [instagram] Fix thumbnail extractor by [nao20010128nao](https://github.com/nao20010128nao)
|
||||||
* [itv] BTCC new pages' URL update (articles instead of races)
|
* [SouthparkDe] Support for English URLs by [xypwn](https://github.com/xypwn)
|
||||||
* [SouthparkDe] Support for English URLs
|
* [spreaker] fix SpreakerShowIE test URL by [pukkandan](https://github.com/pukkandan)
|
||||||
* [spreaker] fix SpreakerShowIE test URL
|
* [Vlive] Fix playlist handling when downloading a channel by [kyuyeunk](https://github.com/kyuyeunk)
|
||||||
* [Vlive] Fix playlist handling when downloading a channel
|
* [tmz] Fix extractor by [diegorodriguezv](https://github.com/diegorodriguezv)
|
||||||
* [generic] Detect embedded bitchute videos
|
* [generic] Detect embedded bitchute videos by [pukkandan](https://github.com/pukkandan)
|
||||||
* [generic] Extract embedded youtube and twitter videos
|
* [generic] Extract embedded youtube and twitter videos by [diegorodriguezv](https://github.com/diegorodriguezv)
|
||||||
* [ffmpeg] Ensure all streams are copied
|
* [ffmpeg] Ensure all streams are copied by [pukkandan](https://github.com/pukkandan)
|
||||||
* Fix for os.rename error when embedding thumbnail to video in a different drive
|
* [embedthumbnail] Fix for os.rename error by [pukkandan](https://github.com/pukkandan)
|
||||||
* make_win.bat: don't use UPX to pack vcruntime140.dll
|
* make_win.bat: don't use UPX to pack vcruntime140.dll by [jbruchon](https://github.com/jbruchon)
|
||||||
12
MANIFEST.in
12
MANIFEST.in
@@ -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 *
|
||||||
|
|||||||
91
Makefile
91
Makefile
@@ -1,12 +1,28 @@
|
|||||||
all: yt-dlp doc man
|
all: yt-dlp doc pypi-files
|
||||||
|
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
|
||||||
|
|||||||
108
README.md
108
README.md
@@ -11,7 +11,7 @@
|
|||||||
[](https://pypi.org/project/yt-dlp)
|
[](https://pypi.org/project/yt-dlp)
|
||||||
[](https://yt-dlp.readthedocs.io)
|
[](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.14**: 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.
|
||||||
|
|
||||||
@@ -66,17 +66,19 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
|
|||||||
* Youtube search (`ytsearch:`, `ytsearchdate:`) along with Search URLs works correctly
|
* Youtube search (`ytsearch:`, `ytsearchdate:`) along with Search URLs works correctly
|
||||||
* Redirect channel's home URL automatically to `/video` to preserve the old behaviour
|
* Redirect channel's home URL automatically to `/video` to preserve the old behaviour
|
||||||
|
|
||||||
|
* **Split video by chapters**: Videos can be split into multiple files based on chapters using `--split-chapters`
|
||||||
|
|
||||||
|
* **Multithreaded fragment downloads**: Fragment downloads can be natively multi-threaded. Use `--concurrent-fragments` (`-N`) option to set the number of threads used
|
||||||
|
|
||||||
* **Aria2c with HLS/DASH**: You can use aria2c as the external downloader for DASH(mpd) and HLS(m3u8) formats. No more slow ffmpeg/native downloads
|
* **Aria2c with HLS/DASH**: You can use aria2c as the external downloader for DASH(mpd) and HLS(m3u8) formats. No more slow ffmpeg/native downloads
|
||||||
|
|
||||||
* **New extractors**: AnimeLab, Philo MSO, Rcs, Gedi, bitwave.tv, mildom, audius, zee5
|
* **New extractors**: AnimeLab, Philo MSO, Rcs, Gedi, bitwave.tv, mildom, audius, zee5, mtv.it, wimtv, pluto.tv
|
||||||
|
|
||||||
* **Fixed extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, tiktok, akamai, ina, rumble, tennistv
|
* **Fixed extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, tiktok, akamai, ina, rumble, tennistv
|
||||||
|
|
||||||
* **Plugin support**: Extractors can be loaded from an external file. See [plugins](#plugins) for details
|
* **Plugin support**: Extractors can be loaded from an external file. See [plugins](#plugins) for details
|
||||||
|
|
||||||
* **Multiple paths and output templates**: You can give different [output templates](#output-template) and download paths for different types of files. You can also set a temporary path where intermediary files are downloaded to. See [`--paths`](https://github.com/yt-dlp/yt-dlp/#:~:text=-P,%20--paths%20TYPE:PATH) for details
|
* **Multiple paths and output templates**: You can give different [output templates](#output-template) and download paths for different types of files. You can also set a temporary path where intermediary files are downloaded to using `--paths` (`-P`)
|
||||||
|
|
||||||
<!-- Relative link doesn't work for "#:~:text=" -->
|
|
||||||
|
|
||||||
* **Portable Configuration**: Configuration files are automatically loaded from the home and root directories. See [configuration](#configuration) for details
|
* **Portable Configuration**: Configuration files are automatically loaded from the home and root directories. See [configuration](#configuration) for details
|
||||||
|
|
||||||
@@ -92,7 +94,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
|
||||||
@@ -103,6 +105,23 @@ You can install yt-dlp using one of the following methods:
|
|||||||
* Use pip+git: `python -m pip install --upgrade git+https://github.com/yt-dlp/yt-dlp.git@release`
|
* Use pip+git: `python -m pip install --upgrade git+https://github.com/yt-dlp/yt-dlp.git@release`
|
||||||
* Install master branch: `python -m pip install --upgrade git+https://github.com/yt-dlp/yt-dlp`
|
* Install master branch: `python -m pip install --upgrade git+https://github.com/yt-dlp/yt-dlp`
|
||||||
|
|
||||||
|
UNIX users (Linux, macOS, BSD) can also install the [latest release](https://github.com/yt-dlp/yt-dlp/releases/latest) one of the following ways:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
|
||||||
|
sudo chmod a+rx /usr/local/bin/yt-dlp
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp
|
||||||
|
sudo chmod a+rx /usr/local/bin/yt-dlp
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo aria2c https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
|
||||||
|
sudo chmod a+rx /usr/local/bin/yt-dlp
|
||||||
|
```
|
||||||
|
|
||||||
### UPDATE
|
### UPDATE
|
||||||
Starting from version `2021.02.09`, you can use `yt-dlp -U` to update if you are using the provided release.
|
Starting from version `2021.02.09`, you can use `yt-dlp -U` to update if you are using the provided release.
|
||||||
If you are using `pip`, simply re-run the same command that was used to install the program.
|
If you are using `pip`, simply re-run the same command that was used to install the program.
|
||||||
@@ -122,12 +141,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...]
|
||||||
|
|
||||||
@@ -177,7 +196,7 @@ Then simply run `make`. You can also run `make youtube_dlc` instead to compile o
|
|||||||
only list them
|
only list them
|
||||||
--no-flat-playlist Extract the videos of a playlist
|
--no-flat-playlist Extract the videos of a playlist
|
||||||
--mark-watched Mark videos watched (YouTube only)
|
--mark-watched Mark videos watched (YouTube only)
|
||||||
--no-mark-watched Do not mark videos watched
|
--no-mark-watched Do not mark videos watched (default)
|
||||||
--no-colors Do not emit color codes in output
|
--no-colors Do not emit color codes in output
|
||||||
|
|
||||||
## Network Options:
|
## Network Options:
|
||||||
@@ -245,7 +264,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
|
||||||
@@ -280,6 +299,8 @@ Then simply run `make`. You can also run `make youtube_dlc` instead to compile o
|
|||||||
--no-include-ads Do not download advertisements (default)
|
--no-include-ads Do not download advertisements (default)
|
||||||
|
|
||||||
## Download Options:
|
## Download Options:
|
||||||
|
-N, --concurrent-fragments N Number of fragments to download
|
||||||
|
concurrently (default is 1)
|
||||||
-r, --limit-rate RATE Maximum download rate in bytes per second
|
-r, --limit-rate RATE Maximum download rate in bytes per second
|
||||||
(e.g. 50K or 4.2M)
|
(e.g. 50K or 4.2M)
|
||||||
-R, --retries RETRIES Number of retries (default is 10), or
|
-R, --retries RETRIES Number of retries (default is 10), or
|
||||||
@@ -317,13 +338,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 +424,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 +514,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 +526,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"
|
||||||
@@ -613,11 +645,12 @@ Then simply run `make`. You can also run `make youtube_dlc` instead to compile o
|
|||||||
arguments to the specified executable only
|
arguments to the specified executable only
|
||||||
when being used by the specified
|
when being used by the specified
|
||||||
postprocessor. Additionally, for
|
postprocessor. Additionally, for
|
||||||
ffmpeg/ffprobe, a number can be appended to
|
ffmpeg/ffprobe, "_i"/"_o" can be appended
|
||||||
the exe name seperated by "_i" to pass the
|
to the prefix optionally followed by a
|
||||||
argument before the specified input file.
|
number to pass the argument before the
|
||||||
Eg: --ppa "Merger+ffmpeg_i1:-v quiet". You
|
specified input/output file. Eg: --ppa
|
||||||
can use this option multiple times to give
|
"Merger+ffmpeg_i1:-v quiet". You can use
|
||||||
|
this option multiple times to give
|
||||||
different arguments to different
|
different arguments to different
|
||||||
postprocessors. (Alias: --ppa)
|
postprocessors. (Alias: --ppa)
|
||||||
-k, --keep-video Keep the intermediate video file on disk
|
-k, --keep-video Keep the intermediate video file on disk
|
||||||
@@ -641,7 +674,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
|
||||||
@@ -663,6 +696,13 @@ Then simply run `make`. You can also run `make youtube_dlc` instead to compile o
|
|||||||
push {} /sdcard/Music/ && rm {}'
|
push {} /sdcard/Music/ && rm {}'
|
||||||
--convert-subs FORMAT Convert the subtitles to other format
|
--convert-subs FORMAT Convert the subtitles to other format
|
||||||
(currently supported: srt|ass|vtt|lrc)
|
(currently supported: srt|ass|vtt|lrc)
|
||||||
|
--split-chapters Split video into multiple files based on
|
||||||
|
internal chapters. The "chapter:" prefix
|
||||||
|
can be used with "--paths" and "--output"
|
||||||
|
to set the output filename for the split
|
||||||
|
files. See "OUTPUT TEMPLATE" for details
|
||||||
|
--no-split-chapters Do not split video based on chapters
|
||||||
|
(default)
|
||||||
|
|
||||||
## SponSkrub (SponsorBlock) Options:
|
## SponSkrub (SponsorBlock) Options:
|
||||||
[SponSkrub](https://github.com/yt-dlp/SponSkrub) is a utility to
|
[SponSkrub](https://github.com/yt-dlp/SponSkrub) is a utility to
|
||||||
@@ -686,6 +726,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
|
||||||
@@ -776,9 +818,9 @@ The `-o` option is used to indicate a template for the output file names while `
|
|||||||
|
|
||||||
**tl;dr:** [navigate me to examples](#output-template-examples).
|
**tl;dr:** [navigate me to examples](#output-template-examples).
|
||||||
|
|
||||||
The basic usage of `-o` is not to set any template arguments when downloading a single file, like in `yt-dlp -o funny_video.flv "https://some/video"`. However, it may contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [python string formatting operations](https://docs.python.org/2/library/stdtypes.html#string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations. Date/time fields can also be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it inside the parantheses seperated from the field name using a `>`. For example, `%(duration>%H-%M-%S)s`.
|
The basic usage of `-o` is not to set any template arguments when downloading a single file, like in `yt-dlp -o funny_video.flv "https://some/video"` (hard-coding file extension like this is not recommended). However, it may contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [python string formatting operations](https://docs.python.org/2/library/stdtypes.html#string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations. Date/time fields can also be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it inside the parantheses seperated from the field name using a `>`. For example, `%(duration>%H-%M-%S)s`.
|
||||||
|
|
||||||
Additionally, you can set different output templates for the various metadata files seperately from the general output template by specifying the type of file followed by the template seperated by a colon ":". The different filetypes supported are `subtitle|thumbnail|description|annotation|infojson|pl_description|pl_infojson`. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video.
|
Additionally, you can set different output templates for the various metadata files seperately from the general output template by specifying the type of file followed by the template seperated by a colon ":". The different filetypes supported are `subtitle`, `thumbnail`, `description`, `annotation`, `infojson`, `pl_description`, `pl_infojson`, `chapter`. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video.
|
||||||
|
|
||||||
The available fields are:
|
The available fields are:
|
||||||
|
|
||||||
@@ -787,6 +829,7 @@ The available fields are:
|
|||||||
- `url` (string): Video URL
|
- `url` (string): Video URL
|
||||||
- `ext` (string): Video filename extension
|
- `ext` (string): Video filename extension
|
||||||
- `alt_title` (string): A secondary title of the video
|
- `alt_title` (string): A secondary title of the video
|
||||||
|
- `description` (string): The description of the video
|
||||||
- `display_id` (string): An alternative identifier for the video
|
- `display_id` (string): An alternative identifier for the video
|
||||||
- `uploader` (string): Full name of the video uploader
|
- `uploader` (string): Full name of the video uploader
|
||||||
- `license` (string): License name the video is licensed under
|
- `license` (string): License name the video is licensed under
|
||||||
@@ -805,7 +848,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
|
||||||
@@ -869,6 +912,13 @@ Available for the media that is a track or a part of a music album:
|
|||||||
- `disc_number` (numeric): Number of the disc or other physical medium the track belongs to
|
- `disc_number` (numeric): Number of the disc or other physical medium the track belongs to
|
||||||
- `release_year` (numeric): Year (YYYY) when the album was released
|
- `release_year` (numeric): Year (YYYY) when the album was released
|
||||||
|
|
||||||
|
Available when using `--split-chapters` for videos with internal chapters:
|
||||||
|
|
||||||
|
- `section_title` (string): Title of the chapter
|
||||||
|
- `section_number` (numeric): Number of the chapter within the file
|
||||||
|
- `section_start` (numeric): Start time of the chapter in seconds
|
||||||
|
- `section_end` (numeric): End time of the chapter in seconds
|
||||||
|
|
||||||
Each aforementioned sequence when referenced in an output template will be replaced by the actual value corresponding to the sequence name. Note that some of the sequences are not guaranteed to be present since they depend on the metadata obtained by a particular extractor. Such sequences will be replaced with placeholder value provided with `--output-na-placeholder` (`NA` by default).
|
Each aforementioned sequence when referenced in an output template will be replaced by the actual value corresponding to the sequence name. Note that some of the sequences are not guaranteed to be present since they depend on the metadata obtained by a particular extractor. Such sequences will be replaced with placeholder value provided with `--output-na-placeholder` (`NA` by default).
|
||||||
|
|
||||||
For example for `-o %(title)s-%(id)s.%(ext)s` and an mp4 video with title `yt-dlp test video` and id `BaW_jenozKcj`, this will result in a `yt-dlp test video-BaW_jenozKcj.mp4` file created in the current directory.
|
For example for `-o %(title)s-%(id)s.%(ext)s` and an mp4 video with title `yt-dlp test video` and id `BaW_jenozKcj`, this will result in a `yt-dlp test video-BaW_jenozKcj.mp4` file created in the current directory.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
docs/Changelog.md
Normal file
5
docs/Changelog.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
orphan: true
|
||||||
|
---
|
||||||
|
```{include} ../Changelog.md
|
||||||
|
```
|
||||||
6
docs/LICENSE.md
Normal file
6
docs/LICENSE.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
orphan: true
|
||||||
|
---
|
||||||
|
# LICENSE
|
||||||
|
```{include} ../LICENSE
|
||||||
|
```
|
||||||
2
docs/README.md
Normal file
2
docs/README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
```{include} ../README.md
|
||||||
|
```
|
||||||
23
docs/conf.py
23
docs/conf.py
@@ -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 +0,0 @@
|
|||||||
../README.md
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
recommonmark>=0.6.0
|
myst-parser
|
||||||
m2r2
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
6
docs/ytdlp_plugins.md
Normal file
6
docs/ytdlp_plugins.md
Normal 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
setup.py
7
setup.py
@@ -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))
|
||||||
|
|||||||
1252
supportedsites.md
Normal file
1252
supportedsites.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,6 @@ class TestAllURLsMatching(unittest.TestCase):
|
|||||||
assertPlaylist('PL63F0C78739B09958')
|
assertPlaylist('PL63F0C78739B09958')
|
||||||
assertTab('https://www.youtube.com/AsapSCIENCE')
|
assertTab('https://www.youtube.com/AsapSCIENCE')
|
||||||
assertTab('https://www.youtube.com/embedded')
|
assertTab('https://www.youtube.com/embedded')
|
||||||
assertTab('https://www.youtube.com/feed') # Own channel's home page
|
|
||||||
assertTab('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')
|
assertTab('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')
|
||||||
assertTab('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
assertTab('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
||||||
assertTab('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')
|
assertTab('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')
|
||||||
|
|||||||
@@ -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,6 +381,7 @@ 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:
|
||||||
|
extractor_retries: Number of times to retry for known errors
|
||||||
dynamic_mpd: Whether to process dynamic DASH manifests (default: True)
|
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)
|
||||||
@@ -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
|
||||||
@@ -1165,6 +1171,9 @@ class YoutubeDL(object):
|
|||||||
else:
|
else:
|
||||||
raise Exception('Invalid result type: %s' % result_type)
|
raise Exception('Invalid result type: %s' % result_type)
|
||||||
|
|
||||||
|
def _ensure_dir_exists(self, path):
|
||||||
|
return make_dir(path, self.report_error)
|
||||||
|
|
||||||
def __process_playlist(self, ie_result, download):
|
def __process_playlist(self, ie_result, download):
|
||||||
# We process each entry in the playlist
|
# We process each entry in the playlist
|
||||||
playlist = ie_result.get('title') or ie_result.get('id')
|
playlist = ie_result.get('title') or ie_result.get('id')
|
||||||
@@ -1181,12 +1190,9 @@ class YoutubeDL(object):
|
|||||||
}
|
}
|
||||||
ie_copy.update(dict(ie_result))
|
ie_copy.update(dict(ie_result))
|
||||||
|
|
||||||
def ensure_dir_exists(path):
|
|
||||||
return make_dir(path, self.report_error)
|
|
||||||
|
|
||||||
if self.params.get('writeinfojson', False):
|
if self.params.get('writeinfojson', False):
|
||||||
infofn = self.prepare_filename(ie_copy, 'pl_infojson')
|
infofn = self.prepare_filename(ie_copy, 'pl_infojson')
|
||||||
if not ensure_dir_exists(encodeFilename(infofn)):
|
if not self._ensure_dir_exists(encodeFilename(infofn)):
|
||||||
return
|
return
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
|
||||||
self.to_screen('[info] Playlist metadata is already present')
|
self.to_screen('[info] Playlist metadata is already present')
|
||||||
@@ -1202,7 +1208,7 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
if self.params.get('writedescription', False):
|
if self.params.get('writedescription', False):
|
||||||
descfn = self.prepare_filename(ie_copy, 'pl_description')
|
descfn = self.prepare_filename(ie_copy, 'pl_description')
|
||||||
if not ensure_dir_exists(encodeFilename(descfn)):
|
if not self._ensure_dir_exists(encodeFilename(descfn)):
|
||||||
return
|
return
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
|
||||||
self.to_screen('[info] Playlist description is already present')
|
self.to_screen('[info] Playlist description is already present')
|
||||||
@@ -1788,12 +1794,16 @@ class YoutubeDL(object):
|
|||||||
if 'display_id' not in info_dict and 'id' in info_dict:
|
if 'display_id' not in info_dict and 'id' in info_dict:
|
||||||
info_dict['display_id'] = info_dict['id']
|
info_dict['display_id'] = info_dict['id']
|
||||||
|
|
||||||
if info_dict.get('upload_date') is None and info_dict.get('timestamp') is not None:
|
for ts_key, date_key in (
|
||||||
|
('timestamp', 'upload_date'),
|
||||||
|
('release_timestamp', 'release_date'),
|
||||||
|
):
|
||||||
|
if info_dict.get(date_key) is None and info_dict.get(ts_key) is not None:
|
||||||
# Working around out-of-range timestamp values (e.g. negative ones on Windows,
|
# Working around out-of-range timestamp values (e.g. negative ones on Windows,
|
||||||
# see http://bugs.python.org/issue1646728)
|
# see http://bugs.python.org/issue1646728)
|
||||||
try:
|
try:
|
||||||
upload_date = datetime.datetime.utcfromtimestamp(info_dict['timestamp'])
|
upload_date = datetime.datetime.utcfromtimestamp(info_dict[ts_key])
|
||||||
info_dict['upload_date'] = upload_date.strftime('%Y%m%d')
|
info_dict[date_key] = upload_date.strftime('%Y%m%d')
|
||||||
except (ValueError, OverflowError, OSError):
|
except (ValueError, OverflowError, OSError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -2036,6 +2046,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 +2070,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)
|
||||||
@@ -2081,17 +2093,14 @@ class YoutubeDL(object):
|
|||||||
if full_filename is None:
|
if full_filename is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
def ensure_dir_exists(path):
|
if not self._ensure_dir_exists(encodeFilename(full_filename)):
|
||||||
return make_dir(path, self.report_error)
|
|
||||||
|
|
||||||
if not ensure_dir_exists(encodeFilename(full_filename)):
|
|
||||||
return
|
return
|
||||||
if not ensure_dir_exists(encodeFilename(temp_filename)):
|
if not self._ensure_dir_exists(encodeFilename(temp_filename)):
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.params.get('writedescription', False):
|
if self.params.get('writedescription', False):
|
||||||
descfn = self.prepare_filename(info_dict, 'description')
|
descfn = self.prepare_filename(info_dict, 'description')
|
||||||
if not ensure_dir_exists(encodeFilename(descfn)):
|
if not self._ensure_dir_exists(encodeFilename(descfn)):
|
||||||
return
|
return
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
|
||||||
self.to_screen('[info] Video description is already present')
|
self.to_screen('[info] Video description is already present')
|
||||||
@@ -2108,7 +2117,7 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
if self.params.get('writeannotations', False):
|
if self.params.get('writeannotations', False):
|
||||||
annofn = self.prepare_filename(info_dict, 'annotation')
|
annofn = self.prepare_filename(info_dict, 'annotation')
|
||||||
if not ensure_dir_exists(encodeFilename(annofn)):
|
if not self._ensure_dir_exists(encodeFilename(annofn)):
|
||||||
return
|
return
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(annofn)):
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(annofn)):
|
||||||
self.to_screen('[info] Video annotations are already present')
|
self.to_screen('[info] Video annotations are already present')
|
||||||
@@ -2166,15 +2175,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' %
|
||||||
@@ -2205,7 +2205,7 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
if self.params.get('writeinfojson', False):
|
if self.params.get('writeinfojson', False):
|
||||||
infofn = self.prepare_filename(info_dict, 'infojson')
|
infofn = self.prepare_filename(info_dict, 'infojson')
|
||||||
if not ensure_dir_exists(encodeFilename(infofn)):
|
if not self._ensure_dir_exists(encodeFilename(infofn)):
|
||||||
return
|
return
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
|
||||||
self.to_screen('[info] Video metadata is already present')
|
self.to_screen('[info] Video metadata is already present')
|
||||||
@@ -2361,7 +2361,7 @@ class YoutubeDL(object):
|
|||||||
fname = prepend_extension(
|
fname = prepend_extension(
|
||||||
self.prepare_filename(new_info, 'temp'),
|
self.prepare_filename(new_info, 'temp'),
|
||||||
'f%s' % f['format_id'], new_info['ext'])
|
'f%s' % f['format_id'], new_info['ext'])
|
||||||
if not ensure_dir_exists(fname):
|
if not self._ensure_dir_exists(fname):
|
||||||
return
|
return
|
||||||
downloaded.append(fname)
|
downloaded.append(fname)
|
||||||
partial_success, real_download = dl(fname, new_info)
|
partial_success, real_download = dl(fname, new_info)
|
||||||
@@ -2438,9 +2438,8 @@ class YoutubeDL(object):
|
|||||||
else:
|
else:
|
||||||
assert fixup_policy in ('ignore', 'never')
|
assert fixup_policy in ('ignore', 'never')
|
||||||
|
|
||||||
if (info_dict.get('protocol') == 'm3u8_native'
|
if ('protocol' in info_dict
|
||||||
or info_dict.get('protocol') == 'm3u8'
|
and get_suitable_downloader(info_dict, self.params).__name__ == 'HlsFD'):
|
||||||
and self.params.get('hls_prefer_native')):
|
|
||||||
if fixup_policy == 'warn':
|
if fixup_policy == 'warn':
|
||||||
self.report_warning('%s: malformed AAC bitstream detected.' % (
|
self.report_warning('%s: malformed AAC bitstream detected.' % (
|
||||||
info_dict['id']))
|
info_dict['id']))
|
||||||
@@ -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'])
|
||||||
|
|||||||
@@ -169,25 +169,35 @@ 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
|
||||||
|
if opts.concurrent_fragment_downloads <= 0:
|
||||||
|
raise ValueError('Concurrent fragments must be positive')
|
||||||
|
|
||||||
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,11 +272,21 @@ 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:
|
||||||
report_conflict('--recode-video', '--remux-video')
|
report_conflict('--recode-video', '--remux-video')
|
||||||
opts.remuxvideo = False
|
opts.remuxvideo = False
|
||||||
|
if opts.sponskrub_cut and opts.split_chapters and opts.sponskrub is not False:
|
||||||
|
report_conflict('--split-chapter', '--sponskrub-cut')
|
||||||
|
opts.sponskrub_cut = False
|
||||||
|
|
||||||
if opts.allow_unplayable_formats:
|
if opts.allow_unplayable_formats:
|
||||||
if opts.extractaudio:
|
if opts.extractaudio:
|
||||||
report_conflict('--allow-unplayable-formats', '--extract-audio')
|
report_conflict('--allow-unplayable-formats', '--extract-audio')
|
||||||
@@ -356,11 +376,7 @@ def _real_main(argv=None):
|
|||||||
})
|
})
|
||||||
if not already_have_thumbnail:
|
if not already_have_thumbnail:
|
||||||
opts.writethumbnail = True
|
opts.writethumbnail = True
|
||||||
# XAttrMetadataPP should be run after post-processors that may change file
|
# This should be below most ffmpeg PP because it may cut parts out from the video
|
||||||
# contents
|
|
||||||
if opts.xattrs:
|
|
||||||
postprocessors.append({'key': 'XAttrMetadata'})
|
|
||||||
# This should be below all ffmpeg PP because it may cut parts out from the video
|
|
||||||
# If opts.sponskrub is None, sponskrub is used, but it silently fails if the executable can't be found
|
# If opts.sponskrub is None, sponskrub is used, but it silently fails if the executable can't be found
|
||||||
if opts.sponskrub is not False:
|
if opts.sponskrub is not False:
|
||||||
postprocessors.append({
|
postprocessors.append({
|
||||||
@@ -371,6 +387,11 @@ def _real_main(argv=None):
|
|||||||
'force': opts.sponskrub_force,
|
'force': opts.sponskrub_force,
|
||||||
'ignoreerror': opts.sponskrub is None,
|
'ignoreerror': opts.sponskrub is None,
|
||||||
})
|
})
|
||||||
|
if opts.split_chapters:
|
||||||
|
postprocessors.append({'key': 'FFmpegSplitChapters'})
|
||||||
|
# XAttrMetadataPP should be run after post-processors that may change file contents
|
||||||
|
if opts.xattrs:
|
||||||
|
postprocessors.append({'key': 'XAttrMetadata'})
|
||||||
# ExecAfterDownload must be the last PP
|
# ExecAfterDownload must be the last PP
|
||||||
if opts.exec_cmd:
|
if opts.exec_cmd:
|
||||||
postprocessors.append({
|
postprocessors.append({
|
||||||
@@ -447,8 +468,10 @@ 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,
|
||||||
|
'concurrent_fragment_downloads': opts.concurrent_fragment_downloads,
|
||||||
'buffersize': opts.buffersize,
|
'buffersize': opts.buffersize,
|
||||||
'noresizebuffer': opts.noresizebuffer,
|
'noresizebuffer': opts.noresizebuffer,
|
||||||
'http_chunk_size': opts.http_chunk_size,
|
'http_chunk_size': opts.http_chunk_size,
|
||||||
@@ -466,7 +489,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 +547,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 +565,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,
|
||||||
|
|||||||
@@ -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'):
|
||||||
|
|||||||
@@ -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):
|
||||||
@@ -326,6 +326,12 @@ class FileDownloader(object):
|
|||||||
"""Report it was impossible to resume download."""
|
"""Report it was impossible to resume download."""
|
||||||
self.to_screen('[download] Unable to resume')
|
self.to_screen('[download] Unable to resume')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def supports_manifest(manifest):
|
||||||
|
""" Whether the downloader can download the fragments from the manifest.
|
||||||
|
Redefine in subclasses if needed. """
|
||||||
|
pass
|
||||||
|
|
||||||
def download(self, filename, info_dict, subtitle=False):
|
def download(self, filename, info_dict, subtitle=False):
|
||||||
"""Download to a filename using the info from info_dict
|
"""Download to a filename using the info from info_dict
|
||||||
Return True on success and False otherwise
|
Return True on success and False otherwise
|
||||||
@@ -359,7 +365,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 +375,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
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
try:
|
||||||
|
import concurrent.futures
|
||||||
|
can_threaded_download = True
|
||||||
|
except ImportError:
|
||||||
|
can_threaded_download = False
|
||||||
|
|
||||||
from ..downloader import _get_real_downloader
|
from ..downloader import _get_real_downloader
|
||||||
from .fragment import FragmentFD
|
from .fragment import FragmentFD
|
||||||
|
|
||||||
from ..compat import compat_urllib_error
|
from ..compat import compat_urllib_error
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
DownloadError,
|
DownloadError,
|
||||||
|
sanitize_open,
|
||||||
urljoin,
|
urljoin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DashSegmentsFD(FragmentFD):
|
class DashSegmentsFD(FragmentFD):
|
||||||
"""
|
"""
|
||||||
Download segments in a DASH manifest
|
Download segments in a DASH manifest. External downloaders can take over
|
||||||
|
the fragment downloads by supporting the 'frag_urls' protocol
|
||||||
"""
|
"""
|
||||||
|
|
||||||
FD_NAME = 'dashsegments'
|
FD_NAME = 'dashsegments'
|
||||||
@@ -37,7 +45,7 @@ class DashSegmentsFD(FragmentFD):
|
|||||||
fragment_retries = self.params.get('fragment_retries', 0)
|
fragment_retries = self.params.get('fragment_retries', 0)
|
||||||
skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True)
|
skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True)
|
||||||
|
|
||||||
fragment_urls = []
|
fragments_to_download = []
|
||||||
frag_index = 0
|
frag_index = 0
|
||||||
for i, fragment in enumerate(fragments):
|
for i, fragment in enumerate(fragments):
|
||||||
frag_index += 1
|
frag_index += 1
|
||||||
@@ -48,9 +56,29 @@ class DashSegmentsFD(FragmentFD):
|
|||||||
assert fragment_base_url
|
assert fragment_base_url
|
||||||
fragment_url = urljoin(fragment_base_url, fragment['path'])
|
fragment_url = urljoin(fragment_base_url, fragment['path'])
|
||||||
|
|
||||||
|
fragments_to_download.append({
|
||||||
|
'frag_index': frag_index,
|
||||||
|
'index': i,
|
||||||
|
'url': fragment_url,
|
||||||
|
})
|
||||||
|
|
||||||
if real_downloader:
|
if real_downloader:
|
||||||
fragment_urls.append(fragment_url)
|
info_copy = info_dict.copy()
|
||||||
continue
|
info_copy['fragments'] = fragments_to_download
|
||||||
|
fd = real_downloader(self.ydl, self.params)
|
||||||
|
# TODO: Make progress updates work without hooking twice
|
||||||
|
# for ph in self._progress_hooks:
|
||||||
|
# fd.add_progress_hook(ph)
|
||||||
|
success = fd.real_download(filename, info_copy)
|
||||||
|
if not success:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
def download_fragment(fragment):
|
||||||
|
i = fragment['index']
|
||||||
|
frag_index = fragment['frag_index']
|
||||||
|
fragment_url = fragment['url']
|
||||||
|
|
||||||
|
ctx['fragment_index'] = frag_index
|
||||||
|
|
||||||
# In DASH, the first segment contains necessary headers to
|
# In DASH, the first segment contains necessary headers to
|
||||||
# generate a valid MP4 file, so always abort for the first segment
|
# generate a valid MP4 file, so always abort for the first segment
|
||||||
@@ -60,8 +88,7 @@ class DashSegmentsFD(FragmentFD):
|
|||||||
try:
|
try:
|
||||||
success, frag_content = self._download_fragment(ctx, fragment_url, info_dict)
|
success, frag_content = self._download_fragment(ctx, fragment_url, info_dict)
|
||||||
if not success:
|
if not success:
|
||||||
return False
|
return False, frag_index
|
||||||
self._append_fragment(ctx, frag_content)
|
|
||||||
break
|
break
|
||||||
except compat_urllib_error.HTTPError as err:
|
except compat_urllib_error.HTTPError as err:
|
||||||
# YouTube may often return 404 HTTP error for a fragment causing the
|
# YouTube may often return 404 HTTP error for a fragment causing the
|
||||||
@@ -77,27 +104,73 @@ class DashSegmentsFD(FragmentFD):
|
|||||||
# Don't retry fragment if error occurred during HTTP downloading
|
# Don't retry fragment if error occurred during HTTP downloading
|
||||||
# itself since it has own retry settings
|
# itself since it has own retry settings
|
||||||
if not fatal:
|
if not fatal:
|
||||||
self.report_skip_fragment(frag_index)
|
|
||||||
break
|
break
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if count > fragment_retries:
|
if count > fragment_retries:
|
||||||
if not fatal:
|
if not fatal:
|
||||||
self.report_skip_fragment(frag_index)
|
return False, frag_index
|
||||||
continue
|
|
||||||
self.report_error('giving up after %s fragment retries' % fragment_retries)
|
self.report_error('giving up after %s fragment retries' % fragment_retries)
|
||||||
return False
|
return False, frag_index
|
||||||
|
|
||||||
if real_downloader:
|
return frag_content, frag_index
|
||||||
info_copy = info_dict.copy()
|
|
||||||
info_copy['url_list'] = fragment_urls
|
def append_fragment(frag_content, frag_index):
|
||||||
fd = real_downloader(self.ydl, self.params)
|
if frag_content:
|
||||||
# TODO: Make progress updates work without hooking twice
|
fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], frag_index)
|
||||||
# for ph in self._progress_hooks:
|
try:
|
||||||
# fd.add_progress_hook(ph)
|
file, frag_sanitized = sanitize_open(fragment_filename, 'rb')
|
||||||
success = fd.real_download(filename, info_copy)
|
ctx['fragment_filename_sanitized'] = frag_sanitized
|
||||||
if not success:
|
file.close()
|
||||||
|
self._append_fragment(ctx, frag_content)
|
||||||
|
return True
|
||||||
|
except FileNotFoundError:
|
||||||
|
if skip_unavailable_fragments:
|
||||||
|
self.report_skip_fragment(frag_index)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.report_error(
|
||||||
|
'fragment %s not found, unable to continue' % frag_index)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
if skip_unavailable_fragments:
|
||||||
|
self.report_skip_fragment(frag_index)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.report_error(
|
||||||
|
'fragment %s not found, unable to continue' % frag_index)
|
||||||
|
return False
|
||||||
|
|
||||||
|
max_workers = self.params.get('concurrent_fragment_downloads', 1)
|
||||||
|
if can_threaded_download and max_workers > 1:
|
||||||
|
self.report_warning('The download speed shown is only of one thread. This is a known issue')
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
|
||||||
|
futures = [pool.submit(download_fragment, fragment) for fragment in fragments_to_download]
|
||||||
|
# timeout must be 0 to return instantly
|
||||||
|
done, not_done = concurrent.futures.wait(futures, timeout=0)
|
||||||
|
try:
|
||||||
|
while not_done:
|
||||||
|
# Check every 1 second for KeyboardInterrupt
|
||||||
|
freshly_done, not_done = concurrent.futures.wait(not_done, timeout=1)
|
||||||
|
done |= freshly_done
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
for future in not_done:
|
||||||
|
future.cancel()
|
||||||
|
# timeout must be none to cancel
|
||||||
|
concurrent.futures.wait(not_done, timeout=None)
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
results = [future.result() for future in futures]
|
||||||
|
|
||||||
|
for frag_content, frag_index in results:
|
||||||
|
result = append_fragment(frag_content, frag_index)
|
||||||
|
if not result:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
for fragment in fragments_to_download:
|
||||||
|
frag_content, frag_index = download_fragment(fragment)
|
||||||
|
result = append_fragment(frag_content, frag_index)
|
||||||
|
if not result:
|
||||||
|
return False
|
||||||
|
|
||||||
self._finish_frag_download(ctx)
|
self._finish_frag_download(ctx)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -108,7 +108,8 @@ class ExternalFD(FileDownloader):
|
|||||||
def _configuration_args(self, *args, **kwargs):
|
def _configuration_args(self, *args, **kwargs):
|
||||||
return cli_configuration_args(
|
return cli_configuration_args(
|
||||||
self.params.get('external_downloader_args'),
|
self.params.get('external_downloader_args'),
|
||||||
self.get_basename(), *args, **kwargs)
|
[self.get_basename(), 'default'],
|
||||||
|
*args, **kwargs)
|
||||||
|
|
||||||
def _call_downloader(self, tmpfilename, info_dict):
|
def _call_downloader(self, tmpfilename, info_dict):
|
||||||
""" Either overwrite this or implement _make_cmd """
|
""" Either overwrite this or implement _make_cmd """
|
||||||
@@ -122,18 +123,14 @@ class ExternalFD(FileDownloader):
|
|||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
self.to_stderr(stderr.decode('utf-8', 'replace'))
|
self.to_stderr(stderr.decode('utf-8', 'replace'))
|
||||||
|
|
||||||
if 'url_list' in info_dict:
|
if 'fragments' in info_dict:
|
||||||
file_list = []
|
file_list = []
|
||||||
for [i, url] in enumerate(info_dict['url_list']):
|
|
||||||
tmpsegmentname = '%s_%s.frag' % (tmpfilename, i)
|
|
||||||
file_list.append(tmpsegmentname)
|
|
||||||
key_list = info_dict.get('key_list')
|
|
||||||
decrypt_info = None
|
|
||||||
dest, _ = sanitize_open(tmpfilename, 'wb')
|
dest, _ = sanitize_open(tmpfilename, 'wb')
|
||||||
for i, file in enumerate(file_list):
|
for i, fragment in enumerate(info_dict['fragments']):
|
||||||
|
file = '%s-Frag%d' % (tmpfilename, i)
|
||||||
|
decrypt_info = fragment.get('decrypt_info')
|
||||||
src, _ = sanitize_open(file, 'rb')
|
src, _ = sanitize_open(file, 'rb')
|
||||||
if key_list:
|
if decrypt_info:
|
||||||
decrypt_info = next((x for x in key_list if x['INDEX'] == i), decrypt_info)
|
|
||||||
if decrypt_info['METHOD'] == 'AES-128':
|
if decrypt_info['METHOD'] == 'AES-128':
|
||||||
iv = decrypt_info.get('IV')
|
iv = decrypt_info.get('IV')
|
||||||
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
|
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
|
||||||
@@ -149,6 +146,7 @@ class ExternalFD(FileDownloader):
|
|||||||
fragment_data = src.read()
|
fragment_data = src.read()
|
||||||
dest.write(fragment_data)
|
dest.write(fragment_data)
|
||||||
src.close()
|
src.close()
|
||||||
|
file_list.append(file)
|
||||||
dest.close()
|
dest.close()
|
||||||
if not self.params.get('keep_fragments', False):
|
if not self.params.get('keep_fragments', False):
|
||||||
for file_path in file_list:
|
for file_path in file_list:
|
||||||
@@ -245,10 +243,19 @@ class Aria2cFD(ExternalFD):
|
|||||||
AVAILABLE_OPT = '-v'
|
AVAILABLE_OPT = '-v'
|
||||||
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'frag_urls')
|
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'frag_urls')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def supports_manifest(manifest):
|
||||||
|
UNSUPPORTED_FEATURES = [
|
||||||
|
r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [1]
|
||||||
|
# 1. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.2
|
||||||
|
]
|
||||||
|
check_results = (not re.search(feature, manifest) for feature in UNSUPPORTED_FEATURES)
|
||||||
|
return all(check_results)
|
||||||
|
|
||||||
def _make_cmd(self, tmpfilename, info_dict):
|
def _make_cmd(self, tmpfilename, info_dict):
|
||||||
cmd = [self.exe, '-c']
|
cmd = [self.exe, '-c']
|
||||||
dn = os.path.dirname(tmpfilename)
|
dn = os.path.dirname(tmpfilename)
|
||||||
if 'url_list' not in info_dict:
|
if 'fragments' not in info_dict:
|
||||||
cmd += ['--out', os.path.basename(tmpfilename)]
|
cmd += ['--out', os.path.basename(tmpfilename)]
|
||||||
verbose_level_args = ['--console-log-level=warn', '--summary-interval=0']
|
verbose_level_args = ['--console-log-level=warn', '--summary-interval=0']
|
||||||
cmd += self._configuration_args(['--file-allocation=none', '-x16', '-j16', '-s16'] + verbose_level_args)
|
cmd += self._configuration_args(['--file-allocation=none', '-x16', '-j16', '-s16'] + verbose_level_args)
|
||||||
@@ -262,14 +269,14 @@ class Aria2cFD(ExternalFD):
|
|||||||
cmd += self._bool_option('--check-certificate', 'nocheckcertificate', 'false', 'true', '=')
|
cmd += self._bool_option('--check-certificate', 'nocheckcertificate', 'false', 'true', '=')
|
||||||
cmd += self._bool_option('--remote-time', 'updatetime', 'true', 'false', '=')
|
cmd += self._bool_option('--remote-time', 'updatetime', 'true', 'false', '=')
|
||||||
cmd += ['--auto-file-renaming=false']
|
cmd += ['--auto-file-renaming=false']
|
||||||
if 'url_list' in info_dict:
|
if 'fragments' in info_dict:
|
||||||
cmd += verbose_level_args
|
cmd += verbose_level_args
|
||||||
cmd += ['--uri-selector', 'inorder', '--download-result=hide']
|
cmd += ['--uri-selector', 'inorder', '--download-result=hide']
|
||||||
url_list_file = '%s.frag.urls' % tmpfilename
|
url_list_file = '%s.frag.urls' % tmpfilename
|
||||||
url_list = []
|
url_list = []
|
||||||
for [i, url] in enumerate(info_dict['url_list']):
|
for i, fragment in enumerate(info_dict['fragments']):
|
||||||
tmpsegmentname = '%s_%s.frag' % (os.path.basename(tmpfilename), i)
|
tmpsegmentname = '%s-Frag%d' % (os.path.basename(tmpfilename), i)
|
||||||
url_list.append('%s\n\tout=%s' % (url, tmpsegmentname))
|
url_list.append('%s\n\tout=%s' % (fragment['url'], tmpsegmentname))
|
||||||
stream, _ = sanitize_open(url_list_file, 'wb')
|
stream, _ = sanitize_open(url_list_file, 'wb')
|
||||||
stream.write('\n'.join(url_list).encode('utf-8'))
|
stream.write('\n'.join(url_list).encode('utf-8'))
|
||||||
stream.close()
|
stream.close()
|
||||||
@@ -282,8 +289,8 @@ class Aria2cFD(ExternalFD):
|
|||||||
|
|
||||||
class HttpieFD(ExternalFD):
|
class HttpieFD(ExternalFD):
|
||||||
@classmethod
|
@classmethod
|
||||||
def available(cls):
|
def available(cls, path=None):
|
||||||
return check_executable('http', ['--version'])
|
return check_executable(path or 'http', ['--version'])
|
||||||
|
|
||||||
def _make_cmd(self, tmpfilename, info_dict):
|
def _make_cmd(self, tmpfilename, info_dict):
|
||||||
cmd = ['http', '--download', '--output', tmpfilename, info_dict['url']]
|
cmd = ['http', '--download', '--output', tmpfilename, info_dict['url']]
|
||||||
@@ -298,7 +305,7 @@ class FFmpegFD(ExternalFD):
|
|||||||
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'm3u8', 'rtsp', 'rtmp', 'mms')
|
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'm3u8', 'rtsp', 'rtmp', 'mms')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def available(cls):
|
def available(cls, path=None): # path is ignored for ffmpeg
|
||||||
return FFmpegPostProcessor().available
|
return FFmpegPostProcessor().available
|
||||||
|
|
||||||
def _call_downloader(self, tmpfilename, info_dict):
|
def _call_downloader(self, tmpfilename, info_dict):
|
||||||
@@ -398,7 +405,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']
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ try:
|
|||||||
can_decrypt_frag = True
|
can_decrypt_frag = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
can_decrypt_frag = False
|
can_decrypt_frag = False
|
||||||
|
try:
|
||||||
|
import concurrent.futures
|
||||||
|
can_threaded_download = True
|
||||||
|
except ImportError:
|
||||||
|
can_threaded_download = False
|
||||||
|
|
||||||
from ..downloader import _get_real_downloader
|
from ..downloader import _get_real_downloader
|
||||||
from .fragment import FragmentFD
|
from .fragment import FragmentFD
|
||||||
@@ -19,12 +24,17 @@ from ..compat import (
|
|||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
parse_m3u8_attributes,
|
parse_m3u8_attributes,
|
||||||
|
sanitize_open,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class HlsFD(FragmentFD):
|
class HlsFD(FragmentFD):
|
||||||
""" A limited implementation that does not require ffmpeg """
|
"""
|
||||||
|
Download segments in a m3u8 manifest. External downloaders can take over
|
||||||
|
the fragment downloads by supporting the 'frag_urls' protocol and
|
||||||
|
re-defining 'supports_manifest' function
|
||||||
|
"""
|
||||||
|
|
||||||
FD_NAME = 'hlsnative'
|
FD_NAME = 'hlsnative'
|
||||||
|
|
||||||
@@ -53,12 +63,15 @@ class HlsFD(FragmentFD):
|
|||||||
UNSUPPORTED_FEATURES += [
|
UNSUPPORTED_FEATURES += [
|
||||||
r'#EXT-X-KEY:METHOD=(?!NONE|AES-128)', # encrypted streams [1]
|
r'#EXT-X-KEY:METHOD=(?!NONE|AES-128)', # encrypted streams [1]
|
||||||
]
|
]
|
||||||
check_results = [not re.search(feature, manifest) for feature in UNSUPPORTED_FEATURES]
|
|
||||||
|
def check_results():
|
||||||
|
yield not info_dict.get('is_live')
|
||||||
is_aes128_enc = '#EXT-X-KEY:METHOD=AES-128' in manifest
|
is_aes128_enc = '#EXT-X-KEY:METHOD=AES-128' in manifest
|
||||||
check_results.append(with_crypto or not is_aes128_enc)
|
yield with_crypto or not is_aes128_enc
|
||||||
check_results.append(not (is_aes128_enc and r'#EXT-X-BYTERANGE' in manifest))
|
yield not (is_aes128_enc and r'#EXT-X-BYTERANGE' in manifest)
|
||||||
check_results.append(not info_dict.get('is_live'))
|
for feature in UNSUPPORTED_FEATURES:
|
||||||
return all(check_results)
|
yield not re.search(feature, manifest)
|
||||||
|
return all(check_results())
|
||||||
|
|
||||||
def real_download(self, filename, info_dict):
|
def real_download(self, filename, info_dict):
|
||||||
man_url = info_dict['url']
|
man_url = info_dict['url']
|
||||||
@@ -84,6 +97,8 @@ class HlsFD(FragmentFD):
|
|||||||
return fd.real_download(filename, info_dict)
|
return fd.real_download(filename, info_dict)
|
||||||
|
|
||||||
real_downloader = _get_real_downloader(info_dict, 'frag_urls', self.params, None)
|
real_downloader = _get_real_downloader(info_dict, 'frag_urls', self.params, None)
|
||||||
|
if real_downloader and not real_downloader.supports_manifest(s):
|
||||||
|
real_downloader = None
|
||||||
|
|
||||||
def is_ad_fragment_start(s):
|
def is_ad_fragment_start(s):
|
||||||
return (s.startswith('#ANVATO-SEGMENT-INFO') and 'type=ad' in s
|
return (s.startswith('#ANVATO-SEGMENT-INFO') and 'type=ad' in s
|
||||||
@@ -93,7 +108,7 @@ class HlsFD(FragmentFD):
|
|||||||
return (s.startswith('#ANVATO-SEGMENT-INFO') and 'type=master' in s
|
return (s.startswith('#ANVATO-SEGMENT-INFO') and 'type=master' in s
|
||||||
or s.startswith('#UPLYNK-SEGMENT') and s.endswith(',segment'))
|
or s.startswith('#UPLYNK-SEGMENT') and s.endswith(',segment'))
|
||||||
|
|
||||||
fragment_urls = []
|
fragments = []
|
||||||
|
|
||||||
media_frags = 0
|
media_frags = 0
|
||||||
ad_frags = 0
|
ad_frags = 0
|
||||||
@@ -136,14 +151,12 @@ class HlsFD(FragmentFD):
|
|||||||
i = 0
|
i = 0
|
||||||
media_sequence = 0
|
media_sequence = 0
|
||||||
decrypt_info = {'METHOD': 'NONE'}
|
decrypt_info = {'METHOD': 'NONE'}
|
||||||
key_list = []
|
|
||||||
byte_range = {}
|
byte_range = {}
|
||||||
discontinuity_count = 0
|
discontinuity_count = 0
|
||||||
frag_index = 0
|
frag_index = 0
|
||||||
ad_frag_next = False
|
ad_frag_next = False
|
||||||
for line in s.splitlines():
|
for line in s.splitlines():
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
download_frag = False
|
|
||||||
if line:
|
if line:
|
||||||
if not line.startswith('#'):
|
if not line.startswith('#'):
|
||||||
if format_index and discontinuity_count != format_index:
|
if format_index and discontinuity_count != format_index:
|
||||||
@@ -160,10 +173,13 @@ class HlsFD(FragmentFD):
|
|||||||
if extra_query:
|
if extra_query:
|
||||||
frag_url = update_url_query(frag_url, extra_query)
|
frag_url = update_url_query(frag_url, extra_query)
|
||||||
|
|
||||||
if real_downloader:
|
fragments.append({
|
||||||
fragment_urls.append(frag_url)
|
'frag_index': frag_index,
|
||||||
continue
|
'url': frag_url,
|
||||||
download_frag = True
|
'decrypt_info': decrypt_info,
|
||||||
|
'byte_range': byte_range,
|
||||||
|
'media_sequence': media_sequence,
|
||||||
|
})
|
||||||
|
|
||||||
elif line.startswith('#EXT-X-MAP'):
|
elif line.startswith('#EXT-X-MAP'):
|
||||||
if format_index and discontinuity_count != format_index:
|
if format_index and discontinuity_count != format_index:
|
||||||
@@ -180,9 +196,14 @@ class HlsFD(FragmentFD):
|
|||||||
else compat_urlparse.urljoin(man_url, map_info.get('URI')))
|
else compat_urlparse.urljoin(man_url, map_info.get('URI')))
|
||||||
if extra_query:
|
if extra_query:
|
||||||
frag_url = update_url_query(frag_url, extra_query)
|
frag_url = update_url_query(frag_url, extra_query)
|
||||||
if real_downloader:
|
|
||||||
fragment_urls.append(frag_url)
|
fragments.append({
|
||||||
continue
|
'frag_index': frag_index,
|
||||||
|
'url': frag_url,
|
||||||
|
'decrypt_info': decrypt_info,
|
||||||
|
'byte_range': byte_range,
|
||||||
|
'media_sequence': media_sequence
|
||||||
|
})
|
||||||
|
|
||||||
if map_info.get('BYTERANGE'):
|
if map_info.get('BYTERANGE'):
|
||||||
splitted_byte_range = map_info.get('BYTERANGE').split('@')
|
splitted_byte_range = map_info.get('BYTERANGE').split('@')
|
||||||
@@ -191,7 +212,6 @@ class HlsFD(FragmentFD):
|
|||||||
'start': sub_range_start,
|
'start': sub_range_start,
|
||||||
'end': sub_range_start + int(splitted_byte_range[0]),
|
'end': sub_range_start + int(splitted_byte_range[0]),
|
||||||
}
|
}
|
||||||
download_frag = True
|
|
||||||
|
|
||||||
elif line.startswith('#EXT-X-KEY'):
|
elif line.startswith('#EXT-X-KEY'):
|
||||||
decrypt_url = decrypt_info.get('URI')
|
decrypt_url = decrypt_info.get('URI')
|
||||||
@@ -206,9 +226,6 @@ class HlsFD(FragmentFD):
|
|||||||
decrypt_info['URI'] = update_url_query(decrypt_info['URI'], extra_query)
|
decrypt_info['URI'] = update_url_query(decrypt_info['URI'], extra_query)
|
||||||
if decrypt_url != decrypt_info['URI']:
|
if decrypt_url != decrypt_info['URI']:
|
||||||
decrypt_info['KEY'] = None
|
decrypt_info['KEY'] = None
|
||||||
key_data = decrypt_info.copy()
|
|
||||||
key_data['INDEX'] = frag_index
|
|
||||||
key_list.append(key_data)
|
|
||||||
|
|
||||||
elif line.startswith('#EXT-X-MEDIA-SEQUENCE'):
|
elif line.startswith('#EXT-X-MEDIA-SEQUENCE'):
|
||||||
media_sequence = int(line[22:])
|
media_sequence = int(line[22:])
|
||||||
@@ -225,8 +242,33 @@ class HlsFD(FragmentFD):
|
|||||||
ad_frag_next = False
|
ad_frag_next = False
|
||||||
elif line.startswith('#EXT-X-DISCONTINUITY'):
|
elif line.startswith('#EXT-X-DISCONTINUITY'):
|
||||||
discontinuity_count += 1
|
discontinuity_count += 1
|
||||||
|
i += 1
|
||||||
|
media_sequence += 1
|
||||||
|
|
||||||
|
# We only download the first fragment during the test
|
||||||
|
if test:
|
||||||
|
fragments = [fragments[0] if fragments else None]
|
||||||
|
|
||||||
|
if real_downloader:
|
||||||
|
info_copy = info_dict.copy()
|
||||||
|
info_copy['fragments'] = fragments
|
||||||
|
fd = real_downloader(self.ydl, self.params)
|
||||||
|
# TODO: Make progress updates work without hooking twice
|
||||||
|
# for ph in self._progress_hooks:
|
||||||
|
# fd.add_progress_hook(ph)
|
||||||
|
success = fd.real_download(filename, info_copy)
|
||||||
|
if not success:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
def download_fragment(fragment):
|
||||||
|
frag_index = fragment['frag_index']
|
||||||
|
frag_url = fragment['url']
|
||||||
|
decrypt_info = fragment['decrypt_info']
|
||||||
|
byte_range = fragment['byte_range']
|
||||||
|
media_sequence = fragment['media_sequence']
|
||||||
|
|
||||||
|
ctx['fragment_index'] = frag_index
|
||||||
|
|
||||||
if download_frag:
|
|
||||||
count = 0
|
count = 0
|
||||||
headers = info_dict.get('http_headers', {})
|
headers = info_dict.get('http_headers', {})
|
||||||
if byte_range:
|
if byte_range:
|
||||||
@@ -236,7 +278,7 @@ class HlsFD(FragmentFD):
|
|||||||
success, frag_content = self._download_fragment(
|
success, frag_content = self._download_fragment(
|
||||||
ctx, frag_url, info_dict, headers)
|
ctx, frag_url, info_dict, headers)
|
||||||
if not success:
|
if not success:
|
||||||
return False
|
return False, frag_index
|
||||||
break
|
break
|
||||||
except compat_urllib_error.HTTPError as err:
|
except compat_urllib_error.HTTPError as err:
|
||||||
# Unavailable (possibly temporary) fragments may be served.
|
# Unavailable (possibly temporary) fragments may be served.
|
||||||
@@ -247,14 +289,7 @@ class HlsFD(FragmentFD):
|
|||||||
if count <= fragment_retries:
|
if count <= fragment_retries:
|
||||||
self.report_retry_fragment(err, frag_index, count, fragment_retries)
|
self.report_retry_fragment(err, frag_index, count, fragment_retries)
|
||||||
if count > fragment_retries:
|
if count > fragment_retries:
|
||||||
if skip_unavailable_fragments:
|
return False, frag_index
|
||||||
i += 1
|
|
||||||
media_sequence += 1
|
|
||||||
self.report_skip_fragment(frag_index)
|
|
||||||
continue
|
|
||||||
self.report_error(
|
|
||||||
'giving up after %s fragment retries' % fragment_retries)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if decrypt_info['METHOD'] == 'AES-128':
|
if decrypt_info['METHOD'] == 'AES-128':
|
||||||
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', media_sequence)
|
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', media_sequence)
|
||||||
@@ -266,24 +301,65 @@ class HlsFD(FragmentFD):
|
|||||||
if not test:
|
if not test:
|
||||||
frag_content = AES.new(
|
frag_content = AES.new(
|
||||||
decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(frag_content)
|
decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(frag_content)
|
||||||
self._append_fragment(ctx, frag_content)
|
|
||||||
# We only download the first fragment during the test
|
|
||||||
if test:
|
|
||||||
break
|
|
||||||
i += 1
|
|
||||||
media_sequence += 1
|
|
||||||
|
|
||||||
if real_downloader:
|
return frag_content, frag_index
|
||||||
info_copy = info_dict.copy()
|
|
||||||
info_copy['url_list'] = fragment_urls
|
def append_fragment(frag_content, frag_index):
|
||||||
info_copy['key_list'] = key_list
|
if frag_content:
|
||||||
fd = real_downloader(self.ydl, self.params)
|
fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], frag_index)
|
||||||
# TODO: Make progress updates work without hooking twice
|
try:
|
||||||
# for ph in self._progress_hooks:
|
file, frag_sanitized = sanitize_open(fragment_filename, 'rb')
|
||||||
# fd.add_progress_hook(ph)
|
ctx['fragment_filename_sanitized'] = frag_sanitized
|
||||||
success = fd.real_download(filename, info_copy)
|
file.close()
|
||||||
if not success:
|
self._append_fragment(ctx, frag_content)
|
||||||
|
return True
|
||||||
|
except FileNotFoundError:
|
||||||
|
if skip_unavailable_fragments:
|
||||||
|
self.report_skip_fragment(frag_index)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.report_error(
|
||||||
|
'fragment %s not found, unable to continue' % frag_index)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
if skip_unavailable_fragments:
|
||||||
|
self.report_skip_fragment(frag_index)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.report_error(
|
||||||
|
'fragment %s not found, unable to continue' % frag_index)
|
||||||
|
return False
|
||||||
|
|
||||||
|
max_workers = self.params.get('concurrent_fragment_downloads', 1)
|
||||||
|
if can_threaded_download and max_workers > 1:
|
||||||
|
self.report_warning('The download speed shown is only of one thread. This is a known issue')
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
|
||||||
|
futures = [pool.submit(download_fragment, fragment) for fragment in fragments]
|
||||||
|
# timeout must be 0 to return instantly
|
||||||
|
done, not_done = concurrent.futures.wait(futures, timeout=0)
|
||||||
|
try:
|
||||||
|
while not_done:
|
||||||
|
# Check every 1 second for KeyboardInterrupt
|
||||||
|
freshly_done, not_done = concurrent.futures.wait(not_done, timeout=1)
|
||||||
|
done |= freshly_done
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
for future in not_done:
|
||||||
|
future.cancel()
|
||||||
|
# timeout must be none to cancel
|
||||||
|
concurrent.futures.wait(not_done, timeout=None)
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
results = [future.result() for future in futures]
|
||||||
|
|
||||||
|
for frag_content, frag_index in results:
|
||||||
|
result = append_fragment(frag_content, frag_index)
|
||||||
|
if not result:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
for fragment in fragments:
|
||||||
|
frag_content, frag_index = download_fragment(fragment)
|
||||||
|
result = append_fragment(frag_content, frag_index)
|
||||||
|
if not result:
|
||||||
|
return False
|
||||||
|
|
||||||
self._finish_frag_download(ctx)
|
self._finish_frag_download(ctx)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class ApplePodcastsIE(InfoExtractor):
|
|||||||
ember_data = self._parse_json(self._search_regex(
|
ember_data = self._parse_json(self._search_regex(
|
||||||
r'id="shoebox-ember-data-store"[^>]*>\s*({.+?})\s*<',
|
r'id="shoebox-ember-data-store"[^>]*>\s*({.+?})\s*<',
|
||||||
webpage, 'ember data'), episode_id)
|
webpage, 'ember data'), episode_id)
|
||||||
|
ember_data = ember_data.get(episode_id) or ember_data
|
||||||
episode = ember_data['data']['attributes']
|
episode = ember_data['data']['attributes']
|
||||||
description = episode.get('description') or {}
|
description = episode.get('description') or {}
|
||||||
|
|
||||||
|
|||||||
37
yt_dlp/extractor/bandaichannel.py
Normal file
37
yt_dlp/extractor/bandaichannel.py
Normal 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'])
|
||||||
@@ -49,6 +49,7 @@ class BandcampIE(InfoExtractor):
|
|||||||
'uploader': 'Ben Prunty',
|
'uploader': 'Ben Prunty',
|
||||||
'timestamp': 1396508491,
|
'timestamp': 1396508491,
|
||||||
'upload_date': '20140403',
|
'upload_date': '20140403',
|
||||||
|
'release_timestamp': 1396483200,
|
||||||
'release_date': '20140403',
|
'release_date': '20140403',
|
||||||
'duration': 260.877,
|
'duration': 260.877,
|
||||||
'track': 'Lanius (Battle)',
|
'track': 'Lanius (Battle)',
|
||||||
@@ -69,6 +70,7 @@ class BandcampIE(InfoExtractor):
|
|||||||
'uploader': 'Mastodon',
|
'uploader': 'Mastodon',
|
||||||
'timestamp': 1322005399,
|
'timestamp': 1322005399,
|
||||||
'upload_date': '20111122',
|
'upload_date': '20111122',
|
||||||
|
'release_timestamp': 1076112000,
|
||||||
'release_date': '20040207',
|
'release_date': '20040207',
|
||||||
'duration': 120.79,
|
'duration': 120.79,
|
||||||
'track': 'Hail to Fire',
|
'track': 'Hail to Fire',
|
||||||
@@ -197,7 +199,7 @@ class BandcampIE(InfoExtractor):
|
|||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
'uploader': artist,
|
'uploader': artist,
|
||||||
'timestamp': timestamp,
|
'timestamp': timestamp,
|
||||||
'release_date': unified_strdate(tralbum.get('album_release_date')),
|
'release_timestamp': unified_timestamp(tralbum.get('album_release_date')),
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
'track': track,
|
'track': track,
|
||||||
'track_number': track_number,
|
'track_number': track_number,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
cid = js['result']['cid']
|
cid = js['result']['cid']
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
'Referer': url
|
'Referer': url
|
||||||
}
|
}
|
||||||
headers.update(self.geo_verification_headers())
|
headers.update(self.geo_verification_headers())
|
||||||
@@ -255,10 +256,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 +263,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 +559,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,10 +27,10 @@ class CBSBaseIE(ThePlatformFeedIE):
|
|||||||
|
|
||||||
|
|
||||||
class CBSIE(CBSBaseIE):
|
class CBSIE(CBSBaseIE):
|
||||||
_VALID_URL = r'(?:cbs:|https?://(?:www\.)?(?:cbs\.com/shows/[^/]+/video|colbertlateshow\.com/(?:video|podcasts))/)(?P<id>[\w-]+)'
|
_VALID_URL = r'(?:cbs:|https?://(?:www\.)?(?:(?:cbs|paramountplus)\.com/shows/[^/]+/video|colbertlateshow\.com/(?:video|podcasts))/)(?P<id>[\w-]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.cbs.com/shows/garth-brooks/video/_u7W953k6la293J7EPTd9oHkSPs6Xn6_/connect-chat-feat-garth-brooks/',
|
'url': 'https://www.cbs.com/shows/garth-brooks/video/_u7W953k6la293J7EPTd9oHkSPs6Xn6_/connect-chat-feat-garth-brooks/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '_u7W953k6la293J7EPTd9oHkSPs6Xn6_',
|
'id': '_u7W953k6la293J7EPTd9oHkSPs6Xn6_',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -52,16 +52,19 @@ class CBSIE(CBSBaseIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://www.colbertlateshow.com/podcasts/dYSwjqPs_X1tvbV_P2FcPWRa_qT6akTC/in-the-bad-room-with-stephen/',
|
'url': 'http://www.colbertlateshow.com/podcasts/dYSwjqPs_X1tvbV_P2FcPWRa_qT6akTC/in-the-bad-room-with-stephen/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.paramountplus.com/shows/all-rise/video/QmR1WhNkh1a_IrdHZrbcRklm176X_rVc/all-rise-space/',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _extract_video_info(self, content_id, site='cbs', mpx_acc=2198311517):
|
def _extract_video_info(self, content_id, site='cbs', mpx_acc=2198311517):
|
||||||
items_data = self._download_xml(
|
items_data = self._download_xml(
|
||||||
'http://can.cbs.com/thunder/player/videoPlayerService.php',
|
'https://can.cbs.com/thunder/player/videoPlayerService.php',
|
||||||
content_id, query={'partner': site, 'contentId': content_id})
|
content_id, query={'partner': site, 'contentId': content_id})
|
||||||
video_data = xpath_element(items_data, './/item')
|
video_data = xpath_element(items_data, './/item')
|
||||||
title = xpath_text(video_data, 'videoTitle', 'title') or xpath_text(video_data, 'videotitle', 'title')
|
title = xpath_text(video_data, 'videoTitle', 'title') or xpath_text(video_data, 'videotitle', 'title')
|
||||||
tp_path = 'dJ5BDC/media/guid/%d/%s' % (mpx_acc, content_id)
|
tp_path = 'dJ5BDC/media/guid/%d/%s' % (mpx_acc, content_id)
|
||||||
tp_release_url = 'http://link.theplatform.com/s/' + tp_path
|
tp_release_url = 'https://link.theplatform.com/s/' + tp_path
|
||||||
|
|
||||||
asset_types = []
|
asset_types = []
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
|
|||||||
@@ -231,8 +231,9 @@ class InfoExtractor(object):
|
|||||||
uploader: Full name of the video uploader.
|
uploader: Full name of the video uploader.
|
||||||
license: License name the video is licensed under.
|
license: License name the video is licensed under.
|
||||||
creator: The creator of the video.
|
creator: The creator of the video.
|
||||||
|
release_timestamp: UNIX timestamp of the moment the video was released.
|
||||||
release_date: The date (YYYYMMDD) when the video was released.
|
release_date: The date (YYYYMMDD) when the video was released.
|
||||||
timestamp: UNIX timestamp of the moment the video became available.
|
timestamp: UNIX timestamp of the moment the video was uploaded
|
||||||
upload_date: Video upload date (YYYYMMDD).
|
upload_date: Video upload date (YYYYMMDD).
|
||||||
If not explicitly set, calculated from timestamp.
|
If not explicitly set, calculated from timestamp.
|
||||||
uploader_id: Nickname or id of the video uploader.
|
uploader_id: Nickname or id of the video uploader.
|
||||||
@@ -264,6 +265,7 @@ class InfoExtractor(object):
|
|||||||
properties (all but one of text or html optional):
|
properties (all but one of text or html optional):
|
||||||
* "author" - human-readable name of the comment author
|
* "author" - human-readable name of the comment author
|
||||||
* "author_id" - user ID of the comment author
|
* "author_id" - user ID of the comment author
|
||||||
|
* "author_thumbnail" - The thumbnail of the comment author
|
||||||
* "id" - Comment ID
|
* "id" - Comment ID
|
||||||
* "html" - Comment as HTML
|
* "html" - Comment as HTML
|
||||||
* "text" - Plain text of the comment
|
* "text" - Plain text of the comment
|
||||||
@@ -271,6 +273,12 @@ class InfoExtractor(object):
|
|||||||
* "parent" - ID of the comment this one is replying to.
|
* "parent" - ID of the comment this one is replying to.
|
||||||
Set to "root" to indicate that this is a
|
Set to "root" to indicate that this is a
|
||||||
comment to the original video.
|
comment to the original video.
|
||||||
|
* "like_count" - Number of positive ratings of the comment
|
||||||
|
* "dislike_count" - Number of negative ratings of the comment
|
||||||
|
* "is_favorited" - Whether the comment is marked as
|
||||||
|
favorite by the video uploader
|
||||||
|
* "author_is_uploader" - Whether the comment is made by
|
||||||
|
the video uploader
|
||||||
age_limit: Age restriction for the video, as an integer (years)
|
age_limit: Age restriction for the video, as an integer (years)
|
||||||
webpage_url: The URL to the video webpage, if given to yt-dlp it
|
webpage_url: The URL to the video webpage, if given to yt-dlp it
|
||||||
should allow to get the same result again. (It will be set
|
should allow to get the same result again. (It will be set
|
||||||
@@ -294,6 +302,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 +622,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:
|
||||||
@@ -1833,8 +1857,9 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
def _extract_m3u8_formats(self, m3u8_url, video_id, ext=None,
|
def _extract_m3u8_formats(self, m3u8_url, video_id, ext=None,
|
||||||
entry_protocol='m3u8', preference=None, quality=None,
|
entry_protocol='m3u8', preference=None, quality=None,
|
||||||
m3u8_id=None, live=False, note=None, errnote=None,
|
m3u8_id=None, note=None, errnote=None,
|
||||||
fatal=True, data=None, headers={}, query={}):
|
fatal=True, live=False, data=None, headers={},
|
||||||
|
query={}):
|
||||||
res = self._download_webpage_handle(
|
res = self._download_webpage_handle(
|
||||||
m3u8_url, video_id,
|
m3u8_url, video_id,
|
||||||
note=note or 'Downloading m3u8 information',
|
note=note or 'Downloading m3u8 information',
|
||||||
@@ -1888,13 +1913,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 +1956,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 +1994,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,13 +2056,14 @@ 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 frmt in playlist_formats:
|
||||||
format_id = []
|
format_id = []
|
||||||
if m3u8_id:
|
if m3u8_id:
|
||||||
format_id.append(m3u8_id)
|
format_id.append(m3u8_id)
|
||||||
format_index = format.get('index')
|
format_index = frmt.get('index')
|
||||||
stream_name = build_stream_name()
|
stream_name = build_stream_name()
|
||||||
# Bandwidth of live streams may differ over time thus making
|
# Bandwidth of live streams may differ over time thus making
|
||||||
# format_id unpredictable. So it's better to keep provided
|
# format_id unpredictable. So it's better to keep provided
|
||||||
@@ -2088,6 +2118,8 @@ class InfoExtractor(object):
|
|||||||
# TODO: update acodec for audio only formats with
|
# TODO: update acodec for audio only formats with
|
||||||
# the same GROUP-ID
|
# the same GROUP-ID
|
||||||
f['acodec'] = 'none'
|
f['acodec'] = 'none'
|
||||||
|
if not f.get('ext'):
|
||||||
|
f['ext'] = 'm4a' if f.get('vcodec') == 'none' else 'mp4'
|
||||||
formats.append(f)
|
formats.append(f)
|
||||||
|
|
||||||
# for DailyMotion
|
# for DailyMotion
|
||||||
|
|||||||
@@ -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': '45918',
|
'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.3sat.de/gesellschaft/schweizweit/waidmannsheil-100.html',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '140913_sendung_schweizweit',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Waidmannsheil',
|
'title': 'Waidmannsheil',
|
||||||
'description': 'md5:cce00ca1d70e21425e72c86a98a56817',
|
'description': 'md5:cce00ca1d70e21425e72c86a98a56817',
|
||||||
'uploader': 'SCHWEIZWEIT',
|
'timestamp': 1410623100,
|
||||||
'uploader_id': '100000210',
|
|
||||||
'upload_date': '20140913'
|
'upload_date': '20140913'
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True, # m3u8 downloads
|
'skip_download': True,
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
{
|
# Same as https://www.zdf.de/filme/filme-sonstige/der-hauptmann-112.html
|
||||||
'url': 'http://www.3sat.de/mediathek/mediathek.php?mode=play&obj=51066',
|
'url': 'https://www.3sat.de/film/spielfilm/der-hauptmann-100.html',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
},
|
}, {
|
||||||
]
|
# 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',
|
||||||
def _parse_smil_formats(self, smil, smil_url, video_id, namespace=None, f4m_params=None, transform_rtmp_url=None):
|
'only_matching': True,
|
||||||
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):
|
|
||||||
video_id = self._match_id(url)
|
|
||||||
details_url = 'http://www.3sat.de/mediathek/xmlservice/web/beitragsDetails?id=%s' % video_id
|
|
||||||
return self.extract_from_xml_url(video_id, details_url)
|
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -449,10 +450,7 @@ from .gamestar import GameStarIE
|
|||||||
from .gaskrank import GaskrankIE
|
from .gaskrank import GaskrankIE
|
||||||
from .gazeta import GazetaIE
|
from .gazeta import GazetaIE
|
||||||
from .gdcvault import GDCVaultIE
|
from .gdcvault import GDCVaultIE
|
||||||
from .gedi import (
|
from .gedidigital import GediDigitalIE
|
||||||
GediIE,
|
|
||||||
GediEmbedsIE,
|
|
||||||
)
|
|
||||||
from .generic import GenericIE
|
from .generic import GenericIE
|
||||||
from .gfycat import GfycatIE
|
from .gfycat import GfycatIE
|
||||||
from .giantbomb import GiantBombIE
|
from .giantbomb import GiantBombIE
|
||||||
@@ -734,9 +732,12 @@ from .mtv import (
|
|||||||
MTVServicesEmbeddedIE,
|
MTVServicesEmbeddedIE,
|
||||||
MTVDEIE,
|
MTVDEIE,
|
||||||
MTVJapanIE,
|
MTVJapanIE,
|
||||||
|
MTVItaliaIE,
|
||||||
|
MTVItaliaProgrammaIE,
|
||||||
)
|
)
|
||||||
from .muenchentv import MuenchenTVIE
|
from .muenchentv import MuenchenTVIE
|
||||||
from .mwave import MwaveIE, MwaveMeetGreetIE
|
from .mwave import MwaveIE, MwaveMeetGreetIE
|
||||||
|
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
|
||||||
@@ -952,6 +953,7 @@ from .plays import PlaysTVIE
|
|||||||
from .playtvak import PlaytvakIE
|
from .playtvak import PlaytvakIE
|
||||||
from .playvid import PlayvidIE
|
from .playvid import PlayvidIE
|
||||||
from .playwire import PlaywireIE
|
from .playwire import PlaywireIE
|
||||||
|
from .plutotv import PlutoTVIE
|
||||||
from .pluralsight import (
|
from .pluralsight import (
|
||||||
PluralsightIE,
|
PluralsightIE,
|
||||||
PluralsightCourseIE,
|
PluralsightCourseIE,
|
||||||
@@ -1558,6 +1560,7 @@ from .weibo import (
|
|||||||
WeiboMobileIE
|
WeiboMobileIE
|
||||||
)
|
)
|
||||||
from .weiqitv import WeiqiTVIE
|
from .weiqitv import WeiqiTVIE
|
||||||
|
from .wimtv import WimTVIE
|
||||||
from .wistia import (
|
from .wistia import (
|
||||||
WistiaIE,
|
WistiaIE,
|
||||||
WistiaPlaylistIE,
|
WistiaPlaylistIE,
|
||||||
@@ -1667,5 +1670,6 @@ from .zdf import ZDFIE, ZDFChannelIE
|
|||||||
from .zhihu import ZhihuIE
|
from .zhihu import ZhihuIE
|
||||||
from .zingmp3 import ZingMp3IE
|
from .zingmp3 import ZingMp3IE
|
||||||
from .zee5 import Zee5IE
|
from .zee5 import Zee5IE
|
||||||
|
from .zee5 import Zee5SeriesIE
|
||||||
from .zoom import ZoomIE
|
from .zoom import ZoomIE
|
||||||
from .zype import ZypeIE
|
from .zype import ZypeIE
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class FujiTVFODPlus7IE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
formats = self._extract_m3u8_formats(
|
formats = self._extract_m3u8_formats(
|
||||||
self._BASE_URL + 'abr/pc_html5/%s.m3u8' % video_id, video_id)
|
self._BASE_URL + 'abr/pc_html5/%s.m3u8' % video_id, video_id, 'mp4')
|
||||||
for f in formats:
|
for f in formats:
|
||||||
wh = self._BITRATE_MAP.get(f.get('tbr'))
|
wh = self._BITRATE_MAP.get(f.get('tbr'))
|
||||||
if wh:
|
if wh:
|
||||||
|
|||||||
@@ -1,266 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..compat import compat_str
|
|
||||||
from ..utils import (
|
|
||||||
base_url,
|
|
||||||
url_basename,
|
|
||||||
urljoin,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class GediBaseIE(InfoExtractor):
|
|
||||||
@staticmethod
|
|
||||||
def _clean_audio_fmts(formats):
|
|
||||||
unique_formats = []
|
|
||||||
for f in formats:
|
|
||||||
if 'acodec' in f:
|
|
||||||
unique_formats.append(f)
|
|
||||||
formats[:] = unique_formats
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id = self._match_id(url)
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
|
||||||
player_data = re.findall(
|
|
||||||
r'PlayerFactory\.setParam\(\'(?P<type>.+?)\',\s*\'(?P<name>.+?)\',\s*\'(?P<val>.+?)\'\);',
|
|
||||||
webpage)
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
audio_fmts = []
|
|
||||||
hls_fmts = []
|
|
||||||
http_fmts = []
|
|
||||||
title = ''
|
|
||||||
thumb = ''
|
|
||||||
|
|
||||||
fmt_reg = r'(?P<t>video|audio)-(?P<p>rrtv|hls)-(?P<h>[\w\d]+)(?:-(?P<br>[\w\d]+))?$'
|
|
||||||
br_reg = r'video-rrtv-(?P<br>\d+)-'
|
|
||||||
|
|
||||||
for t, n, v in player_data:
|
|
||||||
if t == 'format':
|
|
||||||
m = re.match(fmt_reg, n)
|
|
||||||
if m:
|
|
||||||
# audio formats
|
|
||||||
if m.group('t') == 'audio':
|
|
||||||
if m.group('p') == 'hls':
|
|
||||||
audio_fmts.extend(self._extract_m3u8_formats(
|
|
||||||
v, video_id, 'm4a', m3u8_id='hls', fatal=False))
|
|
||||||
elif m.group('p') == 'rrtv':
|
|
||||||
audio_fmts.append({
|
|
||||||
'format_id': 'mp3',
|
|
||||||
'url': v,
|
|
||||||
'tbr': 128,
|
|
||||||
'ext': 'mp3',
|
|
||||||
'vcodec': 'none',
|
|
||||||
'acodec': 'mp3',
|
|
||||||
})
|
|
||||||
|
|
||||||
# video formats
|
|
||||||
elif m.group('t') == 'video':
|
|
||||||
# hls manifest video
|
|
||||||
if m.group('p') == 'hls':
|
|
||||||
hls_fmts.extend(self._extract_m3u8_formats(
|
|
||||||
v, video_id, 'mp4', m3u8_id='hls', fatal=False))
|
|
||||||
# direct mp4 video
|
|
||||||
elif m.group('p') == 'rrtv':
|
|
||||||
if not m.group('br'):
|
|
||||||
mm = re.search(br_reg, v)
|
|
||||||
http_fmts.append({
|
|
||||||
'format_id': 'https-' + m.group('h'),
|
|
||||||
'protocol': 'https',
|
|
||||||
'url': v,
|
|
||||||
'tbr': int(m.group('br')) if m.group('br') else
|
|
||||||
(int(mm.group('br')) if mm.group('br') else 0),
|
|
||||||
'height': int(m.group('h'))
|
|
||||||
})
|
|
||||||
|
|
||||||
elif t == 'param':
|
|
||||||
if n == 'videotitle':
|
|
||||||
title = v
|
|
||||||
if n == 'image_full_play':
|
|
||||||
thumb = v
|
|
||||||
|
|
||||||
title = self._og_search_title(webpage) if title == '' else title
|
|
||||||
|
|
||||||
# clean weird char
|
|
||||||
title = compat_str(title).encode('utf8', 'replace').replace(b'\xc3\x82', b'').decode('utf8', 'replace')
|
|
||||||
|
|
||||||
if audio_fmts:
|
|
||||||
self._clean_audio_fmts(audio_fmts)
|
|
||||||
self._sort_formats(audio_fmts)
|
|
||||||
if hls_fmts:
|
|
||||||
self._sort_formats(hls_fmts)
|
|
||||||
if http_fmts:
|
|
||||||
self._sort_formats(http_fmts)
|
|
||||||
|
|
||||||
formats.extend(audio_fmts)
|
|
||||||
formats.extend(hls_fmts)
|
|
||||||
formats.extend(http_fmts)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'description': self._html_search_meta('twitter:description', webpage),
|
|
||||||
'thumbnail': thumb,
|
|
||||||
'formats': formats,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class GediIE(GediBaseIE):
|
|
||||||
_VALID_URL = r'''(?x)https?://video\.
|
|
||||||
(?:
|
|
||||||
(?:espresso\.)?repubblica
|
|
||||||
|lastampa
|
|
||||||
|huffingtonpost
|
|
||||||
|ilsecoloxix
|
|
||||||
|iltirreno
|
|
||||||
|messaggeroveneto
|
|
||||||
|ilpiccolo
|
|
||||||
|gazzettadimantova
|
|
||||||
|mattinopadova
|
|
||||||
|laprovinciapavese
|
|
||||||
|tribunatreviso
|
|
||||||
|nuovavenezia
|
|
||||||
|gazzettadimodena
|
|
||||||
|lanuovaferrara
|
|
||||||
|corrierealpi
|
|
||||||
|lasentinella
|
|
||||||
)
|
|
||||||
(?:\.gelocal)?\.it/(?!embed/).+?/(?P<id>[\d/]+)(?:\?|\&|$)'''
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://video.lastampa.it/politica/il-paradosso-delle-regionali-la-lega-vince-ma-sembra-aver-perso/121559/121683',
|
|
||||||
'md5': '84658d7fb9e55a6e57ecc77b73137494',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '121559/121683',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Il paradosso delle Regionali: ecco perché la Lega vince ma sembra aver perso',
|
|
||||||
'description': 'md5:de7f4d6eaaaf36c153b599b10f8ce7ca',
|
|
||||||
'thumbnail': r're:^https://www\.repstatic\.it/video/photo/.+?-thumb-social-play\.jpg$',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://video.repubblica.it/motori/record-della-pista-a-spa-francorchamps-la-pagani-huayra-roadster-bc-stupisce/367415/367963',
|
|
||||||
'md5': 'e763b94b7920799a0e0e23ffefa2d157',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '367415/367963',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Record della pista a Spa Francorchamps, la Pagani Huayra Roadster BC stupisce',
|
|
||||||
'description': 'md5:5deb503cefe734a3eb3f07ed74303920',
|
|
||||||
'thumbnail': r're:^https://www\.repstatic\.it/video/photo/.+?-thumb-social-play\.jpg$',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://video.ilsecoloxix.it/sport/cassani-e-i-brividi-azzurri-ai-mondiali-di-imola-qui-mi-sono-innamorato-del-ciclismo-da-ragazzino-incredibile-tornarci-da-ct/66184/66267',
|
|
||||||
'md5': 'e48108e97b1af137d22a8469f2019057',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '66184/66267',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Cassani e i brividi azzurri ai Mondiali di Imola: \\"Qui mi sono innamorato del ciclismo da ragazzino, incredibile tornarci da ct\\"',
|
|
||||||
'description': 'md5:fc9c50894f70a2469bb9b54d3d0a3d3b',
|
|
||||||
'thumbnail': r're:^https://www\.repstatic\.it/video/photo/.+?-thumb-social-play\.jpg$',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://video.iltirreno.gelocal.it/sport/dentro-la-notizia-ferrari-cosa-succede-a-maranello/141059/142723',
|
|
||||||
'md5': 'a6e39f3bdc1842bbd92abbbbef230817',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '141059/142723',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Dentro la notizia - Ferrari, cosa succede a Maranello',
|
|
||||||
'description': 'md5:9907d65b53765681fa3a0b3122617c1f',
|
|
||||||
'thumbnail': r're:^https://www\.repstatic\.it/video/photo/.+?-thumb-social-play\.jpg$',
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
|
|
||||||
|
|
||||||
class GediEmbedsIE(GediBaseIE):
|
|
||||||
_VALID_URL = r'''(?x)https?://video\.
|
|
||||||
(?:
|
|
||||||
(?:espresso\.)?repubblica
|
|
||||||
|lastampa
|
|
||||||
|huffingtonpost
|
|
||||||
|ilsecoloxix
|
|
||||||
|iltirreno
|
|
||||||
|messaggeroveneto
|
|
||||||
|ilpiccolo
|
|
||||||
|gazzettadimantova
|
|
||||||
|mattinopadova
|
|
||||||
|laprovinciapavese
|
|
||||||
|tribunatreviso
|
|
||||||
|nuovavenezia
|
|
||||||
|gazzettadimodena
|
|
||||||
|lanuovaferrara
|
|
||||||
|corrierealpi
|
|
||||||
|lasentinella
|
|
||||||
)
|
|
||||||
(?:\.gelocal)?\.it/embed/.+?/(?P<id>[\d/]+)(?:\?|\&|$)'''
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://video.huffingtonpost.it/embed/politica/cotticelli-non-so-cosa-mi-sia-successo-sto-cercando-di-capire-se-ho-avuto-un-malore/29312/29276?responsive=true&el=video971040871621586700',
|
|
||||||
'md5': 'f4ac23cadfea7fef89bea536583fa7ed',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '29312/29276',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Cotticelli: \\"Non so cosa mi sia successo. Sto cercando di capire se ho avuto un malore\\"',
|
|
||||||
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
|
|
||||||
'thumbnail': r're:^https://www\.repstatic\.it/video/photo/.+?-thumb-social-play\.jpg$',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://video.espresso.repubblica.it/embed/tutti-i-video/01-ted-villa/14772/14870&width=640&height=360',
|
|
||||||
'md5': '0391c2c83c6506581003aaf0255889c0',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '14772/14870',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Festival EMERGENCY, Villa: «La buona informazione aiuta la salute» (14772-14870)',
|
|
||||||
'description': 'md5:2bce954d278248f3c950be355b7c2226',
|
|
||||||
'thumbnail': r're:^https://www\.repstatic\.it/video/photo/.+?-thumb-social-play\.jpg$',
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _sanitize_urls(urls):
|
|
||||||
# add protocol if missing
|
|
||||||
for i, e in enumerate(urls):
|
|
||||||
if e.startswith('//'):
|
|
||||||
urls[i] = 'https:%s' % e
|
|
||||||
# clean iframes urls
|
|
||||||
for i, e in enumerate(urls):
|
|
||||||
urls[i] = urljoin(base_url(e), url_basename(e))
|
|
||||||
return urls
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _extract_urls(webpage):
|
|
||||||
entries = [
|
|
||||||
mobj.group('url')
|
|
||||||
for mobj in re.finditer(r'''(?x)
|
|
||||||
(?:
|
|
||||||
data-frame-src=|
|
|
||||||
<iframe[^\n]+src=
|
|
||||||
)
|
|
||||||
(["'])
|
|
||||||
(?P<url>https?://video\.
|
|
||||||
(?:
|
|
||||||
(?:espresso\.)?repubblica
|
|
||||||
|lastampa
|
|
||||||
|huffingtonpost
|
|
||||||
|ilsecoloxix
|
|
||||||
|iltirreno
|
|
||||||
|messaggeroveneto
|
|
||||||
|ilpiccolo
|
|
||||||
|gazzettadimantova
|
|
||||||
|mattinopadova
|
|
||||||
|laprovinciapavese
|
|
||||||
|tribunatreviso
|
|
||||||
|nuovavenezia
|
|
||||||
|gazzettadimodena
|
|
||||||
|lanuovaferrara
|
|
||||||
|corrierealpi
|
|
||||||
|lasentinella
|
|
||||||
)
|
|
||||||
(?:\.gelocal)?\.it/embed/.+?)
|
|
||||||
\1''', webpage)]
|
|
||||||
return GediEmbedsIE._sanitize_urls(entries)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _extract_url(webpage):
|
|
||||||
urls = GediEmbedsIE._extract_urls(webpage)
|
|
||||||
return urls[0] if urls else None
|
|
||||||
210
yt_dlp/extractor/gedidigital.py
Normal file
210
yt_dlp/extractor/gedidigital.py
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
base_url,
|
||||||
|
determine_ext,
|
||||||
|
int_or_none,
|
||||||
|
url_basename,
|
||||||
|
urljoin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GediDigitalIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'''(?x)(?P<url>(?:https?:)//video\.
|
||||||
|
(?:
|
||||||
|
(?:
|
||||||
|
(?:espresso\.)?repubblica
|
||||||
|
|lastampa
|
||||||
|
|ilsecoloxix
|
||||||
|
|huffingtonpost
|
||||||
|
)|
|
||||||
|
(?:
|
||||||
|
iltirreno
|
||||||
|
|messaggeroveneto
|
||||||
|
|ilpiccolo
|
||||||
|
|gazzettadimantova
|
||||||
|
|mattinopadova
|
||||||
|
|laprovinciapavese
|
||||||
|
|tribunatreviso
|
||||||
|
|nuovavenezia
|
||||||
|
|gazzettadimodena
|
||||||
|
|lanuovaferrara
|
||||||
|
|corrierealpi
|
||||||
|
|lasentinella
|
||||||
|
)\.gelocal
|
||||||
|
)\.it(?:/[^/]+){2,4}/(?P<id>\d+))(?:$|[?&].*)'''
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://video.lastampa.it/politica/il-paradosso-delle-regionali-la-lega-vince-ma-sembra-aver-perso/121559/121683',
|
||||||
|
'md5': '84658d7fb9e55a6e57ecc77b73137494',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '121683',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Il paradosso delle Regionali: ecco perché la Lega vince ma sembra aver perso',
|
||||||
|
'description': 'md5:de7f4d6eaaaf36c153b599b10f8ce7ca',
|
||||||
|
'thumbnail': r're:^https://www\.repstatic\.it/video/photo/.+?-thumb-full-.+?\.jpg$',
|
||||||
|
'duration': 125,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.huffingtonpost.it/embed/politica/cotticelli-non-so-cosa-mi-sia-successo-sto-cercando-di-capire-se-ho-avuto-un-malore/29312/29276?responsive=true&el=video971040871621586700',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.espresso.repubblica.it/embed/tutti-i-video/01-ted-villa/14772/14870&width=640&height=360',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.repubblica.it/motori/record-della-pista-a-spa-francorchamps-la-pagani-huayra-roadster-bc-stupisce/367415/367963',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.ilsecoloxix.it/sport/cassani-e-i-brividi-azzurri-ai-mondiali-di-imola-qui-mi-sono-innamorato-del-ciclismo-da-ragazzino-incredibile-tornarci-da-ct/66184/66267',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.iltirreno.gelocal.it/sport/dentro-la-notizia-ferrari-cosa-succede-a-maranello/141059/142723',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.messaggeroveneto.gelocal.it/locale/maria-giovanna-elmi-covid-vaccino/138155/139268',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.ilpiccolo.gelocal.it/dossier/big-john/dinosauro-big-john-al-via-le-visite-guidate-a-trieste/135226/135751',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.gazzettadimantova.gelocal.it/locale/dal-ponte-visconteo-di-valeggio-l-and-8217sos-dei-ristoratori-aprire-anche-a-cena/137310/137818',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.mattinopadova.gelocal.it/dossier/coronavirus-in-veneto/covid-a-vo-un-anno-dopo-un-cuore-tricolore-per-non-dimenticare/138402/138964',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.laprovinciapavese.gelocal.it/locale/mede-zona-rossa-via-alle-vaccinazioni-per-gli-over-80/137545/138120',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.tribunatreviso.gelocal.it/dossier/coronavirus-in-veneto/ecco-le-prima-vaccinazioni-di-massa-nella-marca/134485/135024',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.nuovavenezia.gelocal.it/locale/camion-troppo-alto-per-il-ponte-ferroviario-perde-il-carico/135734/136266',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.gazzettadimodena.gelocal.it/locale/modena-scoperta-la-proteina-che-predice-il-livello-di-gravita-del-covid/139109/139796',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.lanuovaferrara.gelocal.it/locale/due-bombole-di-gpl-aperte-e-abbandonate-i-vigili-bruciano-il-gas/134391/134957',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.corrierealpi.gelocal.it/dossier/cortina-2021-i-mondiali-di-sci-alpino/mondiali-di-sci-il-timelapse-sulla-splendida-olympia/133760/134331',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.lasentinella.gelocal.it/locale/vestigne-centra-un-auto-e-si-ribalta/138931/139466',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.espresso.repubblica.it/tutti-i-video/01-ted-villa/14772',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sanitize_urls(urls):
|
||||||
|
# add protocol if missing
|
||||||
|
for i, e in enumerate(urls):
|
||||||
|
if e.startswith('//'):
|
||||||
|
urls[i] = 'https:%s' % e
|
||||||
|
# clean iframes urls
|
||||||
|
for i, e in enumerate(urls):
|
||||||
|
urls[i] = urljoin(base_url(e), url_basename(e))
|
||||||
|
return urls
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_urls(webpage):
|
||||||
|
entries = [
|
||||||
|
mobj.group('eurl')
|
||||||
|
for mobj in re.finditer(r'''(?x)
|
||||||
|
(?:
|
||||||
|
data-frame-src=|
|
||||||
|
<iframe[^\n]+src=
|
||||||
|
)
|
||||||
|
(["'])(?P<eurl>%s)\1''' % GediDigitalIE._VALID_URL, webpage)]
|
||||||
|
return GediDigitalIE._sanitize_urls(entries)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_url(webpage):
|
||||||
|
urls = GediDigitalIE._extract_urls(webpage)
|
||||||
|
return urls[0] if urls else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _clean_formats(formats):
|
||||||
|
format_urls = set()
|
||||||
|
clean_formats = []
|
||||||
|
for f in formats:
|
||||||
|
if f['url'] not in format_urls:
|
||||||
|
if f.get('audio_ext') != 'none' and not f.get('acodec'):
|
||||||
|
continue
|
||||||
|
format_urls.add(f['url'])
|
||||||
|
clean_formats.append(f)
|
||||||
|
formats[:] = clean_formats
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
url = re.match(self._VALID_URL, url).group('url')
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
title = self._html_search_meta(
|
||||||
|
['twitter:title', 'og:title'], webpage, fatal=True)
|
||||||
|
player_data = re.findall(
|
||||||
|
r"PlayerFactory\.setParam\('(?P<type>format|param)',\s*'(?P<name>[^']+)',\s*'(?P<val>[^']+)'\);",
|
||||||
|
webpage)
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
duration = thumb = None
|
||||||
|
for t, n, v in player_data:
|
||||||
|
if t == 'format':
|
||||||
|
if n in ('video-hds-vod-ec', 'video-hls-vod-ec', 'video-viralize', 'video-youtube-pfp'):
|
||||||
|
continue
|
||||||
|
elif n.endswith('-vod-ak'):
|
||||||
|
formats.extend(self._extract_akamai_formats(
|
||||||
|
v, video_id, {'http': 'media.gedidigital.it'}))
|
||||||
|
else:
|
||||||
|
ext = determine_ext(v)
|
||||||
|
if ext == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
v, video_id, 'mp4', 'm3u8_native', m3u8_id=n, fatal=False))
|
||||||
|
continue
|
||||||
|
f = {
|
||||||
|
'format_id': n,
|
||||||
|
'url': v,
|
||||||
|
}
|
||||||
|
if ext == 'mp3':
|
||||||
|
abr = int_or_none(self._search_regex(
|
||||||
|
r'-mp3-audio-(\d+)', v, 'abr', default=None))
|
||||||
|
f.update({
|
||||||
|
'abr': abr,
|
||||||
|
'tbr': abr,
|
||||||
|
'acodec': ext,
|
||||||
|
'vcodec': 'none'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
mobj = re.match(r'^video-rrtv-(\d+)(?:-(\d+))?$', n)
|
||||||
|
if mobj:
|
||||||
|
f.update({
|
||||||
|
'height': int(mobj.group(1)),
|
||||||
|
'vbr': int_or_none(mobj.group(2)),
|
||||||
|
})
|
||||||
|
if not f.get('vbr'):
|
||||||
|
f['vbr'] = int_or_none(self._search_regex(
|
||||||
|
r'-video-rrtv-(\d+)', v, 'abr', default=None))
|
||||||
|
formats.append(f)
|
||||||
|
elif t == 'param':
|
||||||
|
if n in ['image_full', 'image']:
|
||||||
|
thumb = v
|
||||||
|
elif n == 'videoDuration':
|
||||||
|
duration = int_or_none(v)
|
||||||
|
|
||||||
|
self._clean_formats(formats)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': self._html_search_meta(
|
||||||
|
['twitter:description', 'og:description', 'description'], webpage),
|
||||||
|
'thumbnail': thumb or self._og_search_thumbnail(webpage),
|
||||||
|
'formats': formats,
|
||||||
|
'duration': duration,
|
||||||
|
}
|
||||||
@@ -127,13 +127,14 @@ from .expressen import ExpressenIE
|
|||||||
from .zype import ZypeIE
|
from .zype import ZypeIE
|
||||||
from .odnoklassniki import OdnoklassnikiIE
|
from .odnoklassniki import OdnoklassnikiIE
|
||||||
from .kinja import KinjaEmbedIE
|
from .kinja import KinjaEmbedIE
|
||||||
from .gedi import GediEmbedsIE
|
from .gedidigital import GediDigitalIE
|
||||||
from .rcs import RCSEmbedsIE
|
from .rcs import RCSEmbedsIE
|
||||||
from .bitchute import BitChuteIE
|
from .bitchute import BitChuteIE
|
||||||
from .rumble import RumbleEmbedIE
|
from .rumble import RumbleEmbedIE
|
||||||
from .arcpublishing import ArcPublishingIE
|
from .arcpublishing import ArcPublishingIE
|
||||||
from .medialaan import MedialaanIE
|
from .medialaan import MedialaanIE
|
||||||
from .simplecast import SimplecastIE
|
from .simplecast import SimplecastIE
|
||||||
|
from .wimtv import WimTVIE
|
||||||
|
|
||||||
|
|
||||||
class GenericIE(InfoExtractor):
|
class GenericIE(InfoExtractor):
|
||||||
@@ -2250,6 +2251,15 @@ class GenericIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
'playlist_mincount': 52,
|
'playlist_mincount': 52,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
# WimTv embed player
|
||||||
|
'url': 'http://www.msmotor.tv/wearefmi-pt-2-2021/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'wearefmi-pt-2-2021',
|
||||||
|
'title': '#WEAREFMI – PT.2 – 2021 – MsMotorTV',
|
||||||
|
},
|
||||||
|
'playlist_count': 1,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
def report_following_redirect(self, new_url):
|
def report_following_redirect(self, new_url):
|
||||||
@@ -3339,17 +3349,22 @@ class GenericIE(InfoExtractor):
|
|||||||
return self.playlist_from_matches(
|
return self.playlist_from_matches(
|
||||||
zype_urls, video_id, video_title, ie=ZypeIE.ie_key())
|
zype_urls, video_id, video_title, ie=ZypeIE.ie_key())
|
||||||
|
|
||||||
# Look for RCS media group embeds
|
gedi_urls = GediDigitalIE._extract_urls(webpage)
|
||||||
gedi_urls = GediEmbedsIE._extract_urls(webpage)
|
|
||||||
if gedi_urls:
|
if gedi_urls:
|
||||||
return self.playlist_from_matches(
|
return self.playlist_from_matches(
|
||||||
gedi_urls, video_id, video_title, ie=GediEmbedsIE.ie_key())
|
gedi_urls, video_id, video_title, ie=GediDigitalIE.ie_key())
|
||||||
|
|
||||||
|
# Look for RCS media group embeds
|
||||||
rcs_urls = RCSEmbedsIE._extract_urls(webpage)
|
rcs_urls = RCSEmbedsIE._extract_urls(webpage)
|
||||||
if rcs_urls:
|
if rcs_urls:
|
||||||
return self.playlist_from_matches(
|
return self.playlist_from_matches(
|
||||||
rcs_urls, video_id, video_title, ie=RCSEmbedsIE.ie_key())
|
rcs_urls, video_id, video_title, ie=RCSEmbedsIE.ie_key())
|
||||||
|
|
||||||
|
wimtv_urls = WimTVIE._extract_urls(webpage)
|
||||||
|
if wimtv_urls:
|
||||||
|
return self.playlist_from_matches(
|
||||||
|
wimtv_urls, video_id, video_title, ie=WimTVIE.ie_key())
|
||||||
|
|
||||||
bitchute_urls = BitChuteIE._extract_urls(webpage)
|
bitchute_urls = BitChuteIE._extract_urls(webpage)
|
||||||
if bitchute_urls:
|
if bitchute_urls:
|
||||||
return self.playlist_from_matches(
|
return self.playlist_from_matches(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import json
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
|
compat_parse_qs,
|
||||||
compat_str,
|
compat_str,
|
||||||
compat_urllib_parse_unquote,
|
compat_urllib_parse_unquote,
|
||||||
|
compat_urllib_parse_urlparse,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
determine_ext,
|
determine_ext,
|
||||||
@@ -21,9 +23,9 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class LBRYBaseIE(InfoExtractor):
|
class LBRYBaseIE(InfoExtractor):
|
||||||
_BASE_URL_REGEX = r'https?://(?:www\.)?(?:lbry\.tv|odysee\.com)/'
|
_BASE_URL_REGEX = r'(?:https?://(?:www\.)?(?:lbry\.tv|odysee\.com)/|lbry://)'
|
||||||
_CLAIM_ID_REGEX = r'[0-9a-f]{1,40}'
|
_CLAIM_ID_REGEX = r'[0-9a-f]{1,40}'
|
||||||
_OPT_CLAIM_ID = '[^:/?#&]+(?::%s)?' % _CLAIM_ID_REGEX
|
_OPT_CLAIM_ID = '[^:/?#&]+(?:[:#]%s)?' % _CLAIM_ID_REGEX
|
||||||
_SUPPORTED_STREAM_TYPES = ['video', 'audio']
|
_SUPPORTED_STREAM_TYPES = ['video', 'audio']
|
||||||
|
|
||||||
def _call_api_proxy(self, method, display_id, params, resource):
|
def _call_api_proxy(self, method, display_id, params, resource):
|
||||||
@@ -41,7 +43,9 @@ class LBRYBaseIE(InfoExtractor):
|
|||||||
'resolve', display_id, {'urls': url}, resource)[url]
|
'resolve', display_id, {'urls': url}, resource)[url]
|
||||||
|
|
||||||
def _permanent_url(self, url, claim_name, claim_id):
|
def _permanent_url(self, url, claim_name, claim_id):
|
||||||
return urljoin(url, '/%s:%s' % (claim_name, claim_id))
|
return urljoin(
|
||||||
|
url.replace('lbry://', 'https://lbry.tv/'),
|
||||||
|
'/%s:%s' % (claim_name, claim_id))
|
||||||
|
|
||||||
def _parse_stream(self, stream, url):
|
def _parse_stream(self, stream, url):
|
||||||
stream_value = stream.get('value') or {}
|
stream_value = stream.get('value') or {}
|
||||||
@@ -60,6 +64,7 @@ class LBRYBaseIE(InfoExtractor):
|
|||||||
'description': stream_value.get('description'),
|
'description': stream_value.get('description'),
|
||||||
'license': stream_value.get('license'),
|
'license': stream_value.get('license'),
|
||||||
'timestamp': int_or_none(stream.get('timestamp')),
|
'timestamp': int_or_none(stream.get('timestamp')),
|
||||||
|
'release_timestamp': int_or_none(stream_value.get('release_time')),
|
||||||
'tags': stream_value.get('tags'),
|
'tags': stream_value.get('tags'),
|
||||||
'duration': int_or_none(media.get('duration')),
|
'duration': int_or_none(media.get('duration')),
|
||||||
'channel': try_get(signing_channel, lambda x: x['value']['title']),
|
'channel': try_get(signing_channel, lambda x: x['value']['title']),
|
||||||
@@ -92,6 +97,8 @@ class LBRYIE(LBRYBaseIE):
|
|||||||
'description': 'md5:f6cb5c704b332d37f5119313c2c98f51',
|
'description': 'md5:f6cb5c704b332d37f5119313c2c98f51',
|
||||||
'timestamp': 1595694354,
|
'timestamp': 1595694354,
|
||||||
'upload_date': '20200725',
|
'upload_date': '20200725',
|
||||||
|
'release_timestamp': 1595340697,
|
||||||
|
'release_date': '20200721',
|
||||||
'width': 1280,
|
'width': 1280,
|
||||||
'height': 720,
|
'height': 720,
|
||||||
}
|
}
|
||||||
@@ -106,6 +113,8 @@ class LBRYIE(LBRYBaseIE):
|
|||||||
'description': 'md5:661ac4f1db09f31728931d7b88807a61',
|
'description': 'md5:661ac4f1db09f31728931d7b88807a61',
|
||||||
'timestamp': 1591312601,
|
'timestamp': 1591312601,
|
||||||
'upload_date': '20200604',
|
'upload_date': '20200604',
|
||||||
|
'release_timestamp': 1591312421,
|
||||||
|
'release_date': '20200604',
|
||||||
'tags': list,
|
'tags': list,
|
||||||
'duration': 2570,
|
'duration': 2570,
|
||||||
'channel': 'The LBRY Foundation',
|
'channel': 'The LBRY Foundation',
|
||||||
@@ -137,6 +146,9 @@ class LBRYIE(LBRYBaseIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://lbry.tv/@lacajadepandora:a/TRUMP-EST%C3%81-BIEN-PUESTO-con-Pilar-Baselga,-Carlos-Senra,-Luis-Palacios-(720p_30fps_H264-192kbit_AAC):1',
|
'url': 'https://lbry.tv/@lacajadepandora:a/TRUMP-EST%C3%81-BIEN-PUESTO-con-Pilar-Baselga,-Carlos-Senra,-Luis-Palacios-(720p_30fps_H264-192kbit_AAC):1',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'lbry://@lbry#3f/odysee#7',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -166,7 +178,7 @@ class LBRYIE(LBRYBaseIE):
|
|||||||
|
|
||||||
class LBRYChannelIE(LBRYBaseIE):
|
class LBRYChannelIE(LBRYBaseIE):
|
||||||
IE_NAME = 'lbry:channel'
|
IE_NAME = 'lbry:channel'
|
||||||
_VALID_URL = LBRYBaseIE._BASE_URL_REGEX + r'(?P<id>@%s)/?(?:[?#&]|$)' % LBRYBaseIE._OPT_CLAIM_ID
|
_VALID_URL = LBRYBaseIE._BASE_URL_REGEX + r'(?P<id>@%s)/?(?:[?&]|$)' % LBRYBaseIE._OPT_CLAIM_ID
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://lbry.tv/@LBRYFoundation:0',
|
'url': 'https://lbry.tv/@LBRYFoundation:0',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -178,20 +190,24 @@ class LBRYChannelIE(LBRYBaseIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://lbry.tv/@LBRYFoundation',
|
'url': 'https://lbry.tv/@LBRYFoundation',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'lbry://@lbry#3f',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
_PAGE_SIZE = 50
|
_PAGE_SIZE = 50
|
||||||
|
|
||||||
def _fetch_page(self, claim_id, url, page):
|
def _fetch_page(self, claim_id, url, params, page):
|
||||||
page += 1
|
page += 1
|
||||||
result = self._call_api_proxy(
|
page_params = {
|
||||||
'claim_search', claim_id, {
|
|
||||||
'channel_ids': [claim_id],
|
'channel_ids': [claim_id],
|
||||||
'claim_type': 'stream',
|
'claim_type': 'stream',
|
||||||
'no_totals': True,
|
'no_totals': True,
|
||||||
'page': page,
|
'page': page,
|
||||||
'page_size': self._PAGE_SIZE,
|
'page_size': self._PAGE_SIZE,
|
||||||
'stream_types': self._SUPPORTED_STREAM_TYPES,
|
}
|
||||||
}, 'page %d' % page)
|
page_params.update(params)
|
||||||
|
result = self._call_api_proxy(
|
||||||
|
'claim_search', claim_id, page_params, 'page %d' % page)
|
||||||
for item in (result.get('items') or []):
|
for item in (result.get('items') or []):
|
||||||
stream_claim_name = item.get('name')
|
stream_claim_name = item.get('name')
|
||||||
stream_claim_id = item.get('claim_id')
|
stream_claim_id = item.get('claim_id')
|
||||||
@@ -212,8 +228,31 @@ class LBRYChannelIE(LBRYBaseIE):
|
|||||||
result = self._resolve_url(
|
result = self._resolve_url(
|
||||||
'lbry://' + display_id, display_id, 'channel')
|
'lbry://' + display_id, display_id, 'channel')
|
||||||
claim_id = result['claim_id']
|
claim_id = result['claim_id']
|
||||||
|
qs = compat_parse_qs(compat_urllib_parse_urlparse(url).query)
|
||||||
|
content = qs.get('content', [None])[0]
|
||||||
|
params = {
|
||||||
|
'fee_amount': qs.get('fee_amount', ['>=0'])[0],
|
||||||
|
'order_by': {
|
||||||
|
'new': ['release_time'],
|
||||||
|
'top': ['effective_amount'],
|
||||||
|
'trending': ['trending_group', 'trending_mixed'],
|
||||||
|
}[qs.get('order', ['new'])[0]],
|
||||||
|
'stream_types': [content] if content in ['audio', 'video'] else self._SUPPORTED_STREAM_TYPES,
|
||||||
|
}
|
||||||
|
duration = qs.get('duration', [None])[0]
|
||||||
|
if duration:
|
||||||
|
params['duration'] = {
|
||||||
|
'long': '>=1200',
|
||||||
|
'short': '<=240',
|
||||||
|
}[duration]
|
||||||
|
language = qs.get('language', ['all'])[0]
|
||||||
|
if language != 'all':
|
||||||
|
languages = [language]
|
||||||
|
if language == 'en':
|
||||||
|
languages.append('none')
|
||||||
|
params['any_languages'] = languages
|
||||||
entries = OnDemandPagedList(
|
entries = OnDemandPagedList(
|
||||||
functools.partial(self._fetch_page, claim_id, url),
|
functools.partial(self._fetch_page, claim_id, url, params),
|
||||||
self._PAGE_SIZE)
|
self._PAGE_SIZE)
|
||||||
result_value = result.get('value') or {}
|
result_value = result.get('value') or {}
|
||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -15,6 +14,7 @@ from ..utils import (
|
|||||||
fix_xml_ampersands,
|
fix_xml_ampersands,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
HEADRequest,
|
HEADRequest,
|
||||||
|
int_or_none,
|
||||||
RegexNotFoundError,
|
RegexNotFoundError,
|
||||||
sanitized_Request,
|
sanitized_Request,
|
||||||
strip_or_none,
|
strip_or_none,
|
||||||
@@ -23,7 +23,6 @@ from ..utils import (
|
|||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
url_basename,
|
url_basename,
|
||||||
get_domain,
|
|
||||||
xpath_text,
|
xpath_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,7 +44,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):
|
||||||
@@ -178,6 +177,22 @@ class MTVServicesInfoExtractor(InfoExtractor):
|
|||||||
raise ExtractorError('Could not find video title')
|
raise ExtractorError('Could not find video title')
|
||||||
title = title.strip()
|
title = title.strip()
|
||||||
|
|
||||||
|
series = find_xpath_attr(
|
||||||
|
itemdoc, './/{http://search.yahoo.com/mrss/}category',
|
||||||
|
'scheme', 'urn:mtvn:franchise')
|
||||||
|
season = find_xpath_attr(
|
||||||
|
itemdoc, './/{http://search.yahoo.com/mrss/}category',
|
||||||
|
'scheme', 'urn:mtvn:seasonN')
|
||||||
|
episode = find_xpath_attr(
|
||||||
|
itemdoc, './/{http://search.yahoo.com/mrss/}category',
|
||||||
|
'scheme', 'urn:mtvn:episodeN')
|
||||||
|
series = series.text if series is not None else None
|
||||||
|
season = season.text if season is not None else None
|
||||||
|
episode = episode.text if episode is not None else None
|
||||||
|
if season and episode:
|
||||||
|
# episode number includes season, so remove it
|
||||||
|
episode = re.sub(r'^%s' % season, '', episode)
|
||||||
|
|
||||||
# This a short id that's used in the webpage urls
|
# This a short id that's used in the webpage urls
|
||||||
mtvn_id = None
|
mtvn_id = None
|
||||||
mtvn_id_node = find_xpath_attr(itemdoc, './/{http://search.yahoo.com/mrss/}category',
|
mtvn_id_node = find_xpath_attr(itemdoc, './/{http://search.yahoo.com/mrss/}category',
|
||||||
@@ -203,6 +218,9 @@ class MTVServicesInfoExtractor(InfoExtractor):
|
|||||||
'description': description,
|
'description': description,
|
||||||
'duration': float_or_none(content_el.attrib.get('duration')),
|
'duration': float_or_none(content_el.attrib.get('duration')),
|
||||||
'timestamp': timestamp,
|
'timestamp': timestamp,
|
||||||
|
'series': series,
|
||||||
|
'season_number': int_or_none(season),
|
||||||
|
'episode_number': int_or_none(episode),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_feed_query(self, uri):
|
def _get_feed_query(self, uri):
|
||||||
@@ -211,9 +229,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 +277,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 +288,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 +300,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 +314,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
|
||||||
|
|
||||||
|
|
||||||
@@ -537,3 +503,152 @@ class MTVDEIE(MTVServicesInfoExtractor):
|
|||||||
'arcEp': 'mtv.de',
|
'arcEp': 'mtv.de',
|
||||||
'mgid': uri,
|
'mgid': uri,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MTVItaliaIE(MTVServicesInfoExtractor):
|
||||||
|
IE_NAME = 'mtv.it'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?mtv\.it/(?:episodi|video|musica)/(?P<id>[0-9a-z]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://www.mtv.it/episodi/24bqab/mario-una-serie-di-maccio-capatonda-cavoli-amario-episodio-completo-S1-E1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '0f0fc78e-45fc-4cce-8f24-971c25477530',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Cavoli amario (episodio completo)',
|
||||||
|
'description': 'md5:4962bccea8fed5b7c03b295ae1340660',
|
||||||
|
'series': 'Mario - Una Serie Di Maccio Capatonda',
|
||||||
|
'season_number': 1,
|
||||||
|
'episode_number': 1,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
_GEO_COUNTRIES = ['IT']
|
||||||
|
_FEED_URL = 'http://feeds.mtvnservices.com/od/feed/intl-mrss-player-feed'
|
||||||
|
|
||||||
|
def _get_feed_query(self, uri):
|
||||||
|
return {
|
||||||
|
'arcEp': 'mtv.it',
|
||||||
|
'mgid': uri,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MTVItaliaProgrammaIE(MTVItaliaIE):
|
||||||
|
IE_NAME = 'mtv.it:programma'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?mtv\.it/(?:programmi|playlist)/(?P<id>[0-9a-z]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
# program page: general
|
||||||
|
'url': 'http://www.mtv.it/programmi/s2rppv/mario-una-serie-di-maccio-capatonda',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'a6f155bc-8220-4640-aa43-9b95f64ffa3d',
|
||||||
|
'title': 'Mario - Una Serie Di Maccio Capatonda',
|
||||||
|
'description': 'md5:72fbffe1f77ccf4e90757dd4e3216153',
|
||||||
|
},
|
||||||
|
'playlist_count': 2,
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# program page: specific season
|
||||||
|
'url': 'http://www.mtv.it/programmi/d9ncjf/mario-una-serie-di-maccio-capatonda-S2',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4deeb5d8-f272-490c-bde2-ff8d261c6dd1',
|
||||||
|
'title': 'Mario - Una Serie Di Maccio Capatonda - Stagione 2',
|
||||||
|
},
|
||||||
|
'playlist_count': 34,
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# playlist page + redirect
|
||||||
|
'url': 'http://www.mtv.it/playlist/sexy-videos/ilctal',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'dee8f9ee-756d-493b-bf37-16d1d2783359',
|
||||||
|
'title': 'Sexy Videos',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 145,
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
_GEO_COUNTRIES = ['IT']
|
||||||
|
_FEED_URL = 'http://www.mtv.it/feeds/triforce/manifest/v8'
|
||||||
|
|
||||||
|
def _get_entries(self, title, url):
|
||||||
|
while True:
|
||||||
|
pg = self._search_regex(r'/(\d+)$', url, 'entries', '1')
|
||||||
|
entries = self._download_json(url, title, 'page %s' % pg)
|
||||||
|
url = try_get(
|
||||||
|
entries, lambda x: x['result']['nextPageURL'], compat_str)
|
||||||
|
entries = try_get(
|
||||||
|
entries, (
|
||||||
|
lambda x: x['result']['data']['items'],
|
||||||
|
lambda x: x['result']['data']['seasons']),
|
||||||
|
list)
|
||||||
|
for entry in entries or []:
|
||||||
|
if entry.get('canonicalURL'):
|
||||||
|
yield self.url_result(entry['canonicalURL'])
|
||||||
|
if not url:
|
||||||
|
break
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
query = {'url': url}
|
||||||
|
info_url = update_url_query(self._FEED_URL, query)
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
info = self._download_json(info_url, video_id).get('manifest')
|
||||||
|
|
||||||
|
redirect = try_get(
|
||||||
|
info, lambda x: x['newLocation']['url'], compat_str)
|
||||||
|
if redirect:
|
||||||
|
return self.url_result(redirect)
|
||||||
|
|
||||||
|
title = info.get('title')
|
||||||
|
video_id = try_get(
|
||||||
|
info, lambda x: x['reporting']['itemId'], compat_str)
|
||||||
|
parent_id = try_get(
|
||||||
|
info, lambda x: x['reporting']['parentId'], compat_str)
|
||||||
|
|
||||||
|
playlist_url = current_url = None
|
||||||
|
for z in (info.get('zones') or {}).values():
|
||||||
|
if z.get('moduleName') in ('INTL_M304', 'INTL_M209'):
|
||||||
|
info_url = z.get('feed')
|
||||||
|
if z.get('moduleName') in ('INTL_M308', 'INTL_M317'):
|
||||||
|
playlist_url = playlist_url or z.get('feed')
|
||||||
|
if z.get('moduleName') in ('INTL_M300',):
|
||||||
|
current_url = current_url or z.get('feed')
|
||||||
|
|
||||||
|
if not info_url:
|
||||||
|
raise ExtractorError('No info found')
|
||||||
|
|
||||||
|
if video_id == parent_id:
|
||||||
|
video_id = self._search_regex(
|
||||||
|
r'([^\/]+)/[^\/]+$', info_url, 'video_id')
|
||||||
|
|
||||||
|
info = self._download_json(info_url, video_id, 'Show infos')
|
||||||
|
info = try_get(info, lambda x: x['result']['data'], dict)
|
||||||
|
title = title or try_get(
|
||||||
|
info, (
|
||||||
|
lambda x: x['title'],
|
||||||
|
lambda x: x['headline']),
|
||||||
|
compat_str)
|
||||||
|
description = try_get(info, lambda x: x['content'], compat_str)
|
||||||
|
|
||||||
|
if current_url:
|
||||||
|
season = try_get(
|
||||||
|
self._download_json(playlist_url, video_id, 'Seasons info'),
|
||||||
|
lambda x: x['result']['data'], dict)
|
||||||
|
current = try_get(
|
||||||
|
season, lambda x: x['currentSeason'], compat_str)
|
||||||
|
seasons = try_get(
|
||||||
|
season, lambda x: x['seasons'], list) or []
|
||||||
|
|
||||||
|
if current in [s.get('eTitle') for s in seasons]:
|
||||||
|
playlist_url = current_url
|
||||||
|
|
||||||
|
title = re.sub(
|
||||||
|
r'[-|]\s*(?:mtv\s*italia|programma|playlist)',
|
||||||
|
'', title, flags=re.IGNORECASE).strip()
|
||||||
|
|
||||||
|
return self.playlist_result(
|
||||||
|
self._get_entries(title, playlist_url),
|
||||||
|
video_id, title, description)
|
||||||
|
|||||||
127
yt_dlp/extractor/mxplayer.py
Normal file
127
yt_dlp/extractor/mxplayer.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
js_to_json,
|
||||||
|
qualities,
|
||||||
|
try_get,
|
||||||
|
url_or_none,
|
||||||
|
urljoin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MxplayerIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?mxplayer\.in/(?:show|movie)/(?:(?P<display_id>[-/a-z0-9]+)-)?(?P<id>[a-z0-9]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'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 (Hindi Dubbed)',
|
||||||
|
'description': 'md5:b195ba93ff1987309cfa58e2839d2a5b'
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
'format': 'bestvideo'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.mxplayer.in/show/watch-shaitaan/season-1/the-infamous-taxi-gang-of-meerut-online-45055d5bcff169ad48f2ad7552a83d6c',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '45055d5bcff169ad48f2ad7552a83d6c',
|
||||||
|
'ext': 'm3u8',
|
||||||
|
'title': 'The infamous taxi gang of Meerut',
|
||||||
|
'description': 'md5:033a0a7e3fd147be4fb7e07a01a3dc28',
|
||||||
|
'season': 'Season 1',
|
||||||
|
'series': 'Shaitaan'
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.mxplayer.in/show/watch-aashram/chapter-1/duh-swapna-online-d445579792b0135598ba1bc9088a84cb',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'd445579792b0135598ba1bc9088a84cb',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Duh Swapna',
|
||||||
|
'description': 'md5:35ff39c4bdac403c53be1e16a04192d8',
|
||||||
|
'season': 'Chapter 1',
|
||||||
|
'series': 'Aashram'
|
||||||
|
},
|
||||||
|
'expected_warnings': ['Unknown MIME type application/mp4 in DASH manifest'],
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
'format': 'bestvideo'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _get_stream_urls(self, video_dict):
|
||||||
|
stream_provider_dict = try_get(
|
||||||
|
video_dict,
|
||||||
|
lambda x: x['stream'][x['stream']['provider']])
|
||||||
|
if not stream_provider_dict:
|
||||||
|
raise ExtractorError('No stream provider found', expected=True)
|
||||||
|
|
||||||
|
for stream_name, stream in stream_provider_dict.items():
|
||||||
|
if stream_name in ('hls', 'dash', 'hlsUrl', 'dashUrl'):
|
||||||
|
stream_type = stream_name.replace('Url', '')
|
||||||
|
if isinstance(stream, dict):
|
||||||
|
for quality, stream_url in stream.items():
|
||||||
|
if stream_url:
|
||||||
|
yield stream_type, quality, stream_url
|
||||||
|
else:
|
||||||
|
yield stream_type, 'base', stream
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id, video_id = re.match(self._VALID_URL, url).groups()
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
source = self._parse_json(
|
||||||
|
js_to_json(self._html_search_regex(
|
||||||
|
r'(?s)<script>window\.state\s*[:=]\s(\{.+\})\n(\w+).*(</script>).*',
|
||||||
|
webpage, 'WindowState')),
|
||||||
|
video_id)
|
||||||
|
if not source:
|
||||||
|
raise ExtractorError('Cannot find source', expected=True)
|
||||||
|
|
||||||
|
config_dict = source['config']
|
||||||
|
video_dict = source['entities'][video_id]
|
||||||
|
|
||||||
|
thumbnails = []
|
||||||
|
for i in video_dict.get('imageInfo') or []:
|
||||||
|
thumbnails.append({
|
||||||
|
'url': urljoin(config_dict['imageBaseUrl'], i['url']),
|
||||||
|
'width': i['width'],
|
||||||
|
'height': i['height'],
|
||||||
|
})
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
get_quality = qualities(['main', 'base', 'high'])
|
||||||
|
for stream_type, quality, stream_url in self._get_stream_urls(video_dict):
|
||||||
|
format_url = url_or_none(urljoin(config_dict['videoCdnBaseUrl'], stream_url))
|
||||||
|
if not format_url:
|
||||||
|
continue
|
||||||
|
if stream_type == 'dash':
|
||||||
|
dash_formats = self._extract_mpd_formats(
|
||||||
|
format_url, video_id, mpd_id='dash-%s' % quality, headers={'Referer': url})
|
||||||
|
for frmt in dash_formats:
|
||||||
|
frmt['quality'] = get_quality(quality)
|
||||||
|
formats.extend(dash_formats)
|
||||||
|
elif stream_type == 'hls':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
format_url, video_id, fatal=False,
|
||||||
|
m3u8_id='hls-%s' % quality, quality=get_quality(quality)))
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'display_id': display_id.replace('/', '-'),
|
||||||
|
'title': video_dict['title'] or self._og_search_title(webpage),
|
||||||
|
'formats': formats,
|
||||||
|
'description': video_dict.get('description'),
|
||||||
|
'season': try_get(video_dict, lambda x: x['container']['title']),
|
||||||
|
'series': try_get(video_dict, lambda x: x['container']['container']['title']),
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
}
|
||||||
@@ -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={
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -599,11 +599,13 @@ class PeerTubeIE(InfoExtractor):
|
|||||||
else:
|
else:
|
||||||
age_limit = None
|
age_limit = None
|
||||||
|
|
||||||
|
webpage_url = 'https://%s/videos/watch/%s' % (host, video_id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'thumbnail': urljoin(url, video.get('thumbnailPath')),
|
'thumbnail': urljoin(webpage_url, video.get('thumbnailPath')),
|
||||||
'timestamp': unified_timestamp(video.get('publishedAt')),
|
'timestamp': unified_timestamp(video.get('publishedAt')),
|
||||||
'uploader': account_data('displayName', compat_str),
|
'uploader': account_data('displayName', compat_str),
|
||||||
'uploader_id': str_or_none(account_data('id', int)),
|
'uploader_id': str_or_none(account_data('id', int)),
|
||||||
@@ -621,5 +623,6 @@ class PeerTubeIE(InfoExtractor):
|
|||||||
'tags': try_get(video, lambda x: x['tags'], list),
|
'tags': try_get(video, lambda x: x['tags'], list),
|
||||||
'categories': categories,
|
'categories': categories,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': subtitles
|
'subtitles': subtitles,
|
||||||
|
'webpage_url': webpage_url,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,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,
|
||||||
|
'timestamp': 1613906100,
|
||||||
|
'upload_date': '20210221',
|
||||||
|
'uploader': 'Phoenix',
|
||||||
|
'channel': 'corona nachgehakt',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# Youtube embed
|
||||||
|
'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': 'phoenix',
|
||||||
'uploader_id': 'phoenix',
|
'uploader_id': 'phoenix',
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
'params': {
|
||||||
'url': 'https://www.phoenix.de/drohnenangriffe-in-saudi-arabien-a-1286995.html?ref=aktuelles',
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.phoenix.de/entwicklungen-in-russland-a-2044720.html',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
},
|
}, {
|
||||||
# an older page: https://www.phoenix.de/sendungen/gespraeche/phoenix-persoenlich/im-dialog-a-177727.html
|
# no media
|
||||||
# seems to not have an embedded video, even though it's uploaded on youtube: https://www.youtube.com/watch?v=4GxnoUHvOkM
|
'url': 'https://www.phoenix.de/sendungen/dokumentationen/mit-dem-jumbo-durch-die-nacht-a-89625.html',
|
||||||
]
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
def extract_from_json_api(self, video_id, api_url):
|
# Same as https://www.zdf.de/politik/phoenix-sendungen/die-gesten-der-maechtigen-100.html
|
||||||
doc = self._download_json(
|
'url': 'https://www.phoenix.de/sendungen/dokumentationen/gesten-der-maechtigen-i-a-89468.html?ref=suche',
|
||||||
api_url, video_id,
|
'only_matching': True,
|
||||||
note="Downloading webpage metadata",
|
}]
|
||||||
errnote="Failed to load webpage metadata")
|
|
||||||
|
|
||||||
for a in doc["absaetze"]:
|
|
||||||
if a["typ"] == "video-youtube":
|
|
||||||
return {
|
|
||||||
'_type': 'url_transparent',
|
|
||||||
'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'),
|
||||||
|
})
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class PinterestBaseIE(InfoExtractor):
|
|||||||
|
|
||||||
title = (data.get('title') or data.get('grid_title') or video_id).strip()
|
title = (data.get('title') or data.get('grid_title') or video_id).strip()
|
||||||
|
|
||||||
|
urls = []
|
||||||
formats = []
|
formats = []
|
||||||
duration = None
|
duration = None
|
||||||
if extract_formats:
|
if extract_formats:
|
||||||
@@ -38,8 +39,9 @@ class PinterestBaseIE(InfoExtractor):
|
|||||||
if not isinstance(format_dict, dict):
|
if not isinstance(format_dict, dict):
|
||||||
continue
|
continue
|
||||||
format_url = url_or_none(format_dict.get('url'))
|
format_url = url_or_none(format_dict.get('url'))
|
||||||
if not format_url:
|
if not format_url or format_url in urls:
|
||||||
continue
|
continue
|
||||||
|
urls.append(format_url)
|
||||||
duration = float_or_none(format_dict.get('duration'), scale=1000)
|
duration = float_or_none(format_dict.get('duration'), scale=1000)
|
||||||
ext = determine_ext(format_url)
|
ext = determine_ext(format_url)
|
||||||
if 'hls' in format_id.lower() or ext == 'm3u8':
|
if 'hls' in format_id.lower() or ext == 'm3u8':
|
||||||
|
|||||||
164
yt_dlp/extractor/plutotv.py
Normal file
164
yt_dlp/extractor/plutotv.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import (
|
||||||
|
compat_str,
|
||||||
|
compat_urlparse,
|
||||||
|
)
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
float_or_none,
|
||||||
|
int_or_none,
|
||||||
|
try_get,
|
||||||
|
url_or_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PlutoTVIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?pluto\.tv/on-demand/(?P<video_type>movies|series)/(?P<slug>.*)/?$'
|
||||||
|
_INFO_URL = 'https://service-vod.clusters.pluto.tv/v3/vod/slugs/'
|
||||||
|
_INFO_QUERY_PARAMS = {
|
||||||
|
'appName': 'web',
|
||||||
|
'appVersion': 'na',
|
||||||
|
'clientID': compat_str(uuid.uuid1()),
|
||||||
|
'clientModelNumber': 'na',
|
||||||
|
'serverSideAds': 'false',
|
||||||
|
'deviceMake': 'unknown',
|
||||||
|
'deviceModel': 'web',
|
||||||
|
'deviceType': 'web',
|
||||||
|
'deviceVersion': 'unknown',
|
||||||
|
'sid': compat_str(uuid.uuid1()),
|
||||||
|
}
|
||||||
|
_TESTS = [
|
||||||
|
{
|
||||||
|
'url': 'https://pluto.tv/on-demand/series/i-love-money/season/2/episode/its-in-the-cards-2009-2-3',
|
||||||
|
'md5': 'ebcdd8ed89aaace9df37924f722fd9bd',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5de6c598e9379ae4912df0a8',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'It\'s In The Cards',
|
||||||
|
'episode': 'It\'s In The Cards',
|
||||||
|
'description': 'The teams face off against each other in a 3-on-2 soccer showdown. Strategy comes into play, though, as each team gets to select their opposing teams’ two defenders.',
|
||||||
|
'series': 'I Love Money',
|
||||||
|
'season_number': 2,
|
||||||
|
'episode_number': 3,
|
||||||
|
'duration': 3600,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://pluto.tv/on-demand/series/i-love-money/season/1/',
|
||||||
|
'playlist_count': 11,
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5de6c582e9379ae4912dedbd',
|
||||||
|
'title': 'I Love Money - Season 1',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://pluto.tv/on-demand/series/i-love-money/',
|
||||||
|
'playlist_count': 26,
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5de6c582e9379ae4912dedbd',
|
||||||
|
'title': 'I Love Money',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://pluto.tv/on-demand/movies/arrival-2015-1-1',
|
||||||
|
'md5': '3cead001d317a018bf856a896dee1762',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5e83ac701fa6a9001bb9df24',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Arrival',
|
||||||
|
'description': 'When mysterious spacecraft touch down across the globe, an elite team - led by expert translator Louise Banks (Academy Award® nominee Amy Adams) – races against time to decipher their intent.',
|
||||||
|
'duration': 9000,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
def _to_ad_free_formats(self, video_id, formats):
|
||||||
|
ad_free_formats = []
|
||||||
|
m3u8_urls = set()
|
||||||
|
for format in formats:
|
||||||
|
res = self._download_webpage(
|
||||||
|
format.get('url'), video_id, note='Downloading m3u8 playlist',
|
||||||
|
fatal=False)
|
||||||
|
if not res:
|
||||||
|
continue
|
||||||
|
first_segment_url = re.search(
|
||||||
|
r'^(https?://.*/)0\-(end|[0-9]+)/[^/]+\.ts$', res,
|
||||||
|
re.MULTILINE)
|
||||||
|
if not first_segment_url:
|
||||||
|
continue
|
||||||
|
m3u8_urls.add(
|
||||||
|
compat_urlparse.urljoin(first_segment_url.group(1), '0-end/master.m3u8'))
|
||||||
|
|
||||||
|
for m3u8_url in m3u8_urls:
|
||||||
|
ad_free_formats.extend(
|
||||||
|
self._extract_m3u8_formats(
|
||||||
|
m3u8_url, video_id, 'mp4', 'm3u8_native',
|
||||||
|
m3u8_id='hls', fatal=False))
|
||||||
|
self._sort_formats(ad_free_formats)
|
||||||
|
return ad_free_formats
|
||||||
|
|
||||||
|
def _get_video_info(self, video_json, slug, series_name=None):
|
||||||
|
video_id = video_json.get('_id', slug)
|
||||||
|
formats = []
|
||||||
|
for video_url in try_get(video_json, lambda x: x['stitched']['urls'], list) or []:
|
||||||
|
if video_url.get('type') != 'hls':
|
||||||
|
continue
|
||||||
|
url = url_or_none(video_url.get('url'))
|
||||||
|
formats.extend(
|
||||||
|
self._extract_m3u8_formats(
|
||||||
|
url, video_id, 'mp4', 'm3u8_native',
|
||||||
|
m3u8_id='hls', fatal=False))
|
||||||
|
info = {
|
||||||
|
'id': video_id,
|
||||||
|
'formats': self._to_ad_free_formats(video_id, formats),
|
||||||
|
'title': video_json.get('name'),
|
||||||
|
'description': video_json.get('description'),
|
||||||
|
'duration': float_or_none(video_json.get('duration'), scale=1000),
|
||||||
|
}
|
||||||
|
if series_name:
|
||||||
|
info.update({
|
||||||
|
'series': series_name,
|
||||||
|
'episode': video_json.get('name'),
|
||||||
|
'season_number': int_or_none(video_json.get('season')),
|
||||||
|
'episode_number': int_or_none(video_json.get('number')),
|
||||||
|
})
|
||||||
|
return info
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
path = compat_urlparse.urlparse(url).path
|
||||||
|
path_components = path.split('/')
|
||||||
|
video_type = path_components[2]
|
||||||
|
info_slug = path_components[3]
|
||||||
|
video_json = self._download_json(self._INFO_URL + info_slug, info_slug,
|
||||||
|
query=self._INFO_QUERY_PARAMS)
|
||||||
|
|
||||||
|
if video_type == 'series':
|
||||||
|
series_name = video_json.get('name', info_slug)
|
||||||
|
season_number = int_or_none(try_get(path_components, lambda x: x[5]))
|
||||||
|
episode_slug = try_get(path_components, lambda x: x[7])
|
||||||
|
|
||||||
|
videos = []
|
||||||
|
for season in video_json['seasons']:
|
||||||
|
if season_number is not None and season_number != int_or_none(season.get('number')):
|
||||||
|
continue
|
||||||
|
for episode in season['episodes']:
|
||||||
|
if episode_slug is not None and episode_slug != episode.get('slug'):
|
||||||
|
continue
|
||||||
|
videos.append(self._get_video_info(episode, episode_slug, series_name))
|
||||||
|
if not videos:
|
||||||
|
raise ExtractorError('Failed to find any videos to extract')
|
||||||
|
if episode_slug is not None and len(videos) == 1:
|
||||||
|
return videos[0]
|
||||||
|
playlist_title = series_name
|
||||||
|
if season_number is not None:
|
||||||
|
playlist_title += ' - Season %d' % season_number
|
||||||
|
return self.playlist_result(videos,
|
||||||
|
playlist_id=video_json.get('_id', info_slug),
|
||||||
|
playlist_title=playlist_title)
|
||||||
|
return self._get_video_info(video_json, info_slug)
|
||||||
@@ -167,6 +167,7 @@ class PornHubIE(PornHubBaseIE):
|
|||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
'skip': 'Video has been flagged for verification in accordance with our trust and safety policy',
|
||||||
}, {
|
}, {
|
||||||
# subtitles
|
# subtitles
|
||||||
'url': 'https://www.pornhub.com/view_video.php?viewkey=ph5af5fef7c2aa7',
|
'url': 'https://www.pornhub.com/view_video.php?viewkey=ph5af5fef7c2aa7',
|
||||||
@@ -265,7 +266,8 @@ class PornHubIE(PornHubBaseIE):
|
|||||||
webpage = dl_webpage('pc')
|
webpage = dl_webpage('pc')
|
||||||
|
|
||||||
error_msg = self._html_search_regex(
|
error_msg = self._html_search_regex(
|
||||||
r'(?s)<div[^>]+class=(["\'])(?:(?!\1).)*\b(?:removed|userMessageSection)\b(?:(?!\1).)*\1[^>]*>(?P<error>.+?)</div>',
|
(r'(?s)<div[^>]+class=(["\'])(?:(?!\1).)*\b(?:removed|userMessageSection)\b(?:(?!\1).)*\1[^>]*>(?P<error>.+?)</div>',
|
||||||
|
r'(?s)<section[^>]+class=["\']noVideo["\'][^>]*>(?P<error>.+?)</section>'),
|
||||||
webpage, 'error message', default=None, group='error')
|
webpage, 'error message', default=None, group='error')
|
||||||
if error_msg:
|
if error_msg:
|
||||||
error_msg = re.sub(r'\s+', ' ', error_msg)
|
error_msg = re.sub(r'\s+', ' ', error_msg)
|
||||||
@@ -394,6 +396,21 @@ class PornHubIE(PornHubBaseIE):
|
|||||||
|
|
||||||
upload_date = None
|
upload_date = None
|
||||||
formats = []
|
formats = []
|
||||||
|
|
||||||
|
def add_format(format_url, height=None):
|
||||||
|
tbr = None
|
||||||
|
mobj = re.search(r'(?P<height>\d+)[pP]?_(?P<tbr>\d+)[kK]', format_url)
|
||||||
|
if mobj:
|
||||||
|
if not height:
|
||||||
|
height = int(mobj.group('height'))
|
||||||
|
tbr = int(mobj.group('tbr'))
|
||||||
|
formats.append({
|
||||||
|
'url': format_url,
|
||||||
|
'format_id': '%dp' % height if height else None,
|
||||||
|
'height': height,
|
||||||
|
'tbr': tbr,
|
||||||
|
})
|
||||||
|
|
||||||
for video_url, height in video_urls:
|
for video_url, height in video_urls:
|
||||||
if not upload_date:
|
if not upload_date:
|
||||||
upload_date = self._search_regex(
|
upload_date = self._search_regex(
|
||||||
@@ -410,18 +427,19 @@ class PornHubIE(PornHubBaseIE):
|
|||||||
video_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
video_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
m3u8_id='hls', fatal=False))
|
m3u8_id='hls', fatal=False))
|
||||||
continue
|
continue
|
||||||
tbr = None
|
if '/video/get_media' in video_url:
|
||||||
mobj = re.search(r'(?P<height>\d+)[pP]?_(?P<tbr>\d+)[kK]', video_url)
|
medias = self._download_json(video_url, video_id, fatal=False)
|
||||||
if mobj:
|
if isinstance(medias, list):
|
||||||
if not height:
|
for media in medias:
|
||||||
height = int(mobj.group('height'))
|
if not isinstance(media, dict):
|
||||||
tbr = int(mobj.group('tbr'))
|
continue
|
||||||
formats.append({
|
video_url = url_or_none(media.get('videoUrl'))
|
||||||
'url': video_url,
|
if not video_url:
|
||||||
'format_id': '%dp' % height if height else None,
|
continue
|
||||||
'height': height,
|
height = int_or_none(media.get('quality'))
|
||||||
'tbr': tbr,
|
add_format(video_url, height)
|
||||||
})
|
continue
|
||||||
|
add_format(video_url)
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
video_uploader = self._html_search_regex(
|
video_uploader = self._html_search_regex(
|
||||||
|
|||||||
@@ -158,6 +158,10 @@ class RaiPlayIE(RaiBaseIE):
|
|||||||
# subtitles at 'subtitlesArray' key (see #27698)
|
# subtitles at 'subtitlesArray' key (see #27698)
|
||||||
'url': 'https://www.raiplay.it/video/2020/12/Report---04-01-2021-2e90f1de-8eee-4de4-ac0e-78d21db5b600.html',
|
'url': 'https://www.raiplay.it/video/2020/12/Report---04-01-2021-2e90f1de-8eee-4de4-ac0e-78d21db5b600.html',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# DRM protected
|
||||||
|
'url': 'https://www.raiplay.it/video/2020/09/Lo-straordinario-mondo-di-Zoey-S1E1-Lo-straordinario-potere-di-Zoey-ed493918-1d32-44b7-8454-862e473d00ff.html',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -166,6 +170,14 @@ class RaiPlayIE(RaiBaseIE):
|
|||||||
media = self._download_json(
|
media = self._download_json(
|
||||||
base + '.json', video_id, 'Downloading video JSON')
|
base + '.json', video_id, 'Downloading video JSON')
|
||||||
|
|
||||||
|
if not self._downloader.params.get('allow_unplayable_formats'):
|
||||||
|
if try_get(
|
||||||
|
media,
|
||||||
|
(lambda x: x['rights_management']['rights']['drm'],
|
||||||
|
lambda x: x['program_info']['rights_management']['rights']['drm']),
|
||||||
|
dict):
|
||||||
|
raise ExtractorError('This video is DRM protected.', expected=True)
|
||||||
|
|
||||||
title = media['name']
|
title = media['name']
|
||||||
video = media['video']
|
video = media['video']
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import io
|
||||||
import re
|
import re
|
||||||
import time
|
import sys
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
@@ -14,56 +15,13 @@ from ..utils import (
|
|||||||
determine_ext,
|
determine_ext,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
|
qualities,
|
||||||
remove_end,
|
remove_end,
|
||||||
remove_start,
|
remove_start,
|
||||||
sanitized_Request,
|
|
||||||
std_headers,
|
std_headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_bytes_to_chr = (lambda x: x) if sys.version_info[0] == 2 else (lambda x: map(chr, x))
|
||||||
def _decrypt_url(png):
|
|
||||||
encrypted_data = compat_b64decode(png)
|
|
||||||
text_index = encrypted_data.find(b'tEXt')
|
|
||||||
text_chunk = encrypted_data[text_index - 4:]
|
|
||||||
length = compat_struct_unpack('!I', text_chunk[:4])[0]
|
|
||||||
# Use bytearray to get integers when iterating in both python 2.x and 3.x
|
|
||||||
data = bytearray(text_chunk[8:8 + length])
|
|
||||||
data = [chr(b) for b in data if b != 0]
|
|
||||||
hash_index = data.index('#')
|
|
||||||
alphabet_data = data[:hash_index]
|
|
||||||
url_data = data[hash_index + 1:]
|
|
||||||
if url_data[0] == 'H' and url_data[3] == '%':
|
|
||||||
# remove useless HQ%% at the start
|
|
||||||
url_data = url_data[4:]
|
|
||||||
|
|
||||||
alphabet = []
|
|
||||||
e = 0
|
|
||||||
d = 0
|
|
||||||
for l in alphabet_data:
|
|
||||||
if d == 0:
|
|
||||||
alphabet.append(l)
|
|
||||||
d = e = (e + 1) % 4
|
|
||||||
else:
|
|
||||||
d -= 1
|
|
||||||
url = ''
|
|
||||||
f = 0
|
|
||||||
e = 3
|
|
||||||
b = 1
|
|
||||||
for letter in url_data:
|
|
||||||
if f == 0:
|
|
||||||
l = int(letter) * 10
|
|
||||||
f = 1
|
|
||||||
else:
|
|
||||||
if e == 0:
|
|
||||||
l += int(letter)
|
|
||||||
url += alphabet[l]
|
|
||||||
e = (b + 3) % 4
|
|
||||||
f = 0
|
|
||||||
b += 1
|
|
||||||
else:
|
|
||||||
e -= 1
|
|
||||||
|
|
||||||
return url
|
|
||||||
|
|
||||||
|
|
||||||
class RTVEALaCartaIE(InfoExtractor):
|
class RTVEALaCartaIE(InfoExtractor):
|
||||||
@@ -79,28 +37,31 @@ class RTVEALaCartaIE(InfoExtractor):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Balonmano - Swiss Cup masculina. Final: España-Suecia',
|
'title': 'Balonmano - Swiss Cup masculina. Final: España-Suecia',
|
||||||
'duration': 5024.566,
|
'duration': 5024.566,
|
||||||
|
'series': 'Balonmano',
|
||||||
},
|
},
|
||||||
|
'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
|
||||||
}, {
|
}, {
|
||||||
'note': 'Live stream',
|
'note': 'Live stream',
|
||||||
'url': 'http://www.rtve.es/alacarta/videos/television/24h-live/1694255/',
|
'url': 'http://www.rtve.es/alacarta/videos/television/24h-live/1694255/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1694255',
|
'id': '1694255',
|
||||||
'ext': 'flv',
|
'ext': 'mp4',
|
||||||
'title': 'TODO',
|
'title': 're:^24H LIVE [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
||||||
|
'is_live': True,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': 'live stream',
|
||||||
},
|
},
|
||||||
'skip': 'The f4m manifest can\'t be used yet',
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.rtve.es/alacarta/videos/servir-y-proteger/servir-proteger-capitulo-104/4236788/',
|
'url': 'http://www.rtve.es/alacarta/videos/servir-y-proteger/servir-proteger-capitulo-104/4236788/',
|
||||||
'md5': 'e55e162379ad587e9640eda4f7353c0f',
|
'md5': 'd850f3c8731ea53952ebab489cf81cbf',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '4236788',
|
'id': '4236788',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Servir y proteger - Capítulo 104 ',
|
'title': 'Servir y proteger - Capítulo 104',
|
||||||
'duration': 3222.0,
|
'duration': 3222.0,
|
||||||
},
|
},
|
||||||
'params': {
|
'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
|
||||||
'skip_download': True, # requires ffmpeg
|
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.rtve.es/m/alacarta/videos/cuentame-como-paso/cuentame-como-paso-t16-ultimo-minuto-nuestra-vida-capitulo-276/2969138/?media=tve',
|
'url': 'http://www.rtve.es/m/alacarta/videos/cuentame-como-paso/cuentame-como-paso-t16-ultimo-minuto-nuestra-vida-capitulo-276/2969138/?media=tve',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@@ -111,58 +72,102 @@ class RTVEALaCartaIE(InfoExtractor):
|
|||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
user_agent_b64 = base64.b64encode(std_headers['User-Agent'].encode('utf-8')).decode('utf-8')
|
user_agent_b64 = base64.b64encode(std_headers['User-Agent'].encode('utf-8')).decode('utf-8')
|
||||||
manager_info = self._download_json(
|
self._manager = self._download_json(
|
||||||
'http://www.rtve.es/odin/loki/' + user_agent_b64,
|
'http://www.rtve.es/odin/loki/' + user_agent_b64,
|
||||||
None, 'Fetching manager info')
|
None, 'Fetching manager info')['manager']
|
||||||
self._manager = manager_info['manager']
|
|
||||||
|
@staticmethod
|
||||||
|
def _decrypt_url(png):
|
||||||
|
encrypted_data = io.BytesIO(compat_b64decode(png)[8:])
|
||||||
|
while True:
|
||||||
|
length = compat_struct_unpack('!I', encrypted_data.read(4))[0]
|
||||||
|
chunk_type = encrypted_data.read(4)
|
||||||
|
if chunk_type == b'IEND':
|
||||||
|
break
|
||||||
|
data = encrypted_data.read(length)
|
||||||
|
if chunk_type == b'tEXt':
|
||||||
|
alphabet_data, text = data.split(b'\0')
|
||||||
|
quality, url_data = text.split(b'%%')
|
||||||
|
alphabet = []
|
||||||
|
e = 0
|
||||||
|
d = 0
|
||||||
|
for l in _bytes_to_chr(alphabet_data):
|
||||||
|
if d == 0:
|
||||||
|
alphabet.append(l)
|
||||||
|
d = e = (e + 1) % 4
|
||||||
|
else:
|
||||||
|
d -= 1
|
||||||
|
url = ''
|
||||||
|
f = 0
|
||||||
|
e = 3
|
||||||
|
b = 1
|
||||||
|
for letter in _bytes_to_chr(url_data):
|
||||||
|
if f == 0:
|
||||||
|
l = int(letter) * 10
|
||||||
|
f = 1
|
||||||
|
else:
|
||||||
|
if e == 0:
|
||||||
|
l += int(letter)
|
||||||
|
url += alphabet[l]
|
||||||
|
e = (b + 3) % 4
|
||||||
|
f = 0
|
||||||
|
b += 1
|
||||||
|
else:
|
||||||
|
e -= 1
|
||||||
|
|
||||||
|
yield quality.decode(), url
|
||||||
|
encrypted_data.read(4) # CRC
|
||||||
|
|
||||||
|
def _extract_png_formats(self, video_id):
|
||||||
|
png = self._download_webpage(
|
||||||
|
'http://www.rtve.es/ztnr/movil/thumbnail/%s/videos/%s.png' % (self._manager, video_id),
|
||||||
|
video_id, 'Downloading url information', query={'q': 'v2'})
|
||||||
|
q = qualities(['Media', 'Alta', 'HQ', 'HD_READY', 'HD_FULL'])
|
||||||
|
formats = []
|
||||||
|
for quality, video_url in self._decrypt_url(png):
|
||||||
|
ext = determine_ext(video_url)
|
||||||
|
if ext == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
video_url, video_id, 'mp4', 'm3u8_native',
|
||||||
|
m3u8_id='hls', fatal=False))
|
||||||
|
elif ext == 'mpd':
|
||||||
|
formats.extend(self._extract_mpd_formats(
|
||||||
|
video_url, video_id, 'dash', fatal=False))
|
||||||
|
else:
|
||||||
|
formats.append({
|
||||||
|
'format_id': quality,
|
||||||
|
'quality': q(quality),
|
||||||
|
'url': video_url,
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
return formats
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
video_id = self._match_id(url)
|
||||||
video_id = mobj.group('id')
|
|
||||||
info = self._download_json(
|
info = self._download_json(
|
||||||
'http://www.rtve.es/api/videos/%s/config/alacarta_videos.json' % video_id,
|
'http://www.rtve.es/api/videos/%s/config/alacarta_videos.json' % video_id,
|
||||||
video_id)['page']['items'][0]
|
video_id)['page']['items'][0]
|
||||||
if info['state'] == 'DESPU':
|
if info['state'] == 'DESPU':
|
||||||
raise ExtractorError('The video is no longer available', expected=True)
|
raise ExtractorError('The video is no longer available', expected=True)
|
||||||
title = info['title']
|
title = info['title'].strip()
|
||||||
png_url = 'http://www.rtve.es/ztnr/movil/thumbnail/%s/videos/%s.png' % (self._manager, video_id)
|
formats = self._extract_png_formats(video_id)
|
||||||
png_request = sanitized_Request(png_url)
|
|
||||||
png_request.add_header('Referer', url)
|
|
||||||
png = self._download_webpage(png_request, video_id, 'Downloading url information')
|
|
||||||
video_url = _decrypt_url(png)
|
|
||||||
ext = determine_ext(video_url)
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
if not video_url.endswith('.f4m') and ext != 'm3u8':
|
|
||||||
if '?' not in video_url:
|
|
||||||
video_url = video_url.replace('resources/', 'auth/resources/')
|
|
||||||
video_url = video_url.replace('.net.rtve', '.multimedia.cdn.rtve')
|
|
||||||
|
|
||||||
if ext == 'm3u8':
|
|
||||||
formats.extend(self._extract_m3u8_formats(
|
|
||||||
video_url, video_id, ext='mp4', entry_protocol='m3u8_native',
|
|
||||||
m3u8_id='hls', fatal=False))
|
|
||||||
elif ext == 'f4m':
|
|
||||||
formats.extend(self._extract_f4m_formats(
|
|
||||||
video_url, video_id, f4m_id='hds', fatal=False))
|
|
||||||
else:
|
|
||||||
formats.append({
|
|
||||||
'url': video_url,
|
|
||||||
})
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
subtitles = None
|
subtitles = None
|
||||||
if info.get('sbtFile') is not None:
|
sbt_file = info.get('sbtFile')
|
||||||
subtitles = self.extract_subtitles(video_id, info['sbtFile'])
|
if sbt_file:
|
||||||
|
subtitles = self.extract_subtitles(video_id, sbt_file)
|
||||||
|
|
||||||
|
is_live = info.get('live') is True
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': self._live_title(title) if is_live else title,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'thumbnail': info.get('image'),
|
'thumbnail': info.get('image'),
|
||||||
'page_url': url,
|
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
'duration': float_or_none(info.get('duration'), scale=1000),
|
'duration': float_or_none(info.get('duration'), 1000),
|
||||||
|
'is_live': is_live,
|
||||||
|
'series': info.get('programTitle'),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_subtitles(self, video_id, sub_file):
|
def _get_subtitles(self, video_id, sub_file):
|
||||||
@@ -174,48 +179,26 @@ class RTVEALaCartaIE(InfoExtractor):
|
|||||||
for s in subs)
|
for s in subs)
|
||||||
|
|
||||||
|
|
||||||
class RTVEInfantilIE(InfoExtractor):
|
class RTVEInfantilIE(RTVEALaCartaIE):
|
||||||
IE_NAME = 'rtve.es:infantil'
|
IE_NAME = 'rtve.es:infantil'
|
||||||
IE_DESC = 'RTVE infantil'
|
IE_DESC = 'RTVE infantil'
|
||||||
_VALID_URL = r'https?://(?:www\.)?rtve\.es/infantil/serie/(?P<show>[^/]*)/video/(?P<short_title>[^/]*)/(?P<id>[0-9]+)/'
|
_VALID_URL = r'https?://(?:www\.)?rtve\.es/infantil/serie/[^/]+/video/[^/]+/(?P<id>[0-9]+)/'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.rtve.es/infantil/serie/cleo/video/maneras-vivir/3040283/',
|
'url': 'http://www.rtve.es/infantil/serie/cleo/video/maneras-vivir/3040283/',
|
||||||
'md5': '915319587b33720b8e0357caaa6617e6',
|
'md5': '5747454717aedf9f9fdf212d1bcfc48d',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '3040283',
|
'id': '3040283',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Maneras de vivir',
|
'title': 'Maneras de vivir',
|
||||||
'thumbnail': 'http://www.rtve.es/resources/jpg/6/5/1426182947956.JPG',
|
'thumbnail': r're:https?://.+/1426182947956\.JPG',
|
||||||
'duration': 357.958,
|
'duration': 357.958,
|
||||||
},
|
},
|
||||||
|
'expected_warnings': ['Failed to download MPD manifest', 'Failed to download m3u8 information'],
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id = self._match_id(url)
|
|
||||||
info = self._download_json(
|
|
||||||
'http://www.rtve.es/api/videos/%s/config/alacarta_videos.json' % video_id,
|
|
||||||
video_id)['page']['items'][0]
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
class RTVELiveIE(RTVEALaCartaIE):
|
||||||
vidplayer_id = self._search_regex(
|
|
||||||
r' id="vidplayer([0-9]+)"', webpage, 'internal video ID')
|
|
||||||
|
|
||||||
png_url = 'http://www.rtve.es/ztnr/movil/thumbnail/default/videos/%s.png' % vidplayer_id
|
|
||||||
png = self._download_webpage(png_url, video_id, 'Downloading url information')
|
|
||||||
video_url = _decrypt_url(png)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': info['title'],
|
|
||||||
'url': video_url,
|
|
||||||
'thumbnail': info.get('image'),
|
|
||||||
'duration': float_or_none(info.get('duration'), scale=1000),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class RTVELiveIE(InfoExtractor):
|
|
||||||
IE_NAME = 'rtve.es:live'
|
IE_NAME = 'rtve.es:live'
|
||||||
IE_DESC = 'RTVE.es live streams'
|
IE_DESC = 'RTVE.es live streams'
|
||||||
_VALID_URL = r'https?://(?:www\.)?rtve\.es/directo/(?P<id>[a-zA-Z0-9-]+)'
|
_VALID_URL = r'https?://(?:www\.)?rtve\.es/directo/(?P<id>[a-zA-Z0-9-]+)'
|
||||||
@@ -225,7 +208,7 @@ class RTVELiveIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'la-1',
|
'id': 'la-1',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 're:^La 1 [0-9]{4}-[0-9]{2}-[0-9]{2}Z[0-9]{6}$',
|
'title': 're:^La 1 [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': 'live stream',
|
'skip_download': 'live stream',
|
||||||
@@ -234,29 +217,22 @@ class RTVELiveIE(InfoExtractor):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
start_time = time.gmtime()
|
|
||||||
video_id = mobj.group('id')
|
video_id = mobj.group('id')
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
title = remove_end(self._og_search_title(webpage), ' en directo en RTVE.es')
|
title = remove_end(self._og_search_title(webpage), ' en directo en RTVE.es')
|
||||||
title = remove_start(title, 'Estoy viendo ')
|
title = remove_start(title, 'Estoy viendo ')
|
||||||
title += ' ' + time.strftime('%Y-%m-%dZ%H%M%S', start_time)
|
|
||||||
|
|
||||||
vidplayer_id = self._search_regex(
|
vidplayer_id = self._search_regex(
|
||||||
(r'playerId=player([0-9]+)',
|
(r'playerId=player([0-9]+)',
|
||||||
r'class=["\'].*?\blive_mod\b.*?["\'][^>]+data-assetid=["\'](\d+)',
|
r'class=["\'].*?\blive_mod\b.*?["\'][^>]+data-assetid=["\'](\d+)',
|
||||||
r'data-id=["\'](\d+)'),
|
r'data-id=["\'](\d+)'),
|
||||||
webpage, 'internal video ID')
|
webpage, 'internal video ID')
|
||||||
png_url = 'http://www.rtve.es/ztnr/movil/thumbnail/amonet/videos/%s.png' % vidplayer_id
|
|
||||||
png = self._download_webpage(png_url, video_id, 'Downloading url information')
|
|
||||||
m3u8_url = _decrypt_url(png)
|
|
||||||
formats = self._extract_m3u8_formats(m3u8_url, video_id, ext='mp4')
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': self._live_title(title),
|
||||||
'formats': formats,
|
'formats': self._extract_png_formats(vidplayer_id),
|
||||||
'is_live': True,
|
'is_live': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,13 +51,16 @@ class ShahidIE(ShahidBaseIE):
|
|||||||
_NETRC_MACHINE = 'shahid'
|
_NETRC_MACHINE = 'shahid'
|
||||||
_VALID_URL = r'https?://shahid\.mbc\.net/ar/(?:serie|show|movie)s/[^/]+/(?P<type>episode|clip|movie)-(?P<id>\d+)'
|
_VALID_URL = r'https?://shahid\.mbc\.net/ar/(?:serie|show|movie)s/[^/]+/(?P<type>episode|clip|movie)-(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://shahid.mbc.net/ar/shows/%D9%85%D8%AC%D9%84%D8%B3-%D8%A7%D9%84%D8%B4%D8%A8%D8%A7%D8%A8-%D8%A7%D9%84%D9%85%D9%88%D8%B3%D9%85-1-%D9%83%D9%84%D9%8A%D8%A8-1/clip-275286',
|
'url': 'https://shahid.mbc.net/ar/shows/%D9%85%D8%AA%D8%AD%D9%81-%D8%A7%D9%84%D8%AF%D8%AD%D9%8A%D8%AD-%D8%A7%D9%84%D9%85%D9%88%D8%B3%D9%85-1-%D9%83%D9%84%D9%8A%D8%A8-1/clip-816924',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '275286',
|
'id': '816924',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'مجلس الشباب الموسم 1 كليب 1',
|
'title': 'متحف الدحيح الموسم 1 كليب 1',
|
||||||
'timestamp': 1506988800,
|
'timestamp': 1602806400,
|
||||||
'upload_date': '20171003',
|
'upload_date': '20201016',
|
||||||
|
'description': 'برومو',
|
||||||
|
'duration': 22,
|
||||||
|
'categories': ['كوميديا'],
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
@@ -109,12 +112,15 @@ class ShahidIE(ShahidBaseIE):
|
|||||||
page_type = 'episode'
|
page_type = 'episode'
|
||||||
|
|
||||||
playout = self._call_api(
|
playout = self._call_api(
|
||||||
'playout/url/' + video_id, video_id)['playout']
|
'playout/new/url/' + video_id, video_id)['playout']
|
||||||
|
|
||||||
if not self._downloader.params.get('allow_unplayable_formats') and playout.get('drm'):
|
if not self._downloader.params.get('allow_unplayable_formats') and playout.get('drm'):
|
||||||
raise ExtractorError('This video is DRM protected.', expected=True)
|
raise ExtractorError('This video is DRM protected.', expected=True)
|
||||||
|
|
||||||
formats = self._extract_m3u8_formats(playout['url'], video_id, 'mp4')
|
formats = self._extract_m3u8_formats(re.sub(
|
||||||
|
# https://docs.aws.amazon.com/mediapackage/latest/ug/manifest-filtering.html
|
||||||
|
r'aws\.manifestfilter=[\w:;,-]+&?',
|
||||||
|
'', playout['url']), video_id, 'mp4')
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
# video = self._call_api(
|
# video = self._call_api(
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ from .mtv import MTVServicesInfoExtractor
|
|||||||
|
|
||||||
class SouthParkIE(MTVServicesInfoExtractor):
|
class SouthParkIE(MTVServicesInfoExtractor):
|
||||||
IE_NAME = 'southpark.cc.com'
|
IE_NAME = 'southpark.cc.com'
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?P<url>southpark\.cc\.com/(?:clips|(?:full-)?episodes|collections)/(?P<id>.+?)(\?|#|$))'
|
_VALID_URL = r'https?://(?:www\.)?(?P<url>southpark(?:\.cc|studios)\.com/(?:clips|(?:full-)?episodes|collections)/(?P<id>.+?)(\?|#|$))'
|
||||||
|
|
||||||
_FEED_URL = 'http://www.southparkstudios.com/feeds/video-player/mrss'
|
_FEED_URL = 'http://feeds.mtvnservices.com/od/feed/intl-mrss-player-feed'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://southpark.cc.com/clips/104437/bat-daded#tab=featured',
|
'url': 'http://southpark.cc.com/clips/104437/bat-daded#tab=featured',
|
||||||
@@ -23,8 +23,20 @@ class SouthParkIE(MTVServicesInfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://southpark.cc.com/collections/7758/fan-favorites/1',
|
'url': 'http://southpark.cc.com/collections/7758/fan-favorites/1',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.southparkstudios.com/episodes/h4o269/south-park-stunning-and-brave-season-19-ep-1',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
def _get_feed_query(self, uri):
|
||||||
|
return {
|
||||||
|
'accountOverride': 'intl.mtvi.com',
|
||||||
|
'arcEp': 'shared.southpark.global',
|
||||||
|
'ep': '90877963',
|
||||||
|
'imageEp': 'shared.southpark.global',
|
||||||
|
'mgid': uri,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SouthParkEsIE(SouthParkIE):
|
class SouthParkEsIE(SouthParkIE):
|
||||||
IE_NAME = 'southpark.cc.com:español'
|
IE_NAME = 'southpark.cc.com:español'
|
||||||
|
|||||||
@@ -1,82 +1,105 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..compat import (
|
||||||
|
compat_parse_qs,
|
||||||
|
compat_urllib_parse_urlparse,
|
||||||
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
clean_html,
|
||||||
|
float_or_none,
|
||||||
|
int_or_none,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
sanitized_Request,
|
strip_or_none,
|
||||||
|
try_get,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SportDeutschlandIE(InfoExtractor):
|
class SportDeutschlandIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://sportdeutschland\.tv/(?P<sport>[^/?#]+)/(?P<id>[^?#/]+)(?:$|[?#])'
|
_VALID_URL = r'https?://sportdeutschland\.tv/(?P<id>(?:[^/]+/)?[^?#/&]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://sportdeutschland.tv/badminton/re-live-deutsche-meisterschaften-2020-halbfinals?playlistId=0',
|
'url': 'https://sportdeutschland.tv/badminton/re-live-deutsche-meisterschaften-2020-halbfinals?playlistId=0',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 're-live-deutsche-meisterschaften-2020-halbfinals',
|
'id': '5318cac0275701382770543d7edaf0a0',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 're:Re-live: Deutsche Meisterschaften 2020.*Halbfinals',
|
'title': 'Re-live: Deutsche Meisterschaften 2020 - Halbfinals - Teil 1',
|
||||||
'categories': ['Badminton-Deutschland'],
|
'duration': 16106.36,
|
||||||
'view_count': int,
|
|
||||||
'thumbnail': r're:^https?://.*\.(?:jpg|png)$',
|
|
||||||
'timestamp': int,
|
|
||||||
'upload_date': '20200201',
|
|
||||||
'description': 're:.*', # meaningless description for THIS video
|
|
||||||
},
|
},
|
||||||
|
'params': {
|
||||||
|
'noplaylist': True,
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://sportdeutschland.tv/badminton/re-live-deutsche-meisterschaften-2020-halbfinals?playlistId=0',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'c6e2fdd01f63013854c47054d2ab776f',
|
||||||
|
'title': 'Re-live: Deutsche Meisterschaften 2020 - Halbfinals',
|
||||||
|
'description': 'md5:5263ff4c31c04bb780c9f91130b48530',
|
||||||
|
'duration': 31397,
|
||||||
|
},
|
||||||
|
'playlist_count': 2,
|
||||||
|
}, {
|
||||||
|
'url': 'https://sportdeutschland.tv/freeride-world-tour-2021-fieberbrunn-oesterreich',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
display_id = self._match_id(url)
|
||||||
video_id = mobj.group('id')
|
data = self._download_json(
|
||||||
sport_id = mobj.group('sport')
|
'https://backend.sportdeutschland.tv/api/permalinks/' + display_id,
|
||||||
|
display_id, query={'access_token': 'true'})
|
||||||
api_url = 'https://proxy.vidibusdynamic.net/ssl/backend.sportdeutschland.tv/api/permalinks/%s/%s?access_token=true' % (
|
|
||||||
sport_id, video_id)
|
|
||||||
req = sanitized_Request(api_url, headers={
|
|
||||||
'Accept': 'application/vnd.vidibus.v2.html+json',
|
|
||||||
'Referer': url,
|
|
||||||
})
|
|
||||||
data = self._download_json(req, video_id)
|
|
||||||
|
|
||||||
asset = data['asset']
|
asset = data['asset']
|
||||||
categories = [data['section']['title']]
|
title = (asset.get('title') or asset['label']).strip()
|
||||||
|
asset_id = asset.get('id') or asset.get('uuid')
|
||||||
formats = []
|
info = {
|
||||||
smil_url = asset['video']
|
'id': asset_id,
|
||||||
if '.smil' in smil_url:
|
'title': title,
|
||||||
m3u8_url = smil_url.replace('.smil', '.m3u8')
|
'description': clean_html(asset.get('body') or asset.get('description')) or asset.get('teaser'),
|
||||||
formats.extend(
|
'duration': int_or_none(asset.get('seconds')),
|
||||||
self._extract_m3u8_formats(m3u8_url, video_id, ext='mp4'))
|
}
|
||||||
|
videos = asset.get('videos') or []
|
||||||
smil_doc = self._download_xml(
|
if len(videos) > 1:
|
||||||
smil_url, video_id, note='Downloading SMIL metadata')
|
playlist_id = compat_parse_qs(compat_urllib_parse_urlparse(url).query).get('playlistId', [None])[0]
|
||||||
base_url_el = smil_doc.find('./head/meta')
|
if playlist_id:
|
||||||
if base_url_el:
|
if self._downloader.params.get('noplaylist'):
|
||||||
base_url = base_url_el.attrib['base']
|
videos = [videos[int(playlist_id)]]
|
||||||
formats.extend([{
|
self.to_screen('Downloading just a single video because of --no-playlist')
|
||||||
'format_id': 'rmtp',
|
|
||||||
'url': base_url if base_url_el else n.attrib['src'],
|
|
||||||
'play_path': n.attrib['src'],
|
|
||||||
'ext': 'flv',
|
|
||||||
'preference': -100,
|
|
||||||
'format_note': 'Seems to fail at example stream',
|
|
||||||
} for n in smil_doc.findall('./body/video')])
|
|
||||||
else:
|
else:
|
||||||
formats.append({'url': smil_url})
|
self.to_screen('Downloading playlist %s - add --no-playlist to just download video' % asset_id)
|
||||||
|
|
||||||
self._sort_formats(formats)
|
def entries():
|
||||||
|
for i, video in enumerate(videos, 1):
|
||||||
return {
|
video_id = video.get('uuid')
|
||||||
|
video_url = video.get('url')
|
||||||
|
if not (video_id and video_url):
|
||||||
|
continue
|
||||||
|
formats = self._extract_m3u8_formats(
|
||||||
|
video_url.replace('.smil', '.m3u8'), video_id, 'mp4', fatal=False)
|
||||||
|
if not formats:
|
||||||
|
continue
|
||||||
|
yield {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'title': asset['title'],
|
'title': title + ' - ' + (video.get('label') or 'Teil %d' % i),
|
||||||
'thumbnail': asset.get('image'),
|
'duration': float_or_none(video.get('duration')),
|
||||||
'description': asset.get('teaser'),
|
|
||||||
'duration': asset.get('duration'),
|
|
||||||
'categories': categories,
|
|
||||||
'view_count': asset.get('views'),
|
|
||||||
'rtmp_live': asset.get('live'),
|
|
||||||
'timestamp': parse_iso8601(asset.get('date')),
|
|
||||||
}
|
}
|
||||||
|
info.update({
|
||||||
|
'_type': 'multi_video',
|
||||||
|
'entries': entries(),
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
formats = self._extract_m3u8_formats(
|
||||||
|
videos[0]['url'].replace('.smil', '.m3u8'), asset_id, 'mp4')
|
||||||
|
section_title = strip_or_none(try_get(data, lambda x: x['section']['title']))
|
||||||
|
info.update({
|
||||||
|
'formats': formats,
|
||||||
|
'display_id': asset.get('permalink'),
|
||||||
|
'thumbnail': try_get(asset, lambda x: x['images'][0]),
|
||||||
|
'categories': [section_title] if section_title else None,
|
||||||
|
'view_count': int_or_none(asset.get('views')),
|
||||||
|
'is_live': asset.get('is_live') is True,
|
||||||
|
'timestamp': parse_iso8601(asset.get('date') or asset.get('published_at')),
|
||||||
|
})
|
||||||
|
return info
|
||||||
|
|||||||
@@ -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'):
|
|
||||||
|
if protocol in ('HDS', 'HLS'):
|
||||||
|
if source.get('tokenType') == 'AKAMAI':
|
||||||
|
format_url = self._get_tokenized_src(
|
||||||
|
format_url, media_id, format_id)
|
||||||
|
formats.extend(self._extract_akamai_formats(
|
||||||
|
format_url, media_id))
|
||||||
|
elif protocol == 'HLS':
|
||||||
formats.extend(self._extract_m3u8_formats(
|
formats.extend(self._extract_m3u8_formats(
|
||||||
asset_url, media_id, 'mp4', 'm3u8_native',
|
format_url, media_id, 'mp4', 'm3u8_native',
|
||||||
m3u8_id=format_id, fatal=False))
|
m3u8_id=format_id, fatal=False))
|
||||||
else:
|
elif protocol in ('HTTP', 'HTTPS'):
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
'url': asset_url,
|
'url': format_url,
|
||||||
'quality': preference(quality),
|
'quality': q(quality),
|
||||||
'ext': 'flv' if protocol == 'RTMP' else None,
|
})
|
||||||
|
|
||||||
|
# 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')
|
||||||
|
|||||||
@@ -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'),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from ..utils import (
|
|||||||
|
|
||||||
class TrovoBaseIE(InfoExtractor):
|
class TrovoBaseIE(InfoExtractor):
|
||||||
_VALID_URL_BASE = r'https?://(?:www\.)?trovo\.live/'
|
_VALID_URL_BASE = r'https?://(?:www\.)?trovo\.live/'
|
||||||
|
_HEADERS = {'Origin': 'https://trovo.live'}
|
||||||
|
|
||||||
def _extract_streamer_info(self, data):
|
def _extract_streamer_info(self, data):
|
||||||
streamer_info = data.get('streamerInfo') or {}
|
streamer_info = data.get('streamerInfo') or {}
|
||||||
@@ -68,6 +69,7 @@ class TrovoIE(TrovoBaseIE):
|
|||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
'height': int_or_none(format_id[:-1]) if format_id else None,
|
'height': int_or_none(format_id[:-1]) if format_id else None,
|
||||||
'url': play_url,
|
'url': play_url,
|
||||||
|
'http_headers': self._HEADERS,
|
||||||
})
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
@@ -153,6 +155,7 @@ class TrovoVodIE(TrovoBaseIE):
|
|||||||
'protocol': 'm3u8_native',
|
'protocol': 'm3u8_native',
|
||||||
'tbr': int_or_none(play_info.get('bitrate')),
|
'tbr': int_or_none(play_info.get('bitrate')),
|
||||||
'url': play_url,
|
'url': play_url,
|
||||||
|
'http_headers': self._HEADERS,
|
||||||
})
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
remove_start,
|
remove_start,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
|
strip_or_none,
|
||||||
try_get,
|
try_get,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,6 +26,10 @@ class TVerIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://tver.jp/episode/79622438',
|
'url': 'https://tver.jp/episode/79622438',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# subtitle = ' '
|
||||||
|
'url': 'https://tver.jp/corner/f0068870',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
_TOKEN = None
|
_TOKEN = None
|
||||||
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s'
|
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s'
|
||||||
@@ -47,8 +52,12 @@ class TVerIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if service == 'cx':
|
if service == 'cx':
|
||||||
|
title = main['title']
|
||||||
|
subtitle = strip_or_none(main.get('subtitle'))
|
||||||
|
if subtitle:
|
||||||
|
title += ' - ' + subtitle
|
||||||
info.update({
|
info.update({
|
||||||
'title': main.get('subtitle') or main['title'],
|
'title': title,
|
||||||
'url': 'https://i.fod.fujitv.co.jp/plus7/web/%s/%s.html' % (p_id[:4], p_id),
|
'url': 'https://i.fod.fujitv.co.jp/plus7/web/%s/%s.html' % (p_id[:4], p_id),
|
||||||
'ie_key': 'FujiTVFODPlus7',
|
'ie_key': 'FujiTVFODPlus7',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -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,24 +279,6 @@ class VikiIE(VikiBaseIE):
|
|||||||
})
|
})
|
||||||
|
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
try:
|
|
||||||
# New way to fetch subtitles
|
|
||||||
new_video = self._download_json(
|
|
||||||
'https://www.viki.com/api/videos/%s' % video_id, video_id,
|
|
||||||
'Downloading new video JSON to get subtitles', fatal=False,
|
|
||||||
headers={
|
|
||||||
'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():
|
for subtitle_lang, _ in (video.get('subtitle_completions') or {}).items():
|
||||||
subtitles[subtitle_lang] = [{
|
subtitles[subtitle_lang] = [{
|
||||||
'ext': subtitles_format,
|
'ext': subtitles_format,
|
||||||
@@ -386,9 +361,6 @@ 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():
|
|
||||||
add_format(format_id, format_dict)
|
|
||||||
if not formats:
|
|
||||||
streams = self._call_api(
|
streams = self._call_api(
|
||||||
'videos/%s/streams.json' % video_id, video_id,
|
'videos/%s/streams.json' % video_id, video_id,
|
||||||
'Downloading video streams JSON')
|
'Downloading video streams JSON')
|
||||||
|
|||||||
@@ -498,6 +498,24 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'url': 'https://vimeo.com/album/2632481/video/79010983',
|
'url': 'https://vimeo.com/album/2632481/video/79010983',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://vimeo.com/showcase/3253534/video/119195465',
|
||||||
|
'note': 'A video in a password protected album (showcase)',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '119195465',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'youtube-dl test video \'ä"BaW_jenozKc',
|
||||||
|
'uploader': 'Philipp Hagemeister',
|
||||||
|
'uploader_id': 'user20132939',
|
||||||
|
'description': 'md5:fa7b6c6d8db0bdc353893df2f111855b',
|
||||||
|
'upload_date': '20150209',
|
||||||
|
'timestamp': 1423518307,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'format': 'best[protocol=https]',
|
||||||
|
'videopassword': 'youtube-dl',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
# source file returns 403: Forbidden
|
# source file returns 403: Forbidden
|
||||||
'url': 'https://vimeo.com/7809605',
|
'url': 'https://vimeo.com/7809605',
|
||||||
@@ -564,6 +582,44 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
self._login()
|
self._login()
|
||||||
|
|
||||||
|
def _try_album_password(self, url):
|
||||||
|
album_id = self._search_regex(
|
||||||
|
r'vimeo\.com/(?:album|showcase)/([^/]+)', url, 'album id', default=None)
|
||||||
|
if not album_id:
|
||||||
|
return
|
||||||
|
viewer = self._download_json(
|
||||||
|
'https://vimeo.com/_rv/viewer', album_id, fatal=False)
|
||||||
|
if not viewer:
|
||||||
|
webpage = self._download_webpage(url, album_id)
|
||||||
|
viewer = self._parse_json(self._search_regex(
|
||||||
|
r'bootstrap_data\s*=\s*({.+?})</script>',
|
||||||
|
webpage, 'bootstrap data'), album_id)['viewer']
|
||||||
|
jwt = viewer['jwt']
|
||||||
|
album = self._download_json(
|
||||||
|
'https://api.vimeo.com/albums/' + album_id,
|
||||||
|
album_id, headers={'Authorization': 'jwt ' + jwt},
|
||||||
|
query={'fields': 'description,name,privacy'})
|
||||||
|
if try_get(album, lambda x: x['privacy']['view']) == 'password':
|
||||||
|
password = self._downloader.params.get('videopassword')
|
||||||
|
if not password:
|
||||||
|
raise ExtractorError(
|
||||||
|
'This album is protected by a password, use the --video-password option',
|
||||||
|
expected=True)
|
||||||
|
self._set_vimeo_cookie('vuid', viewer['vuid'])
|
||||||
|
try:
|
||||||
|
self._download_json(
|
||||||
|
'https://vimeo.com/showcase/%s/auth' % album_id,
|
||||||
|
album_id, 'Verifying the password', data=urlencode_postdata({
|
||||||
|
'password': password,
|
||||||
|
'token': viewer['xsrft'],
|
||||||
|
}), headers={
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
})
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
|
||||||
|
raise ExtractorError('Wrong password', expected=True)
|
||||||
|
raise
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
url, data = unsmuggle_url(url, {})
|
url, data = unsmuggle_url(url, {})
|
||||||
headers = std_headers.copy()
|
headers = std_headers.copy()
|
||||||
@@ -591,6 +647,7 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
elif any(p in url for p in ('play_redirect_hls', 'moogaloop.swf')):
|
elif any(p in url for p in ('play_redirect_hls', 'moogaloop.swf')):
|
||||||
url = 'https://vimeo.com/' + video_id
|
url = 'https://vimeo.com/' + video_id
|
||||||
|
|
||||||
|
self._try_album_password(url)
|
||||||
try:
|
try:
|
||||||
# Retrieve video webpage to extract further information
|
# Retrieve video webpage to extract further information
|
||||||
webpage, urlh = self._download_webpage_handle(
|
webpage, urlh = self._download_webpage_handle(
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ from ..compat import compat_urllib_parse_unquote
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
try_get,
|
||||||
|
unified_timestamp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -19,14 +21,17 @@ class VoxMediaVolumeIE(OnceIE):
|
|||||||
|
|
||||||
setup = self._parse_json(self._search_regex(
|
setup = self._parse_json(self._search_regex(
|
||||||
r'setup\s*=\s*({.+});', webpage, 'setup'), video_id)
|
r'setup\s*=\s*({.+});', webpage, 'setup'), video_id)
|
||||||
video_data = setup.get('video') or {}
|
player_setup = setup.get('player_setup') or setup
|
||||||
|
video_data = player_setup.get('video') or {}
|
||||||
|
formatted_metadata = video_data.get('formatted_metadata') or {}
|
||||||
info = {
|
info = {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': video_data.get('title_short'),
|
'title': player_setup.get('title') or video_data.get('title_short'),
|
||||||
'description': video_data.get('description_long') or video_data.get('description_short'),
|
'description': video_data.get('description_long') or video_data.get('description_short'),
|
||||||
'thumbnail': video_data.get('brightcove_thumbnail')
|
'thumbnail': formatted_metadata.get('thumbnail') or video_data.get('brightcove_thumbnail'),
|
||||||
|
'timestamp': unified_timestamp(formatted_metadata.get('video_publish_date')),
|
||||||
}
|
}
|
||||||
asset = setup.get('asset') or setup.get('params') or {}
|
asset = try_get(setup, lambda x: x['embed_assets']['chorus'], dict) or {}
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
hls_url = asset.get('hls_url')
|
hls_url = asset.get('hls_url')
|
||||||
@@ -47,6 +52,7 @@ class VoxMediaVolumeIE(OnceIE):
|
|||||||
if formats:
|
if formats:
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
info['formats'] = formats
|
info['formats'] = formats
|
||||||
|
info['duration'] = int_or_none(asset.get('duration'))
|
||||||
return info
|
return info
|
||||||
|
|
||||||
for provider_video_type in ('ooyala', 'youtube', 'brightcove'):
|
for provider_video_type in ('ooyala', 'youtube', 'brightcove'):
|
||||||
@@ -84,7 +90,7 @@ class VoxMediaIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
# Volume embed, Youtube
|
# Volume embed, Youtube
|
||||||
'url': 'http://www.theverge.com/2014/10/21/7025853/google-nexus-6-hands-on-photos-video-android-phablet',
|
'url': 'http://www.theverge.com/2014/10/21/7025853/google-nexus-6-hands-on-photos-video-android-phablet',
|
||||||
'md5': '4c8f4a0937752b437c3ebc0ed24802b5',
|
'md5': 'fd19aa0cf3a0eea515d4fd5c8c0e9d68',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'Gy8Md3Eky38',
|
'id': 'Gy8Md3Eky38',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -93,6 +99,7 @@ class VoxMediaIE(InfoExtractor):
|
|||||||
'uploader_id': 'TheVerge',
|
'uploader_id': 'TheVerge',
|
||||||
'upload_date': '20141021',
|
'upload_date': '20141021',
|
||||||
'uploader': 'The Verge',
|
'uploader': 'The Verge',
|
||||||
|
'timestamp': 1413907200,
|
||||||
},
|
},
|
||||||
'add_ie': ['Youtube'],
|
'add_ie': ['Youtube'],
|
||||||
'skip': 'similar to the previous test',
|
'skip': 'similar to the previous test',
|
||||||
@@ -100,13 +107,13 @@ class VoxMediaIE(InfoExtractor):
|
|||||||
# Volume embed, Youtube
|
# Volume embed, Youtube
|
||||||
'url': 'http://www.vox.com/2016/3/31/11336640/mississippi-lgbt-religious-freedom-bill',
|
'url': 'http://www.vox.com/2016/3/31/11336640/mississippi-lgbt-religious-freedom-bill',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'YCjDnX-Xzhg',
|
'id': '22986359b',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': "Mississippi's laws are so bad that its anti-LGBTQ law isn't needed to allow discrimination",
|
'title': "Mississippi's laws are so bad that its anti-LGBTQ law isn't needed to allow discrimination",
|
||||||
'description': 'md5:fc1317922057de31cd74bce91eb1c66c',
|
'description': 'md5:fc1317922057de31cd74bce91eb1c66c',
|
||||||
'uploader_id': 'voxdotcom',
|
|
||||||
'upload_date': '20150915',
|
'upload_date': '20150915',
|
||||||
'uploader': 'Vox',
|
'timestamp': 1442332800,
|
||||||
|
'duration': 285,
|
||||||
},
|
},
|
||||||
'add_ie': ['Youtube'],
|
'add_ie': ['Youtube'],
|
||||||
'skip': 'similar to the previous test',
|
'skip': 'similar to the previous test',
|
||||||
@@ -160,6 +167,9 @@ class VoxMediaIE(InfoExtractor):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Post-Post-PC CEO: The Full Code Conference Video of Microsoft\'s Satya Nadella',
|
'title': 'Post-Post-PC CEO: The Full Code Conference Video of Microsoft\'s Satya Nadella',
|
||||||
'description': 'The longtime veteran was chosen earlier this year as the software giant\'s third leader in its history.',
|
'description': 'The longtime veteran was chosen earlier this year as the software giant\'s third leader in its history.',
|
||||||
|
'timestamp': 1402938000,
|
||||||
|
'upload_date': '20140616',
|
||||||
|
'duration': 4114,
|
||||||
},
|
},
|
||||||
'add_ie': ['VoxMediaVolume'],
|
'add_ie': ['VoxMediaVolume'],
|
||||||
}]
|
}]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
163
yt_dlp/extractor/wimtv.py
Normal file
163
yt_dlp/extractor/wimtv.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
determine_ext,
|
||||||
|
parse_duration,
|
||||||
|
urlencode_postdata,
|
||||||
|
ExtractorError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WimTVIE(InfoExtractor):
|
||||||
|
_player = None
|
||||||
|
_UUID_RE = r'[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}'
|
||||||
|
_VALID_URL = r'''(?x)
|
||||||
|
https?://platform.wim.tv/
|
||||||
|
(?:
|
||||||
|
(?:embed/)?\?
|
||||||
|
|\#/webtv/.+?/
|
||||||
|
)
|
||||||
|
(?P<type>vod|live|cast)[=/]
|
||||||
|
(?P<id>%s).*?''' % _UUID_RE
|
||||||
|
_TESTS = [{
|
||||||
|
# vod stream
|
||||||
|
'url': 'https://platform.wim.tv/embed/?vod=db29fb32-bade-47b6-a3a6-cb69fe80267a',
|
||||||
|
'md5': 'db29fb32-bade-47b6-a3a6-cb69fe80267a',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'db29fb32-bade-47b6-a3a6-cb69fe80267a',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'AMA SUPERCROSS 2020 - R2 ST. LOUIS',
|
||||||
|
'duration': 6481,
|
||||||
|
'thumbnail': r're:https?://.+?/thumbnail/.+?/720$'
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# live stream
|
||||||
|
'url': 'https://platform.wim.tv/embed/?live=28e22c22-49db-40f3-8c37-8cbb0ff44556&autostart=true',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '28e22c22-49db-40f3-8c37-8cbb0ff44556',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Streaming MSmotorTV',
|
||||||
|
'is_live': True,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://platform.wim.tv/#/webtv/automotornews/vod/422492b6-539e-474d-9c6b-68c9d5893365',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://platform.wim.tv/#/webtv/renzoarborechannel/cast/f47e0d15-5b45-455e-bf0d-dba8ffa96365',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_urls(webpage):
|
||||||
|
return [
|
||||||
|
mobj.group('url')
|
||||||
|
for mobj in re.finditer(
|
||||||
|
r'<iframe[^>]+src=["\'](?P<url>%s)' % WimTVIE._VALID_URL,
|
||||||
|
webpage)]
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
if not self._player:
|
||||||
|
self._get_player_data()
|
||||||
|
|
||||||
|
def _get_player_data(self):
|
||||||
|
msg_id = 'Player data'
|
||||||
|
self._player = {}
|
||||||
|
|
||||||
|
datas = [{
|
||||||
|
'url': 'https://platform.wim.tv/common/libs/player/wimtv/wim-rest.js',
|
||||||
|
'vars': [{
|
||||||
|
'regex': r'appAuth = "(.+?)"',
|
||||||
|
'variable': 'app_auth',
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
'url': 'https://platform.wim.tv/common/config/endpointconfig.js',
|
||||||
|
'vars': [{
|
||||||
|
'regex': r'PRODUCTION_HOSTNAME_THUMB = "(.+?)"',
|
||||||
|
'variable': 'thumb_server',
|
||||||
|
}, {
|
||||||
|
'regex': r'PRODUCTION_HOSTNAME_THUMB\s*\+\s*"(.+?)"',
|
||||||
|
'variable': 'thumb_server_path',
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
|
||||||
|
for data in datas:
|
||||||
|
temp = self._download_webpage(data['url'], msg_id)
|
||||||
|
for var in data['vars']:
|
||||||
|
val = self._search_regex(var['regex'], temp, msg_id)
|
||||||
|
if not val:
|
||||||
|
raise ExtractorError('%s not found' % var['variable'])
|
||||||
|
self._player[var['variable']] = val
|
||||||
|
|
||||||
|
def _generate_token(self):
|
||||||
|
json = self._download_json(
|
||||||
|
'https://platform.wim.tv/wimtv-server/oauth/token', 'Token generation',
|
||||||
|
headers={'Authorization': 'Basic %s' % self._player['app_auth']},
|
||||||
|
data=urlencode_postdata({'grant_type': 'client_credentials'}))
|
||||||
|
token = json.get('access_token')
|
||||||
|
if not token:
|
||||||
|
raise ExtractorError('access token not generated')
|
||||||
|
return token
|
||||||
|
|
||||||
|
def _generate_thumbnail(self, thumb_id, width='720'):
|
||||||
|
if not thumb_id or not self._player.get('thumb_server'):
|
||||||
|
return None
|
||||||
|
if not self._player.get('thumb_server_path'):
|
||||||
|
self._player['thumb_server_path'] = ''
|
||||||
|
return '%s%s/asset/thumbnail/%s/%s' % (
|
||||||
|
self._player['thumb_server'],
|
||||||
|
self._player['thumb_server_path'],
|
||||||
|
thumb_id, width)
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
urlc = re.match(self._VALID_URL, url).groupdict()
|
||||||
|
video_id = urlc['id']
|
||||||
|
stream_type = is_live = None
|
||||||
|
if urlc['type'] in {'live', 'cast'}:
|
||||||
|
stream_type = urlc['type'] + '/channel'
|
||||||
|
is_live = True
|
||||||
|
else:
|
||||||
|
stream_type = 'vod'
|
||||||
|
is_live = False
|
||||||
|
token = self._generate_token()
|
||||||
|
json = self._download_json(
|
||||||
|
'https://platform.wim.tv/wimtv-server/api/public/%s/%s/play' % (
|
||||||
|
stream_type, video_id), video_id,
|
||||||
|
headers={'Authorization': 'Bearer %s' % token,
|
||||||
|
'Content-Type': 'application/json'},
|
||||||
|
data=bytes('{}', 'utf-8'))
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for src in json.get('srcs') or []:
|
||||||
|
if src.get('mimeType') == 'application/x-mpegurl':
|
||||||
|
formats.extend(
|
||||||
|
self._extract_m3u8_formats(
|
||||||
|
src.get('uniqueStreamer'), video_id, 'mp4'))
|
||||||
|
if src.get('mimeType') == 'video/flash':
|
||||||
|
formats.append({
|
||||||
|
'format_id': 'rtmp',
|
||||||
|
'url': src.get('uniqueStreamer'),
|
||||||
|
'ext': determine_ext(src.get('uniqueStreamer'), 'flv'),
|
||||||
|
'rtmp_live': is_live,
|
||||||
|
})
|
||||||
|
json = json.get('resource')
|
||||||
|
thumb = self._generate_thumbnail(json.get('thumbnailId'))
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': json.get('title') or json.get('name'),
|
||||||
|
'duration': parse_duration(json.get('duration')),
|
||||||
|
'formats': formats,
|
||||||
|
'thumbnail': thumb,
|
||||||
|
'is_live': is_live,
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -25,6 +26,7 @@ from ..compat import (
|
|||||||
from ..jsinterp import JSInterpreter
|
from ..jsinterp import JSInterpreter
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
clean_html,
|
clean_html,
|
||||||
|
dict_get,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
format_field,
|
format_field,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
@@ -58,9 +60,9 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
|||||||
_TFA_URL = 'https://accounts.google.com/_/signin/challenge?hl=en&TL={0}'
|
_TFA_URL = 'https://accounts.google.com/_/signin/challenge?hl=en&TL={0}'
|
||||||
|
|
||||||
_RESERVED_NAMES = (
|
_RESERVED_NAMES = (
|
||||||
r'embed|e|watch_popup|channel|c|user|playlist|watch|w|v|movies|results|shared|hashtag|'
|
r'channel|c|user|playlist|watch|w|v|embed|e|watch_popup|'
|
||||||
r'storefront|oops|index|account|reporthistory|t/terms|about|upload|signin|logout|'
|
r'movies|results|shared|hashtag|trending|feed|feeds|'
|
||||||
r'feed/(?:watch_later|history|subscriptions|library|trending|recommended)')
|
r'storefront|oops|index|account|reporthistory|t/terms|about|upload|signin|logout')
|
||||||
|
|
||||||
_NETRC_MACHINE = 'youtube'
|
_NETRC_MACHINE = 'youtube'
|
||||||
# If True it will raise an error if no login info is provided
|
# If True it will raise an error if no login info is provided
|
||||||
@@ -270,28 +272,46 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
|||||||
if not self._login():
|
if not self._login():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
_YT_WEB_CLIENT_VERSION = '2.20210301.08.00'
|
||||||
_DEFAULT_API_DATA = {
|
_DEFAULT_API_DATA = {
|
||||||
'context': {
|
'context': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'WEB',
|
'clientName': 'WEB',
|
||||||
'clientVersion': '2.20201021.03.00',
|
'clientVersion': _YT_WEB_CLIENT_VERSION,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_DEFAULT_BASIC_API_HEADERS = {
|
||||||
|
'X-YouTube-Client-Name': '1',
|
||||||
|
'X-YouTube-Client-Version': _YT_WEB_CLIENT_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
_YT_INITIAL_DATA_RE = r'(?:window\s*\[\s*["\']ytInitialData["\']\s*\]|ytInitialData)\s*=\s*({.+?})\s*;'
|
_YT_INITIAL_DATA_RE = r'(?:window\s*\[\s*["\']ytInitialData["\']\s*\]|ytInitialData)\s*=\s*({.+?})\s*;'
|
||||||
_YT_INITIAL_PLAYER_RESPONSE_RE = r'ytInitialPlayerResponse\s*=\s*({.+?})\s*;'
|
_YT_INITIAL_PLAYER_RESPONSE_RE = r'ytInitialPlayerResponse\s*=\s*({.+?})\s*;'
|
||||||
_YT_INITIAL_BOUNDARY_RE = r'(?:var\s+meta|</script|\n)'
|
_YT_INITIAL_BOUNDARY_RE = r'(?:var\s+meta|</script|\n)'
|
||||||
|
|
||||||
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):
|
||||||
@@ -301,6 +321,27 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
|||||||
self._YT_INITIAL_DATA_RE), webpage, 'yt initial data'),
|
self._YT_INITIAL_DATA_RE), webpage, 'yt initial data'),
|
||||||
video_id)
|
video_id)
|
||||||
|
|
||||||
|
def _extract_identity_token(self, webpage, item_id):
|
||||||
|
ytcfg = self._extract_ytcfg(item_id, webpage)
|
||||||
|
if ytcfg:
|
||||||
|
token = try_get(ytcfg, lambda x: x['ID_TOKEN'], compat_str)
|
||||||
|
if token:
|
||||||
|
return token
|
||||||
|
return self._search_regex(
|
||||||
|
r'\bID_TOKEN["\']\s*:\s*["\'](.+?)["\']', webpage,
|
||||||
|
'identity token', default=None)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_account_syncid(data):
|
||||||
|
"""Extract syncId required to download private playlists of secondary channels"""
|
||||||
|
sync_ids = (
|
||||||
|
try_get(data, lambda x: x['responseContext']['mainAppWebResponseContext']['datasyncId'], compat_str)
|
||||||
|
or '').split("||")
|
||||||
|
if len(sync_ids) >= 2 and sync_ids[1]:
|
||||||
|
# datasyncid is of the form "channel_syncid||user_syncid" for secondary channel
|
||||||
|
# and just "user_syncid||" for primary channel. We only want the channel_syncid
|
||||||
|
return sync_ids[0]
|
||||||
|
|
||||||
def _extract_ytcfg(self, video_id, webpage):
|
def _extract_ytcfg(self, video_id, webpage):
|
||||||
return self._parse_json(
|
return self._parse_json(
|
||||||
self._search_regex(
|
self._search_regex(
|
||||||
@@ -1448,12 +1489,278 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
(r'%s\s*%s' % (regex, self._YT_INITIAL_BOUNDARY_RE),
|
(r'%s\s*%s' % (regex, self._YT_INITIAL_BOUNDARY_RE),
|
||||||
regex), webpage, name, default='{}'), video_id, fatal=False)
|
regex), webpage, name, default='{}'), video_id, fatal=False)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _join_text_entries(runs):
|
||||||
|
text = None
|
||||||
|
for run in runs:
|
||||||
|
if not isinstance(run, dict):
|
||||||
|
continue
|
||||||
|
sub_text = try_get(run, lambda x: x['text'], compat_str)
|
||||||
|
if sub_text:
|
||||||
|
if not text:
|
||||||
|
text = sub_text
|
||||||
|
continue
|
||||||
|
text += sub_text
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _extract_comment(self, comment_renderer, parent=None):
|
||||||
|
comment_id = comment_renderer.get('commentId')
|
||||||
|
if not comment_id:
|
||||||
|
return
|
||||||
|
comment_text_runs = try_get(comment_renderer, lambda x: x['contentText']['runs']) or []
|
||||||
|
text = self._join_text_entries(comment_text_runs) or ''
|
||||||
|
comment_time_text = try_get(comment_renderer, lambda x: x['publishedTimeText']['runs']) or []
|
||||||
|
time_text = self._join_text_entries(comment_time_text)
|
||||||
|
|
||||||
|
author = try_get(comment_renderer, lambda x: x['authorText']['simpleText'], compat_str)
|
||||||
|
author_id = try_get(comment_renderer,
|
||||||
|
lambda x: x['authorEndpoint']['browseEndpoint']['browseId'], compat_str)
|
||||||
|
votes = str_to_int(try_get(comment_renderer, (lambda x: x['voteCount']['simpleText'],
|
||||||
|
lambda x: x['likeCount']), compat_str)) or 0
|
||||||
|
author_thumbnail = try_get(comment_renderer,
|
||||||
|
lambda x: x['authorThumbnail']['thumbnails'][-1]['url'], compat_str)
|
||||||
|
|
||||||
|
author_is_uploader = try_get(comment_renderer, lambda x: x['authorIsChannelOwner'], bool)
|
||||||
|
is_liked = try_get(comment_renderer, lambda x: x['isLiked'], bool)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': comment_id,
|
||||||
|
'text': text,
|
||||||
|
# TODO: This should be parsed to timestamp
|
||||||
|
'time_text': time_text,
|
||||||
|
'like_count': votes,
|
||||||
|
'is_favorited': is_liked,
|
||||||
|
'author': author,
|
||||||
|
'author_id': author_id,
|
||||||
|
'author_thumbnail': author_thumbnail,
|
||||||
|
'author_is_uploader': author_is_uploader,
|
||||||
|
'parent': parent or 'root'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _comment_entries(self, root_continuation_data, identity_token, account_syncid,
|
||||||
|
session_token_list, parent=None, comment_counts=None):
|
||||||
|
|
||||||
|
def extract_thread(parent_renderer):
|
||||||
|
contents = try_get(parent_renderer, lambda x: x['contents'], list) or []
|
||||||
|
if not parent:
|
||||||
|
comment_counts[2] = 0
|
||||||
|
for content in contents:
|
||||||
|
comment_thread_renderer = try_get(content, lambda x: x['commentThreadRenderer'])
|
||||||
|
comment_renderer = try_get(
|
||||||
|
comment_thread_renderer, (lambda x: x['comment']['commentRenderer'], dict)) or try_get(
|
||||||
|
content, (lambda x: x['commentRenderer'], dict))
|
||||||
|
|
||||||
|
if not comment_renderer:
|
||||||
|
continue
|
||||||
|
comment = self._extract_comment(comment_renderer, parent)
|
||||||
|
if not comment:
|
||||||
|
continue
|
||||||
|
comment_counts[0] += 1
|
||||||
|
yield comment
|
||||||
|
# Attempt to get the replies
|
||||||
|
comment_replies_renderer = try_get(
|
||||||
|
comment_thread_renderer, lambda x: x['replies']['commentRepliesRenderer'], dict)
|
||||||
|
|
||||||
|
if comment_replies_renderer:
|
||||||
|
comment_counts[2] += 1
|
||||||
|
comment_entries_iter = self._comment_entries(
|
||||||
|
comment_replies_renderer, identity_token, account_syncid,
|
||||||
|
parent=comment.get('id'), session_token_list=session_token_list,
|
||||||
|
comment_counts=comment_counts)
|
||||||
|
|
||||||
|
for reply_comment in comment_entries_iter:
|
||||||
|
yield reply_comment
|
||||||
|
|
||||||
|
if not comment_counts:
|
||||||
|
# comment so far, est. total comments, current comment thread #
|
||||||
|
comment_counts = [0, 0, 0]
|
||||||
|
headers = self._DEFAULT_BASIC_API_HEADERS.copy()
|
||||||
|
|
||||||
|
# TODO: Generalize the download code with TabIE
|
||||||
|
if identity_token:
|
||||||
|
headers['x-youtube-identity-token'] = identity_token
|
||||||
|
|
||||||
|
if account_syncid:
|
||||||
|
headers['X-Goog-PageId'] = account_syncid
|
||||||
|
headers['X-Goog-AuthUser'] = 0
|
||||||
|
|
||||||
|
continuation = YoutubeTabIE._extract_continuation(root_continuation_data) # TODO
|
||||||
|
first_continuation = False
|
||||||
|
if parent is None:
|
||||||
|
first_continuation = True
|
||||||
|
|
||||||
|
for page_num in itertools.count(0):
|
||||||
|
if not continuation:
|
||||||
|
break
|
||||||
|
retries = self._downloader.params.get('extractor_retries', 3)
|
||||||
|
count = -1
|
||||||
|
last_error = None
|
||||||
|
|
||||||
|
while count < retries:
|
||||||
|
count += 1
|
||||||
|
if last_error:
|
||||||
|
self.report_warning('%s. Retrying ...' % last_error)
|
||||||
|
try:
|
||||||
|
query = {
|
||||||
|
'ctoken': continuation['ctoken'],
|
||||||
|
'pbj': 1,
|
||||||
|
'type': 'next',
|
||||||
|
}
|
||||||
|
if parent:
|
||||||
|
query['action_get_comment_replies'] = 1
|
||||||
|
else:
|
||||||
|
query['action_get_comments'] = 1
|
||||||
|
|
||||||
|
comment_prog_str = '(%d/%d)' % (comment_counts[0], comment_counts[1])
|
||||||
|
if page_num == 0:
|
||||||
|
if first_continuation:
|
||||||
|
note_prefix = "Downloading initial comment continuation page"
|
||||||
|
else:
|
||||||
|
note_prefix = " Downloading comment reply thread %d %s" % (comment_counts[2], comment_prog_str)
|
||||||
|
else:
|
||||||
|
note_prefix = "%sDownloading comment%s page %d %s" % (
|
||||||
|
" " if parent else "",
|
||||||
|
' replies' if parent else '',
|
||||||
|
page_num,
|
||||||
|
comment_prog_str)
|
||||||
|
|
||||||
|
browse = self._download_json(
|
||||||
|
'https://www.youtube.com/comment_service_ajax', None,
|
||||||
|
'%s %s' % (note_prefix, '(retry #%d)' % count if count else ''),
|
||||||
|
headers=headers, query=query,
|
||||||
|
data=urlencode_postdata({
|
||||||
|
'session_token': session_token_list[0]
|
||||||
|
}))
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code in (500, 503, 404, 413):
|
||||||
|
if e.cause.code == 413:
|
||||||
|
self.report_warning("Assumed end of comments (received HTTP Error 413)")
|
||||||
|
return
|
||||||
|
# Downloading page may result in intermittent 5xx HTTP error
|
||||||
|
# Sometimes a 404 is also recieved. See: https://github.com/ytdl-org/youtube-dl/issues/28289
|
||||||
|
last_error = 'HTTP Error %s' % e.cause.code
|
||||||
|
if e.cause.code == 404:
|
||||||
|
last_error = last_error + " (this API is probably deprecated)"
|
||||||
|
if count < retries:
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
session_token = try_get(browse, lambda x: x['xsrf_token'], compat_str)
|
||||||
|
if session_token:
|
||||||
|
session_token_list[0] = session_token
|
||||||
|
|
||||||
|
response = try_get(browse,
|
||||||
|
(lambda x: x['response'],
|
||||||
|
lambda x: x[1]['response'])) or {}
|
||||||
|
|
||||||
|
if response.get('continuationContents'):
|
||||||
|
break
|
||||||
|
|
||||||
|
# YouTube sometimes gives reload: now json if something went wrong (e.g. bad auth)
|
||||||
|
if browse.get('reload'):
|
||||||
|
raise ExtractorError("Invalid or missing params in continuation request", expected=False)
|
||||||
|
|
||||||
|
# TODO: not tested, merged from old extractor
|
||||||
|
err_msg = browse.get('externalErrorMessage')
|
||||||
|
if err_msg:
|
||||||
|
raise ExtractorError('YouTube said: %s' % err_msg, expected=False)
|
||||||
|
|
||||||
|
# Youtube sometimes sends incomplete data
|
||||||
|
# See: https://github.com/ytdl-org/youtube-dl/issues/28194
|
||||||
|
last_error = 'Incomplete data received'
|
||||||
|
if count >= retries:
|
||||||
|
self._downloader.report_error(last_error)
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
break
|
||||||
|
|
||||||
|
known_continuation_renderers = {
|
||||||
|
'itemSectionContinuation': extract_thread,
|
||||||
|
'commentRepliesContinuation': extract_thread
|
||||||
|
}
|
||||||
|
|
||||||
|
# extract next root continuation from the results
|
||||||
|
continuation_contents = try_get(
|
||||||
|
response, lambda x: x['continuationContents'], dict) or {}
|
||||||
|
|
||||||
|
for key, value in continuation_contents.items():
|
||||||
|
if key not in known_continuation_renderers:
|
||||||
|
continue
|
||||||
|
continuation_renderer = value
|
||||||
|
|
||||||
|
if first_continuation:
|
||||||
|
first_continuation = False
|
||||||
|
expected_comment_count = try_get(
|
||||||
|
continuation_renderer,
|
||||||
|
(lambda x: x['header']['commentsHeaderRenderer']['countText']['runs'][0]['text'],
|
||||||
|
lambda x: x['header']['commentsHeaderRenderer']['commentsCount']['runs'][0]['text']),
|
||||||
|
compat_str)
|
||||||
|
|
||||||
|
if expected_comment_count:
|
||||||
|
comment_counts[1] = str_to_int(expected_comment_count)
|
||||||
|
self.to_screen("Downloading ~%d comments" % str_to_int(expected_comment_count))
|
||||||
|
yield comment_counts[1]
|
||||||
|
|
||||||
|
# TODO: cli arg.
|
||||||
|
# 1/True for newest, 0/False for popular (default)
|
||||||
|
comment_sort_index = int(True)
|
||||||
|
sort_continuation_renderer = try_get(
|
||||||
|
continuation_renderer,
|
||||||
|
lambda x: x['header']['commentsHeaderRenderer']['sortMenu']['sortFilterSubMenuRenderer']['subMenuItems']
|
||||||
|
[comment_sort_index]['continuation']['reloadContinuationData'], dict)
|
||||||
|
# If this fails, the initial continuation page
|
||||||
|
# starts off with popular anyways.
|
||||||
|
if sort_continuation_renderer:
|
||||||
|
continuation = YoutubeTabIE._build_continuation_query(
|
||||||
|
continuation=sort_continuation_renderer.get('continuation'),
|
||||||
|
ctp=sort_continuation_renderer.get('clickTrackingParams'))
|
||||||
|
self.to_screen("Sorting comments by %s" % ('popular' if comment_sort_index == 0 else 'newest'))
|
||||||
|
break
|
||||||
|
|
||||||
|
for entry in known_continuation_renderers[key](continuation_renderer):
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
continuation = YoutubeTabIE._extract_continuation(continuation_renderer) # TODO
|
||||||
|
break
|
||||||
|
|
||||||
|
def _extract_comments(self, ytcfg, video_id, contents, webpage, xsrf_token):
|
||||||
|
"""Entry for comment extraction"""
|
||||||
|
comments = []
|
||||||
|
known_entry_comment_renderers = (
|
||||||
|
'itemSectionRenderer',
|
||||||
|
)
|
||||||
|
estimated_total = 0
|
||||||
|
for entry in contents:
|
||||||
|
for key, renderer in entry.items():
|
||||||
|
if key not in known_entry_comment_renderers:
|
||||||
|
continue
|
||||||
|
|
||||||
|
comment_iter = self._comment_entries(
|
||||||
|
renderer,
|
||||||
|
identity_token=self._extract_identity_token(webpage, item_id=video_id),
|
||||||
|
account_syncid=self._extract_account_syncid(ytcfg),
|
||||||
|
session_token_list=[xsrf_token])
|
||||||
|
|
||||||
|
for comment in comment_iter:
|
||||||
|
if isinstance(comment, int):
|
||||||
|
estimated_total = comment
|
||||||
|
continue
|
||||||
|
comments.append(comment)
|
||||||
|
break
|
||||||
|
self.to_screen("Downloaded %d/%d comments" % (len(comments), estimated_total))
|
||||||
|
return {
|
||||||
|
'comments': comments,
|
||||||
|
'comment_count': len(comments),
|
||||||
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
url, smuggled_data = unsmuggle_url(url, {})
|
url, smuggled_data = unsmuggle_url(url, {})
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
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:
|
||||||
@@ -2008,152 +2315,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
errnote='Unable to download video annotations', fatal=False,
|
errnote='Unable to download video annotations', fatal=False,
|
||||||
data=urlencode_postdata({xsrf_field_name: xsrf_token}))
|
data=urlencode_postdata({xsrf_field_name: xsrf_token}))
|
||||||
|
|
||||||
# Get comments
|
|
||||||
# TODO: Refactor and move to seperate function
|
|
||||||
if get_comments:
|
if get_comments:
|
||||||
expected_video_comment_count = 0
|
info['__post_extractor'] = lambda: self._extract_comments(ytcfg, video_id, contents, webpage, xsrf_token)
|
||||||
video_comments = []
|
|
||||||
|
|
||||||
def find_value(html, key, num_chars=2, separator='"'):
|
|
||||||
pos_begin = html.find(key) + len(key) + num_chars
|
|
||||||
pos_end = html.find(separator, pos_begin)
|
|
||||||
return html[pos_begin: pos_end]
|
|
||||||
|
|
||||||
def search_dict(partial, key):
|
|
||||||
if isinstance(partial, dict):
|
|
||||||
for k, v in partial.items():
|
|
||||||
if k == key:
|
|
||||||
yield v
|
|
||||||
else:
|
|
||||||
for o in search_dict(v, key):
|
|
||||||
yield o
|
|
||||||
elif isinstance(partial, list):
|
|
||||||
for i in partial:
|
|
||||||
for o in search_dict(i, key):
|
|
||||||
yield o
|
|
||||||
|
|
||||||
continuations = []
|
|
||||||
if initial_data:
|
|
||||||
try:
|
|
||||||
ncd = next(search_dict(initial_data, 'nextContinuationData'))
|
|
||||||
continuations = [ncd['continuation']]
|
|
||||||
# Handle videos where comments have been disabled entirely
|
|
||||||
except StopIteration:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_continuation(continuation, session_token, replies=False):
|
|
||||||
query = {
|
|
||||||
'pbj': 1,
|
|
||||||
'ctoken': continuation,
|
|
||||||
}
|
|
||||||
if replies:
|
|
||||||
query['action_get_comment_replies'] = 1
|
|
||||||
else:
|
|
||||||
query['action_get_comments'] = 1
|
|
||||||
|
|
||||||
while True:
|
|
||||||
content, handle = self._download_webpage_handle(
|
|
||||||
'https://www.youtube.com/comment_service_ajax',
|
|
||||||
video_id,
|
|
||||||
note=False,
|
|
||||||
expected_status=[413],
|
|
||||||
data=urlencode_postdata({
|
|
||||||
'session_token': session_token
|
|
||||||
}),
|
|
||||||
query=query,
|
|
||||||
headers={
|
|
||||||
'Accept': '*/*',
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0',
|
|
||||||
'X-YouTube-Client-Name': '1',
|
|
||||||
'X-YouTube-Client-Version': '2.20201202.06.01'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
response_code = handle.getcode()
|
|
||||||
if (response_code == 200):
|
|
||||||
return self._parse_json(content, video_id)
|
|
||||||
if (response_code == 413):
|
|
||||||
return None
|
|
||||||
raise ExtractorError('Unexpected HTTP error code: %s' % response_code)
|
|
||||||
|
|
||||||
first_continuation = True
|
|
||||||
chain_msg = ''
|
|
||||||
self.to_screen('Downloading comments')
|
|
||||||
while continuations:
|
|
||||||
continuation = continuations.pop()
|
|
||||||
comment_response = get_continuation(continuation, xsrf_token)
|
|
||||||
if not comment_response:
|
|
||||||
continue
|
|
||||||
if list(search_dict(comment_response, 'externalErrorMessage')):
|
|
||||||
raise ExtractorError('Error returned from server: ' + next(search_dict(comment_response, 'externalErrorMessage')))
|
|
||||||
|
|
||||||
if 'continuationContents' not in comment_response['response']:
|
|
||||||
# Something is wrong here. Youtube won't accept this continuation token for some reason and responds with a user satisfaction dialog (error?)
|
|
||||||
continue
|
|
||||||
# not sure if this actually helps
|
|
||||||
if 'xsrf_token' in comment_response:
|
|
||||||
xsrf_token = comment_response['xsrf_token']
|
|
||||||
|
|
||||||
item_section = comment_response['response']['continuationContents']['itemSectionContinuation']
|
|
||||||
if first_continuation:
|
|
||||||
expected_video_comment_count = int(item_section['header']['commentsHeaderRenderer']['countText']['runs'][0]['text'].replace(' Comments', '').replace('1 Comment', '1').replace(',', ''))
|
|
||||||
first_continuation = False
|
|
||||||
if 'contents' not in item_section:
|
|
||||||
# continuation returned no comments?
|
|
||||||
# set an empty array as to not break the for loop
|
|
||||||
item_section['contents'] = []
|
|
||||||
|
|
||||||
for meta_comment in item_section['contents']:
|
|
||||||
comment = meta_comment['commentThreadRenderer']['comment']['commentRenderer']
|
|
||||||
video_comments.append({
|
|
||||||
'id': comment['commentId'],
|
|
||||||
'text': ''.join([c['text'] for c in try_get(comment, lambda x: x['contentText']['runs'], list) or []]),
|
|
||||||
'time_text': ''.join([c['text'] for c in comment['publishedTimeText']['runs']]),
|
|
||||||
'author': comment.get('authorText', {}).get('simpleText', ''),
|
|
||||||
'votes': comment.get('voteCount', {}).get('simpleText', '0'),
|
|
||||||
'author_thumbnail': comment['authorThumbnail']['thumbnails'][-1]['url'],
|
|
||||||
'parent': 'root'
|
|
||||||
})
|
|
||||||
if 'replies' not in meta_comment['commentThreadRenderer']:
|
|
||||||
continue
|
|
||||||
|
|
||||||
reply_continuations = [rcn['nextContinuationData']['continuation'] for rcn in meta_comment['commentThreadRenderer']['replies']['commentRepliesRenderer']['continuations']]
|
|
||||||
while reply_continuations:
|
|
||||||
time.sleep(1)
|
|
||||||
continuation = reply_continuations.pop()
|
|
||||||
replies_data = get_continuation(continuation, xsrf_token, True)
|
|
||||||
if not replies_data or 'continuationContents' not in replies_data[1]['response']:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self._downloader.params.get('verbose', False):
|
|
||||||
chain_msg = ' (chain %s)' % comment['commentId']
|
|
||||||
self.to_screen('Comments downloaded: %d of ~%d%s' % (len(video_comments), expected_video_comment_count, chain_msg))
|
|
||||||
reply_comment_meta = replies_data[1]['response']['continuationContents']['commentRepliesContinuation']
|
|
||||||
for reply_meta in reply_comment_meta.get('contents', {}):
|
|
||||||
reply_comment = reply_meta['commentRenderer']
|
|
||||||
video_comments.append({
|
|
||||||
'id': reply_comment['commentId'],
|
|
||||||
'text': ''.join([c['text'] for c in reply_comment['contentText']['runs']]),
|
|
||||||
'time_text': ''.join([c['text'] for c in reply_comment['publishedTimeText']['runs']]),
|
|
||||||
'author': reply_comment.get('authorText', {}).get('simpleText', ''),
|
|
||||||
'votes': reply_comment.get('voteCount', {}).get('simpleText', '0'),
|
|
||||||
'author_thumbnail': reply_comment['authorThumbnail']['thumbnails'][-1]['url'],
|
|
||||||
'parent': comment['commentId']
|
|
||||||
})
|
|
||||||
if 'continuations' not in reply_comment_meta or len(reply_comment_meta['continuations']) == 0:
|
|
||||||
continue
|
|
||||||
reply_continuations += [rcn['nextContinuationData']['continuation'] for rcn in reply_comment_meta['continuations']]
|
|
||||||
|
|
||||||
self.to_screen('Comments downloaded: %d of ~%d' % (len(video_comments), expected_video_comment_count))
|
|
||||||
if 'continuations' in item_section:
|
|
||||||
continuations += [ncd['nextContinuationData']['continuation'] for ncd in item_section['continuations']]
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
self.to_screen('Total comments downloaded: %d of ~%d' % (len(video_comments), expected_video_comment_count))
|
|
||||||
info.update({
|
|
||||||
'comments': video_comments,
|
|
||||||
'comment_count': expected_video_comment_count
|
|
||||||
})
|
|
||||||
|
|
||||||
self.mark_watched(video_id, player_response)
|
self.mark_watched(video_id, player_response)
|
||||||
|
|
||||||
@@ -2500,17 +2663,22 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
|||||||
channel_url, 'channel id')
|
channel_url, 'channel id')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _extract_grid_item_renderer(item):
|
def _extract_basic_item_renderer(item):
|
||||||
for item_kind in ('Playlist', 'Video', 'Channel'):
|
# Modified from _extract_grid_item_renderer
|
||||||
renderer = item.get('grid%sRenderer' % item_kind)
|
known_renderers = (
|
||||||
if renderer:
|
'playlistRenderer', 'videoRenderer', 'channelRenderer'
|
||||||
|
'gridPlaylistRenderer', 'gridVideoRenderer', 'gridChannelRenderer'
|
||||||
|
)
|
||||||
|
for key, renderer in item.items():
|
||||||
|
if key not in known_renderers:
|
||||||
|
continue
|
||||||
return renderer
|
return renderer
|
||||||
|
|
||||||
def _grid_entries(self, grid_renderer):
|
def _grid_entries(self, grid_renderer):
|
||||||
for item in grid_renderer['items']:
|
for item in grid_renderer['items']:
|
||||||
if not isinstance(item, dict):
|
if not isinstance(item, dict):
|
||||||
continue
|
continue
|
||||||
renderer = self._extract_grid_item_renderer(item)
|
renderer = self._extract_basic_item_renderer(item)
|
||||||
if not isinstance(renderer, dict):
|
if not isinstance(renderer, dict):
|
||||||
continue
|
continue
|
||||||
title = try_get(
|
title = try_get(
|
||||||
@@ -2539,7 +2707,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
|||||||
content = shelf_renderer.get('content')
|
content = shelf_renderer.get('content')
|
||||||
if not isinstance(content, dict):
|
if not isinstance(content, dict):
|
||||||
return
|
return
|
||||||
renderer = content.get('gridRenderer')
|
renderer = content.get('gridRenderer') or content.get('expandedShelfContentsRenderer')
|
||||||
if renderer:
|
if renderer:
|
||||||
# TODO: add support for nested playlists so each shelf is processed
|
# TODO: add support for nested playlists so each shelf is processed
|
||||||
# as separate playlist
|
# as separate playlist
|
||||||
@@ -2581,20 +2749,6 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
|||||||
continue
|
continue
|
||||||
yield self._extract_video(renderer)
|
yield self._extract_video(renderer)
|
||||||
|
|
||||||
r""" # Not needed in the new implementation
|
|
||||||
def _itemSection_entries(self, item_sect_renderer):
|
|
||||||
for content in item_sect_renderer['contents']:
|
|
||||||
if not isinstance(content, dict):
|
|
||||||
continue
|
|
||||||
renderer = content.get('videoRenderer', {})
|
|
||||||
if not isinstance(renderer, dict):
|
|
||||||
continue
|
|
||||||
video_id = renderer.get('videoId')
|
|
||||||
if not video_id:
|
|
||||||
continue
|
|
||||||
yield self._extract_video(renderer)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _rich_entries(self, rich_grid_renderer):
|
def _rich_entries(self, rich_grid_renderer):
|
||||||
renderer = try_get(
|
renderer = try_get(
|
||||||
rich_grid_renderer, lambda x: x['content']['videoRenderer'], dict) or {}
|
rich_grid_renderer, lambda x: x['content']['videoRenderer'], dict) or {}
|
||||||
@@ -2693,7 +2847,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, item_id, identity_token, account_syncid):
|
||||||
|
|
||||||
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 []
|
||||||
@@ -2753,30 +2907,51 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
|||||||
if identity_token:
|
if identity_token:
|
||||||
headers['x-youtube-identity-token'] = identity_token
|
headers['x-youtube-identity-token'] = identity_token
|
||||||
|
|
||||||
|
if account_syncid:
|
||||||
|
headers['X-Goog-PageId'] = account_syncid
|
||||||
|
headers['X-Goog-AuthUser'] = 0
|
||||||
|
|
||||||
for page_num in itertools.count(1):
|
for page_num in itertools.count(1):
|
||||||
if not continuation:
|
if not continuation:
|
||||||
break
|
break
|
||||||
count = 0
|
retries = self._downloader.params.get('extractor_retries', 3)
|
||||||
retries = 3
|
count = -1
|
||||||
while count <= retries:
|
last_error = None
|
||||||
try:
|
while count < retries:
|
||||||
# Downloading page may result in intermittent 5xx HTTP error
|
|
||||||
# that is usually worked around with a retry
|
|
||||||
browse = self._download_json(
|
|
||||||
'https://www.youtube.com/browse_ajax', None,
|
|
||||||
'Downloading page %d%s'
|
|
||||||
% (page_num, ' (retry #%d)' % count if count else ''),
|
|
||||||
headers=headers, query=continuation)
|
|
||||||
break
|
|
||||||
except ExtractorError as e:
|
|
||||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code in (500, 503):
|
|
||||||
count += 1
|
count += 1
|
||||||
if count <= retries:
|
if last_error:
|
||||||
|
self.report_warning('%s. Retrying ...' % last_error)
|
||||||
|
try:
|
||||||
|
response = self._call_api(
|
||||||
|
ep="browse", fatal=True, headers=headers,
|
||||||
|
video_id='%s page %s' % (item_id, page_num),
|
||||||
|
query={
|
||||||
|
'continuation': continuation['continuation'],
|
||||||
|
'clickTracking': {'clickTrackingParams': continuation['itct']},
|
||||||
|
},
|
||||||
|
note='Downloading API JSON%s' % (' (retry #%d)' % count if count else ''))
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code in (500, 503, 404):
|
||||||
|
# Downloading page may result in intermittent 5xx HTTP error
|
||||||
|
# Sometimes a 404 is also recieved. See: https://github.com/ytdl-org/youtube-dl/issues/28289
|
||||||
|
last_error = 'HTTP Error %s' % e.cause.code
|
||||||
|
if count < retries:
|
||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
if not browse:
|
else:
|
||||||
|
# Youtube sometimes sends incomplete data
|
||||||
|
# See: https://github.com/ytdl-org/youtube-dl/issues/28194
|
||||||
|
if dict_get(response,
|
||||||
|
('continuationContents', 'onResponseReceivedActions', 'onResponseReceivedEndpoints')):
|
||||||
break
|
break
|
||||||
response = try_get(browse, lambda x: x[1]['response'], dict)
|
|
||||||
|
# Youtube may send alerts if there was an issue with the continuation page
|
||||||
|
self._extract_alerts(response, expected=False)
|
||||||
|
|
||||||
|
last_error = 'Incomplete data received'
|
||||||
|
if count >= retries:
|
||||||
|
self._downloader.report_error(last_error)
|
||||||
|
|
||||||
if not response:
|
if not response:
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -2805,11 +2980,13 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
|||||||
'gridPlaylistRenderer': (self._grid_entries, 'items'),
|
'gridPlaylistRenderer': (self._grid_entries, 'items'),
|
||||||
'gridVideoRenderer': (self._grid_entries, 'items'),
|
'gridVideoRenderer': (self._grid_entries, 'items'),
|
||||||
'playlistVideoRenderer': (self._playlist_entries, 'contents'),
|
'playlistVideoRenderer': (self._playlist_entries, 'contents'),
|
||||||
'itemSectionRenderer': (self._playlist_entries, 'contents'),
|
'itemSectionRenderer': (extract_entries, 'contents'), # for feeds
|
||||||
'richItemRenderer': (extract_entries, 'contents'), # for hashtag
|
'richItemRenderer': (extract_entries, 'contents'), # for hashtag
|
||||||
|
'backstagePostThreadRenderer': (self._post_thread_continuation_entries, 'contents')
|
||||||
}
|
}
|
||||||
continuation_items = try_get(
|
continuation_items = try_get(
|
||||||
response, lambda x: x['onResponseReceivedActions'][0]['appendContinuationItemsAction']['continuationItems'], list)
|
response,
|
||||||
|
lambda x: dict_get(x, ('onResponseReceivedActions', 'onResponseReceivedEndpoints'))[0]['appendContinuationItemsAction']['continuationItems'], list)
|
||||||
continuation_item = try_get(continuation_items, lambda x: x[0], dict) or {}
|
continuation_item = try_get(continuation_items, lambda x: x[0], dict) or {}
|
||||||
video_items_renderer = None
|
video_items_renderer = None
|
||||||
for key, value in continuation_item.items():
|
for key, value in continuation_item.items():
|
||||||
@@ -2856,7 +3033,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
|||||||
try_get(owner, lambda x: x['navigationEndpoint']['browseEndpoint']['canonicalBaseUrl'], compat_str))
|
try_get(owner, lambda x: x['navigationEndpoint']['browseEndpoint']['canonicalBaseUrl'], compat_str))
|
||||||
return {k: v for k, v in uploader.items() if v is not None}
|
return {k: v for k, v in uploader.items() if v is not None}
|
||||||
|
|
||||||
def _extract_from_tabs(self, item_id, webpage, data, tabs, identity_token):
|
def _extract_from_tabs(self, item_id, webpage, data, tabs):
|
||||||
playlist_id = title = description = channel_url = channel_name = channel_id = None
|
playlist_id = title = description = channel_url = channel_name = channel_id = None
|
||||||
thumbnails_list = tags = []
|
thumbnails_list = tags = []
|
||||||
|
|
||||||
@@ -2920,16 +3097,41 @@ 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, playlist_id,
|
||||||
|
self._extract_identity_token(webpage, item_id),
|
||||||
|
self._extract_account_syncid(data)),
|
||||||
**metadata)
|
**metadata)
|
||||||
|
|
||||||
|
def _extract_mix_playlist(self, playlist, playlist_id):
|
||||||
|
first_id = last_id = None
|
||||||
|
for page_num in itertools.count(1):
|
||||||
|
videos = list(self._playlist_entries(playlist))
|
||||||
|
if not videos:
|
||||||
|
return
|
||||||
|
start = next((i for i, v in enumerate(videos) if v['id'] == last_id), -1) + 1
|
||||||
|
if start >= len(videos):
|
||||||
|
return
|
||||||
|
for video in videos[start:]:
|
||||||
|
if video['id'] == first_id:
|
||||||
|
self.to_screen('First video %s found again; Assuming end of Mix' % first_id)
|
||||||
|
return
|
||||||
|
yield video
|
||||||
|
first_id = first_id or videos[0]['id']
|
||||||
|
last_id = videos[-1]['id']
|
||||||
|
|
||||||
|
_, data = self._extract_webpage(
|
||||||
|
'https://www.youtube.com/watch?list=%s&v=%s' % (playlist_id, last_id),
|
||||||
|
'%s page %d' % (playlist_id, page_num))
|
||||||
|
playlist = try_get(
|
||||||
|
data, lambda x: x['contents']['twoColumnWatchNextResults']['playlist']['playlist'], dict)
|
||||||
|
|
||||||
def _extract_from_playlist(self, item_id, url, data, playlist):
|
def _extract_from_playlist(self, item_id, url, data, playlist):
|
||||||
title = playlist.get('title') or try_get(
|
title = playlist.get('title') or try_get(
|
||||||
data, lambda x: x['titleText']['simpleText'], compat_str)
|
data, lambda x: x['titleText']['simpleText'], compat_str)
|
||||||
playlist_id = playlist.get('playlistId') or item_id
|
playlist_id = playlist.get('playlistId') or item_id
|
||||||
# Inline playlist rendition continuation does not always work
|
|
||||||
# at Youtube side, so delegating regular tab-based playlist URL
|
# Delegating everything except mix playlists to regular tab-based playlist URL
|
||||||
# processing whenever possible.
|
|
||||||
playlist_url = urljoin(url, try_get(
|
playlist_url = urljoin(url, try_get(
|
||||||
playlist, lambda x: x['endpoint']['commandMetadata']['webCommandMetadata']['url'],
|
playlist, lambda x: x['endpoint']['commandMetadata']['webCommandMetadata']['url'],
|
||||||
compat_str))
|
compat_str))
|
||||||
@@ -2937,17 +3139,18 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
|||||||
return self.url_result(
|
return self.url_result(
|
||||||
playlist_url, ie=YoutubeTabIE.ie_key(), video_id=playlist_id,
|
playlist_url, ie=YoutubeTabIE.ie_key(), video_id=playlist_id,
|
||||||
video_title=title)
|
video_title=title)
|
||||||
return self.playlist_result(
|
|
||||||
self._playlist_entries(playlist), playlist_id=playlist_id,
|
|
||||||
playlist_title=title)
|
|
||||||
|
|
||||||
@staticmethod
|
return self.playlist_result(
|
||||||
def _extract_alerts(data):
|
self._extract_mix_playlist(playlist, playlist_id),
|
||||||
|
playlist_id=playlist_id, playlist_title=title)
|
||||||
|
|
||||||
|
def _extract_alerts(self, data, expected=False):
|
||||||
|
|
||||||
|
def _real_extract_alerts():
|
||||||
for alert_dict in try_get(data, lambda x: x['alerts'], list) or []:
|
for alert_dict in try_get(data, lambda x: x['alerts'], list) or []:
|
||||||
if not isinstance(alert_dict, dict):
|
if not isinstance(alert_dict, dict):
|
||||||
continue
|
continue
|
||||||
for renderer in alert_dict:
|
for alert in alert_dict.values():
|
||||||
alert = alert_dict[renderer]
|
|
||||||
alert_type = alert.get('type')
|
alert_type = alert.get('type')
|
||||||
if not alert_type:
|
if not alert_type:
|
||||||
continue
|
continue
|
||||||
@@ -2959,74 +3162,91 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
|||||||
if message:
|
if message:
|
||||||
yield alert_type, message
|
yield alert_type, message
|
||||||
|
|
||||||
def _extract_identity_token(self, webpage, item_id):
|
|
||||||
ytcfg = self._extract_ytcfg(item_id, webpage)
|
|
||||||
if ytcfg:
|
|
||||||
token = try_get(ytcfg, lambda x: x['ID_TOKEN'], compat_str)
|
|
||||||
if token:
|
|
||||||
return token
|
|
||||||
return self._search_regex(
|
|
||||||
r'\bID_TOKEN["\']\s*:\s*["\'](.+?)["\']', webpage,
|
|
||||||
'identity token', default=None)
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
item_id = self._match_id(url)
|
|
||||||
url = compat_urlparse.urlunparse(
|
|
||||||
compat_urlparse.urlparse(url)._replace(netloc='www.youtube.com'))
|
|
||||||
is_home = re.match(r'(?P<pre>%s)(?P<post>/?(?![^#?]).*$)' % self._VALID_URL, url)
|
|
||||||
if is_home is not None and is_home.group('not_channel') is None and item_id != 'feed':
|
|
||||||
self._downloader.report_warning(
|
|
||||||
'A channel/user page was given. All the channel\'s videos will be downloaded. '
|
|
||||||
'To download only the videos in the home page, add a "/featured" to the URL')
|
|
||||||
url = '%s/videos%s' % (is_home.group('pre'), is_home.group('post') or '')
|
|
||||||
|
|
||||||
# Handle both video/playlist URLs
|
|
||||||
qs = compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
|
|
||||||
video_id = qs.get('v', [None])[0]
|
|
||||||
playlist_id = qs.get('list', [None])[0]
|
|
||||||
|
|
||||||
if is_home is not None and is_home.group('not_channel') is not None and is_home.group('not_channel').startswith('watch') and not video_id:
|
|
||||||
if playlist_id:
|
|
||||||
self._downloader.report_warning('%s is not a valid Youtube URL. Trying to download playlist %s' % (url, playlist_id))
|
|
||||||
url = 'https://www.youtube.com/playlist?list=%s' % playlist_id
|
|
||||||
# return self.url_result(playlist_id, ie=YoutubePlaylistIE.ie_key())
|
|
||||||
else:
|
|
||||||
raise ExtractorError('Unable to recognize tab page')
|
|
||||||
if video_id and playlist_id:
|
|
||||||
if self._downloader.params.get('noplaylist'):
|
|
||||||
self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
|
|
||||||
return self.url_result(video_id, ie=YoutubeIE.ie_key(), video_id=video_id)
|
|
||||||
self.to_screen('Downloading playlist %s - add --no-playlist to just download video %s' % (playlist_id, video_id))
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, item_id)
|
|
||||||
identity_token = self._extract_identity_token(webpage, item_id)
|
|
||||||
data = self._extract_yt_initial_data(item_id, webpage)
|
|
||||||
err_msg = None
|
err_msg = None
|
||||||
for alert_type, alert_message in self._extract_alerts(data):
|
for alert_type, alert_message in _real_extract_alerts():
|
||||||
if alert_type.lower() == 'error':
|
if alert_type.lower() == 'error':
|
||||||
if err_msg:
|
if err_msg:
|
||||||
self._downloader.report_warning('YouTube said: %s - %s' % ('ERROR', err_msg))
|
self._downloader.report_warning('YouTube said: %s - %s' % ('ERROR', err_msg))
|
||||||
err_msg = alert_message
|
err_msg = alert_message
|
||||||
else:
|
else:
|
||||||
self._downloader.report_warning('YouTube said: %s - %s' % (alert_type, alert_message))
|
self._downloader.report_warning('YouTube said: %s - %s' % (alert_type, alert_message))
|
||||||
|
|
||||||
if err_msg:
|
if err_msg:
|
||||||
raise ExtractorError('YouTube said: %s' % err_msg, expected=True)
|
raise ExtractorError('YouTube said: %s' % err_msg, expected=expected)
|
||||||
|
|
||||||
|
def _extract_webpage(self, url, item_id):
|
||||||
|
retries = self._downloader.params.get('extractor_retries', 3)
|
||||||
|
count = -1
|
||||||
|
last_error = 'Incomplete yt initial data recieved'
|
||||||
|
while count < retries:
|
||||||
|
count += 1
|
||||||
|
# Sometimes youtube returns a webpage with incomplete ytInitialData
|
||||||
|
# See: https://github.com/yt-dlp/yt-dlp/issues/116
|
||||||
|
if count:
|
||||||
|
self.report_warning('%s. Retrying ...' % last_error)
|
||||||
|
webpage = self._download_webpage(
|
||||||
|
url, item_id,
|
||||||
|
'Downloading webpage%s' % (' (retry #%d)' % count if count else ''))
|
||||||
|
data = self._extract_yt_initial_data(item_id, webpage)
|
||||||
|
self._extract_alerts(data, expected=True)
|
||||||
|
if data.get('contents') or data.get('currentVideoEndpoint'):
|
||||||
|
break
|
||||||
|
if count >= retries:
|
||||||
|
self._downloader.report_error(last_error)
|
||||||
|
return webpage, data
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
item_id = self._match_id(url)
|
||||||
|
url = compat_urlparse.urlunparse(
|
||||||
|
compat_urlparse.urlparse(url)._replace(netloc='www.youtube.com'))
|
||||||
|
|
||||||
|
# This is not matched in a channel page with a tab selected
|
||||||
|
mobj = re.match(r'(?P<pre>%s)(?P<post>/?(?![^#?]).*$)' % self._VALID_URL, url)
|
||||||
|
mobj = mobj.groupdict() if mobj else {}
|
||||||
|
if mobj and not mobj.get('not_channel'):
|
||||||
|
self._downloader.report_warning(
|
||||||
|
'A channel/user page was given. All the channel\'s videos will be downloaded. '
|
||||||
|
'To download only the videos in the home page, add a "/featured" to the URL')
|
||||||
|
url = '%s/videos%s' % (mobj.get('pre'), mobj.get('post') or '')
|
||||||
|
|
||||||
|
# Handle both video/playlist URLs
|
||||||
|
qs = compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
|
||||||
|
video_id = qs.get('v', [None])[0]
|
||||||
|
playlist_id = qs.get('list', [None])[0]
|
||||||
|
|
||||||
|
if not video_id and (mobj.get('not_channel') or '').startswith('watch'):
|
||||||
|
if not playlist_id:
|
||||||
|
# If there is neither video or playlist ids,
|
||||||
|
# youtube redirects to home page, which is undesirable
|
||||||
|
raise ExtractorError('Unable to recognize tab page')
|
||||||
|
self._downloader.report_warning('A video URL was given without video ID. Trying to download playlist %s' % playlist_id)
|
||||||
|
url = 'https://www.youtube.com/playlist?list=%s' % playlist_id
|
||||||
|
|
||||||
|
if video_id and playlist_id:
|
||||||
|
if self._downloader.params.get('noplaylist'):
|
||||||
|
self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
|
||||||
|
return self.url_result(video_id, ie=YoutubeIE.ie_key(), video_id=video_id)
|
||||||
|
self.to_screen('Downloading playlist %s; add --no-playlist to just download video %s' % (playlist_id, video_id))
|
||||||
|
|
||||||
|
webpage, data = self._extract_webpage(url, item_id)
|
||||||
|
|
||||||
tabs = try_get(
|
tabs = try_get(
|
||||||
data, lambda x: x['contents']['twoColumnBrowseResultsRenderer']['tabs'], list)
|
data, lambda x: x['contents']['twoColumnBrowseResultsRenderer']['tabs'], list)
|
||||||
if tabs:
|
if tabs:
|
||||||
return self._extract_from_tabs(item_id, webpage, data, tabs, identity_token)
|
return self._extract_from_tabs(item_id, webpage, data, tabs)
|
||||||
|
|
||||||
playlist = try_get(
|
playlist = try_get(
|
||||||
data, lambda x: x['contents']['twoColumnWatchNextResults']['playlist']['playlist'], dict)
|
data, lambda x: x['contents']['twoColumnWatchNextResults']['playlist']['playlist'], dict)
|
||||||
if playlist:
|
if playlist:
|
||||||
return self._extract_from_playlist(item_id, url, data, playlist)
|
return self._extract_from_playlist(item_id, url, data, playlist)
|
||||||
# Fallback to video extraction if no playlist alike page is recognized.
|
|
||||||
# First check for the current video then try the v attribute of URL query.
|
|
||||||
video_id = try_get(
|
video_id = try_get(
|
||||||
data, lambda x: x['currentVideoEndpoint']['watchEndpoint']['videoId'],
|
data, lambda x: x['currentVideoEndpoint']['watchEndpoint']['videoId'],
|
||||||
compat_str) or video_id
|
compat_str) or video_id
|
||||||
if video_id:
|
if video_id:
|
||||||
|
self._downloader.report_warning('Unable to recognize playlist. Downloading just video %s' % video_id)
|
||||||
return self.url_result(video_id, ie=YoutubeIE.ie_key(), video_id=video_id)
|
return self.url_result(video_id, ie=YoutubeIE.ie_key(), video_id=video_id)
|
||||||
# Failed to recognize
|
|
||||||
raise ExtractorError('Unable to recognize tab page')
|
raise ExtractorError('Unable to recognize tab page')
|
||||||
|
|
||||||
|
|
||||||
@@ -3191,26 +3411,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(
|
||||||
@@ -3302,7 +3510,6 @@ class YoutubeFeedsInfoExtractor(YoutubeTabIE):
|
|||||||
Subclasses must define the _FEED_NAME property.
|
Subclasses must define the _FEED_NAME property.
|
||||||
"""
|
"""
|
||||||
_LOGIN_REQUIRED = True
|
_LOGIN_REQUIRED = True
|
||||||
# _MAX_PAGES = 5
|
|
||||||
_TESTS = []
|
_TESTS = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -3362,8 +3569,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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..compat import compat_str
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_age_limit,
|
parse_age_limit,
|
||||||
@@ -16,24 +17,34 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class Zee5IE(InfoExtractor):
|
class Zee5IE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?zee5\.com/[^#?]*/(?P<display_id>[-\w]+)/(?P<id>[-\d]+)'
|
_VALID_URL = r'''(?x)
|
||||||
|
(?:
|
||||||
|
zee5:|
|
||||||
|
(?:https?://)(?:www\.)?zee5\.com/(?:[^#?]+/)?
|
||||||
|
(?:
|
||||||
|
(?:tvshows|kids|zee5originals)(?:/[^#/?]+){3}
|
||||||
|
|movies/[^#/?]+
|
||||||
|
)/(?P<display_id>[^#/?]+)/
|
||||||
|
)
|
||||||
|
(?P<id>[^#/?]+)/?(?:$|[?#])
|
||||||
|
'''
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.zee5.com/movies/details/krishna-the-birth/0-0-63098',
|
'url': 'https://www.zee5.com/movies/details/krishna-the-birth/0-0-63098',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
"id": "0-0-63098",
|
'id': '0-0-63098',
|
||||||
"ext": "m3u8",
|
'ext': 'mp4',
|
||||||
"display_id": "krishna-the-birth",
|
'display_id': 'krishna-the-birth',
|
||||||
"title": "Krishna - The Birth",
|
'title': 'Krishna - The Birth',
|
||||||
"duration": 4368,
|
'duration': 4368,
|
||||||
"average_rating": 4,
|
'average_rating': 4,
|
||||||
"description": str,
|
'description': str,
|
||||||
"alt_title": "Krishna - The Birth",
|
'alt_title': 'Krishna - The Birth',
|
||||||
"uploader": "Zee Entertainment Enterprises Ltd",
|
'uploader': 'Zee Entertainment Enterprises Ltd',
|
||||||
"release_date": "20060101",
|
'release_date': '20060101',
|
||||||
"upload_date": "20060101",
|
'upload_date': '20060101',
|
||||||
"timestamp": 1136073600,
|
'timestamp': 1136073600,
|
||||||
"thumbnail": "https://akamaividz.zee5.com/resources/0-0-63098/list/270x152/0063098_list_80888170.jpg",
|
'thumbnail': 'https://akamaividz.zee5.com/resources/0-0-63098/list/270x152/0063098_list_80888170.jpg',
|
||||||
"tags": list
|
'tags': list
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'format': 'bv',
|
'format': 'bv',
|
||||||
@@ -41,37 +52,43 @@ class Zee5IE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://zee5.com/tvshows/details/krishna-balram/0-6-1871/episode-1-the-test-of-bramha/0-1-233402',
|
'url': 'https://zee5.com/tvshows/details/krishna-balram/0-6-1871/episode-1-the-test-of-bramha/0-1-233402',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
"id": "0-1-233402",
|
'id': '0-1-233402',
|
||||||
'ext': 'm3u8',
|
'ext': 'mp4',
|
||||||
"display_id": "episode-1-the-test-of-bramha",
|
'display_id': 'episode-1-the-test-of-bramha',
|
||||||
"title": "Episode 1 - The Test Of Bramha",
|
'title': 'Episode 1 - The Test Of Bramha',
|
||||||
"duration": 1336,
|
'duration': 1336,
|
||||||
"average_rating": 4,
|
'average_rating': 4,
|
||||||
"description": str,
|
'description': str,
|
||||||
"alt_title": "Episode 1 - The Test Of Bramha",
|
'alt_title': 'Episode 1 - The Test Of Bramha',
|
||||||
"uploader": "Green Gold",
|
'uploader': 'Green Gold',
|
||||||
"release_date": "20090101",
|
'release_date': '20090101',
|
||||||
"upload_date": "20090101",
|
'upload_date': '20090101',
|
||||||
"timestamp": 1230768000,
|
'timestamp': 1230768000,
|
||||||
"thumbnail": "https://akamaividz.zee5.com/resources/0-1-233402/list/270x152/01233402_list.jpg",
|
'thumbnail': 'https://akamaividz.zee5.com/resources/0-1-233402/list/270x152/01233402_list.jpg',
|
||||||
"series": "Krishna Balram",
|
'series': 'Krishna Balram',
|
||||||
"season_number": 1,
|
'season_number': 1,
|
||||||
"episode_number": 1,
|
'episode_number': 1,
|
||||||
"tags": list,
|
'tags': list,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'format': 'bv',
|
'format': 'bv',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.zee5.com/hi/tvshows/details/kundali-bhagya/0-6-366/kundali-bhagya-march-08-2021/0-1-manual_7g9jv1os7730?country=IN',
|
||||||
|
'only_matching': True
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.zee5.com/global/hi/tvshows/details/kundali-bhagya/0-6-366/kundali-bhagya-march-08-2021/0-1-manual_7g9jv1os7730',
|
||||||
|
'only_matching': True
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id, display_id = re.match(self._VALID_URL, url).group('id', 'display_id')
|
video_id, display_id = re.match(self._VALID_URL, url).group('id', 'display_id')
|
||||||
access_token_request = self._download_json(
|
access_token_request = self._download_json(
|
||||||
'https://useraction.zee5.com/token/platform_tokens.php?platform_name=web_app',
|
'https://useraction.zee5.com/token/platform_tokens.php?platform_name=web_app',
|
||||||
video_id, note="Downloading access token")
|
video_id, note='Downloading access token')
|
||||||
token_request = self._download_json(
|
token_request = self._download_json(
|
||||||
'https://useraction.zee5.com/tokennd',
|
'https://useraction.zee5.com/tokennd',
|
||||||
video_id, note="Downloading video token")
|
video_id, note='Downloading video token')
|
||||||
json_data = self._download_json(
|
json_data = self._download_json(
|
||||||
'https://gwapi.zee5.com/content/details/{}?translation=en&country=IN'.format(video_id),
|
'https://gwapi.zee5.com/content/details/{}?translation=en&country=IN'.format(video_id),
|
||||||
video_id, headers={'X-Access-Token': access_token_request['token']})
|
video_id, headers={'X-Access-Token': access_token_request['token']})
|
||||||
@@ -111,3 +128,78 @@ class Zee5IE(InfoExtractor):
|
|||||||
'episode_number': int_or_none(try_get(json_data, lambda x: x['index'])),
|
'episode_number': int_or_none(try_get(json_data, lambda x: x['index'])),
|
||||||
'tags': try_get(json_data, lambda x: x['tags'], list)
|
'tags': try_get(json_data, lambda x: x['tags'], list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Zee5SeriesIE(InfoExtractor):
|
||||||
|
IE_NAME = 'zee5:series'
|
||||||
|
_VALID_URL = r'''(?x)
|
||||||
|
(?:
|
||||||
|
zee5:series:|
|
||||||
|
(?:https?://)(?:www\.)?zee5\.com/(?:[^#?]+/)?
|
||||||
|
(?:tvshows|kids|zee5originals)(?:/[^#/?]+){2}/
|
||||||
|
)
|
||||||
|
(?P<id>[^#/?]+)/?(?:$|[?#])
|
||||||
|
'''
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.zee5.com/kids/kids-shows/krishna-balram/0-6-1871',
|
||||||
|
'playlist_mincount': 43,
|
||||||
|
'info_dict': {
|
||||||
|
'id': '0-6-1871',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.zee5.com/tvshows/details/bhabi-ji-ghar-par-hai/0-6-199',
|
||||||
|
'playlist_mincount': 1500,
|
||||||
|
'info_dict': {
|
||||||
|
'id': '0-6-199',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.zee5.com/tvshows/details/agent-raghav-crime-branch/0-6-965',
|
||||||
|
'playlist_mincount': 25,
|
||||||
|
'info_dict': {
|
||||||
|
'id': '0-6-965',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.zee5.com/ta/tvshows/details/nagabhairavi/0-6-3201',
|
||||||
|
'playlist_mincount': 3,
|
||||||
|
'info_dict': {
|
||||||
|
'id': '0-6-3201',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.zee5.com/global/hi/tvshows/details/khwaabon-ki-zamin-par/0-6-270',
|
||||||
|
'playlist_mincount': 150,
|
||||||
|
'info_dict': {
|
||||||
|
'id': '0-6-270',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def _entries(self, show_id):
|
||||||
|
access_token_request = self._download_json(
|
||||||
|
'https://useraction.zee5.com/token/platform_tokens.php?platform_name=web_app',
|
||||||
|
show_id, note='Downloading access token')
|
||||||
|
headers = {
|
||||||
|
'X-Access-Token': access_token_request['token'],
|
||||||
|
'Referer': 'https://www.zee5.com/',
|
||||||
|
}
|
||||||
|
show_url = 'https://gwapi.zee5.com/content/tvshow/{}?translation=en&country=IN'.format(show_id)
|
||||||
|
|
||||||
|
page_num = 0
|
||||||
|
show_json = self._download_json(show_url, video_id=show_id, headers=headers)
|
||||||
|
for season in show_json.get('seasons') or []:
|
||||||
|
season_id = try_get(season, lambda x: x['id'], compat_str)
|
||||||
|
next_url = 'https://gwapi.zee5.com/content/tvshow/?season_id={}&type=episode&translation=en&country=IN&on_air=false&asset_subtype=tvshow&page=1&limit=100'.format(season_id)
|
||||||
|
while next_url:
|
||||||
|
page_num += 1
|
||||||
|
episodes_json = self._download_json(
|
||||||
|
next_url, video_id=show_id, headers=headers,
|
||||||
|
note='Downloading JSON metadata page %d' % page_num)
|
||||||
|
for episode in try_get(episodes_json, lambda x: x['episode'], list) or []:
|
||||||
|
video_id = episode.get('id')
|
||||||
|
yield self.url_result(
|
||||||
|
'zee5:%s' % video_id,
|
||||||
|
ie=Zee5IE.ie_key(), video_id=video_id)
|
||||||
|
next_url = url_or_none(episodes_json.get('next_episode_api'))
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
show_id = self._match_id(url)
|
||||||
|
return self.playlist_result(self._entries(show_id), playlist_id=show_id)
|
||||||
|
|||||||
@@ -214,12 +214,11 @@ def parseOpts(overrideArguments=None):
|
|||||||
help='Mark videos watched (YouTube only)')
|
help='Mark videos watched (YouTube only)')
|
||||||
general.add_option(
|
general.add_option(
|
||||||
'--no-mark-watched',
|
'--no-mark-watched',
|
||||||
action='store_false', dest='mark_watched', default=False,
|
action='store_false', dest='mark_watched',
|
||||||
help='Do not mark videos watched')
|
help='Do not mark videos watched (default)')
|
||||||
general.add_option(
|
general.add_option(
|
||||||
'--no-colors',
|
'--no-colors',
|
||||||
action='store_true', dest='no_color',
|
action='store_true', dest='no_color', default=False,
|
||||||
default=False,
|
|
||||||
help='Do not emit color codes in output')
|
help='Do not emit color codes in output')
|
||||||
|
|
||||||
network = optparse.OptionGroup(parser, 'Network Options')
|
network = optparse.OptionGroup(parser, 'Network Options')
|
||||||
@@ -347,7 +346,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 +368,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',
|
||||||
@@ -558,6 +557,10 @@ def parseOpts(overrideArguments=None):
|
|||||||
help='Languages of the subtitles to download (optional) separated by commas, use --list-subs for available language tags')
|
help='Languages of the subtitles to download (optional) separated by commas, use --list-subs for available language tags')
|
||||||
|
|
||||||
downloader = optparse.OptionGroup(parser, 'Download Options')
|
downloader = optparse.OptionGroup(parser, 'Download Options')
|
||||||
|
downloader.add_option(
|
||||||
|
'-N', '--concurrent-fragments',
|
||||||
|
dest='concurrent_fragment_downloads', metavar='N', default=1, type=int,
|
||||||
|
help='Number of fragments to download concurrently (default is %default)')
|
||||||
downloader.add_option(
|
downloader.add_option(
|
||||||
'-r', '--limit-rate', '--rate-limit',
|
'-r', '--limit-rate', '--rate-limit',
|
||||||
dest='ratelimit', metavar='RATE',
|
dest='ratelimit', metavar='RATE',
|
||||||
@@ -634,16 +637,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 +699,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 +721,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 +988,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',
|
||||||
@@ -1073,8 +1090,8 @@ def parseOpts(overrideArguments=None):
|
|||||||
'The supported executables are: SponSkrub, FFmpeg, FFprobe, and AtomicParsley. '
|
'The supported executables are: SponSkrub, FFmpeg, FFprobe, and AtomicParsley. '
|
||||||
'You can also specify "PP+EXE:ARGS" to give the arguments to the specified executable '
|
'You can also specify "PP+EXE:ARGS" to give the arguments to the specified executable '
|
||||||
'only when being used by the specified postprocessor. Additionally, for ffmpeg/ffprobe, '
|
'only when being used by the specified postprocessor. Additionally, for ffmpeg/ffprobe, '
|
||||||
'a number can be appended to the exe name seperated by "_i" to pass the argument '
|
'"_i"/"_o" can be appended to the prefix optionally followed by a number to pass the argument '
|
||||||
'before the specified input file. Eg: --ppa "Merger+ffmpeg_i1:-v quiet". '
|
'before the specified input/output file. Eg: --ppa "Merger+ffmpeg_i1:-v quiet". '
|
||||||
'You can use this option multiple times to give different arguments to different '
|
'You can use this option multiple times to give different arguments to different '
|
||||||
'postprocessors. (Alias: --ppa)'))
|
'postprocessors. (Alias: --ppa)'))
|
||||||
postproc.add_option(
|
postproc.add_option(
|
||||||
@@ -1129,7 +1146,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". '
|
||||||
@@ -1140,7 +1157,7 @@ def parseOpts(overrideArguments=None):
|
|||||||
help='Write metadata to the video file\'s xattrs (using dublin core and xdg standards)')
|
help='Write metadata to the video file\'s xattrs (using dublin core and xdg standards)')
|
||||||
postproc.add_option(
|
postproc.add_option(
|
||||||
'--fixup',
|
'--fixup',
|
||||||
metavar='POLICY', dest='fixup', default='detect_or_warn',
|
metavar='POLICY', dest='fixup', default=None,
|
||||||
help=(
|
help=(
|
||||||
'Automatically correct known faults of the file. '
|
'Automatically correct known faults of the file. '
|
||||||
'One of never (do nothing), warn (only emit a warning), '
|
'One of never (do nothing), warn (only emit a warning), '
|
||||||
@@ -1165,6 +1182,17 @@ def parseOpts(overrideArguments=None):
|
|||||||
'--convert-subs', '--convert-subtitles',
|
'--convert-subs', '--convert-subtitles',
|
||||||
metavar='FORMAT', dest='convertsubtitles', default=None,
|
metavar='FORMAT', dest='convertsubtitles', default=None,
|
||||||
help='Convert the subtitles to other format (currently supported: srt|ass|vtt|lrc)')
|
help='Convert the subtitles to other format (currently supported: srt|ass|vtt|lrc)')
|
||||||
|
postproc.add_option(
|
||||||
|
'--split-chapters', '--split-tracks',
|
||||||
|
dest='split_chapters', action='store_true', default=False,
|
||||||
|
help=(
|
||||||
|
'Split video into multiple files based on internal chapters. '
|
||||||
|
'The "chapter:" prefix can be used with "--paths" and "--output" to '
|
||||||
|
'set the output filename for the split files. See "OUTPUT TEMPLATE" for details'))
|
||||||
|
postproc.add_option(
|
||||||
|
'--no-split-chapters', '--no-split-tracks',
|
||||||
|
dest='split_chapters', action='store_false',
|
||||||
|
help='Do not split video based on chapters (default)')
|
||||||
|
|
||||||
sponskrub = optparse.OptionGroup(parser, 'SponSkrub (SponsorBlock) Options', description=(
|
sponskrub = optparse.OptionGroup(parser, 'SponSkrub (SponsorBlock) Options', description=(
|
||||||
'SponSkrub (https://github.com/yt-dlp/SponSkrub) is a utility to mark/remove sponsor segments '
|
'SponSkrub (https://github.com/yt-dlp/SponSkrub) is a utility to mark/remove sponsor segments '
|
||||||
@@ -1204,6 +1232,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,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from .ffmpeg import (
|
|||||||
FFmpegVideoConvertorPP,
|
FFmpegVideoConvertorPP,
|
||||||
FFmpegVideoRemuxerPP,
|
FFmpegVideoRemuxerPP,
|
||||||
FFmpegSubtitlesConvertorPP,
|
FFmpegSubtitlesConvertorPP,
|
||||||
|
FFmpegSplitChaptersPP,
|
||||||
)
|
)
|
||||||
from .xattrpp import XAttrMetadataPP
|
from .xattrpp import XAttrMetadataPP
|
||||||
from .execafterdownload import ExecAfterDownloadPP
|
from .execafterdownload import ExecAfterDownloadPP
|
||||||
@@ -31,6 +32,7 @@ __all__ = [
|
|||||||
'ExecAfterDownloadPP',
|
'ExecAfterDownloadPP',
|
||||||
'FFmpegEmbedSubtitlePP',
|
'FFmpegEmbedSubtitlePP',
|
||||||
'FFmpegExtractAudioPP',
|
'FFmpegExtractAudioPP',
|
||||||
|
'FFmpegSplitChaptersPP',
|
||||||
'FFmpegFixupM3u8PP',
|
'FFmpegFixupM3u8PP',
|
||||||
'FFmpegFixupM4aPP',
|
'FFmpegFixupM4aPP',
|
||||||
'FFmpegFixupStretchedPP',
|
'FFmpegFixupStretchedPP',
|
||||||
|
|||||||
@@ -91,10 +91,18 @@ class PostProcessor(object):
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.report_warning(errnote)
|
self.report_warning(errnote)
|
||||||
|
|
||||||
def _configuration_args(self, *args, **kwargs):
|
def _configuration_args(self, exe, keys=None, default=[], use_compat=True):
|
||||||
|
pp_key = self.pp_key().lower()
|
||||||
|
exe = exe.lower()
|
||||||
|
root_key = exe if pp_key == exe else '%s+%s' % (pp_key, exe)
|
||||||
|
keys = ['%s%s' % (root_key, k) for k in (keys or [''])]
|
||||||
|
if root_key in keys:
|
||||||
|
keys += [root_key] + ([] if pp_key == exe else [(self.pp_key(), exe)]) + ['default']
|
||||||
|
else:
|
||||||
|
use_compat = False
|
||||||
return cli_configuration_args(
|
return cli_configuration_args(
|
||||||
self._downloader.params.get('postprocessor_args'),
|
self._downloader.params.get('postprocessor_args'),
|
||||||
self.pp_key().lower(), *args, **kwargs)
|
keys, default, use_compat)
|
||||||
|
|
||||||
|
|
||||||
class AudioConversionError(PostProcessingError):
|
class AudioConversionError(PostProcessingError):
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
|||||||
thumbnail_filename = thumbnail_jpg_filename
|
thumbnail_filename = thumbnail_jpg_filename
|
||||||
thumbnail_ext = 'jpg'
|
thumbnail_ext = 'jpg'
|
||||||
|
|
||||||
|
mtime = os.stat(encodeFilename(filename)).st_mtime
|
||||||
|
|
||||||
success = True
|
success = True
|
||||||
if info['ext'] == 'mp3':
|
if info['ext'] == 'mp3':
|
||||||
options = [
|
options = [
|
||||||
@@ -139,7 +141,7 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
|||||||
encodeFilename(thumbnail_filename, True),
|
encodeFilename(thumbnail_filename, True),
|
||||||
encodeArgument('-o'),
|
encodeArgument('-o'),
|
||||||
encodeFilename(temp_filename, True)]
|
encodeFilename(temp_filename, True)]
|
||||||
cmd += [encodeArgument(o) for o in self._configuration_args(exe='AtomicParsley')]
|
cmd += [encodeArgument(o) for o in self._configuration_args('AtomicParsley')]
|
||||||
|
|
||||||
self.to_screen('Adding thumbnail to "%s"' % filename)
|
self.to_screen('Adding thumbnail to "%s"' % filename)
|
||||||
self.write_debug('AtomicParsley command line: %s' % shell_quote(cmd))
|
self.write_debug('AtomicParsley command line: %s' % shell_quote(cmd))
|
||||||
@@ -187,10 +189,14 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
|
|||||||
os.remove(encodeFilename(filename))
|
os.remove(encodeFilename(filename))
|
||||||
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
|
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
|
||||||
|
|
||||||
|
self.try_utime(filename, mtime, mtime)
|
||||||
|
|
||||||
files_to_delete = [thumbnail_filename]
|
files_to_delete = [thumbnail_filename]
|
||||||
if self._already_have_thumbnail:
|
if self._already_have_thumbnail:
|
||||||
info['__files_to_move'][original_thumbnail] = replace_extension(
|
info['__files_to_move'][original_thumbnail] = replace_extension(
|
||||||
info['__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
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import json
|
|||||||
|
|
||||||
from .common import AudioConversionError, PostProcessor
|
from .common import AudioConversionError, PostProcessor
|
||||||
|
|
||||||
|
from ..compat import compat_str
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
encodeArgument,
|
encodeArgument,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
@@ -234,25 +235,35 @@ class FFmpegPostProcessor(PostProcessor):
|
|||||||
return num, len(streams)
|
return num, len(streams)
|
||||||
|
|
||||||
def run_ffmpeg_multiple_files(self, input_paths, out_path, opts):
|
def run_ffmpeg_multiple_files(self, input_paths, out_path, opts):
|
||||||
|
return self.real_run_ffmpeg(
|
||||||
|
[(path, []) for path in input_paths],
|
||||||
|
[(out_path, opts)])
|
||||||
|
|
||||||
|
def real_run_ffmpeg(self, input_path_opts, output_path_opts):
|
||||||
self.check_version()
|
self.check_version()
|
||||||
|
|
||||||
oldest_mtime = min(
|
oldest_mtime = min(
|
||||||
os.stat(encodeFilename(path)).st_mtime for path in input_paths)
|
os.stat(encodeFilename(path)).st_mtime for path, _ in input_path_opts)
|
||||||
|
|
||||||
cmd = [encodeFilename(self.executable, True), encodeArgument('-y')]
|
cmd = [encodeFilename(self.executable, True), encodeArgument('-y')]
|
||||||
# avconv does not have repeat option
|
# avconv does not have repeat option
|
||||||
if self.basename == 'ffmpeg':
|
if self.basename == 'ffmpeg':
|
||||||
cmd += [encodeArgument('-loglevel'), encodeArgument('repeat+info')]
|
cmd += [encodeArgument('-loglevel'), encodeArgument('repeat+info')]
|
||||||
|
|
||||||
def make_args(file, pre=[], post=[], *args, **kwargs):
|
def make_args(file, args, name, number):
|
||||||
args = pre + self._configuration_args(*args, **kwargs) + post
|
keys = ['_%s%d' % (name, number), '_%s' % name]
|
||||||
|
if name == 'o' and number == 1:
|
||||||
|
keys.append('')
|
||||||
|
args += self._configuration_args(self.basename, keys)
|
||||||
|
if name == 'i':
|
||||||
|
args.append('-i')
|
||||||
return (
|
return (
|
||||||
[encodeArgument(o) for o in args]
|
[encodeArgument(arg) for arg in args]
|
||||||
+ [encodeFilename(self._ffmpeg_filename_argument(file), True)])
|
+ [encodeFilename(self._ffmpeg_filename_argument(file), True)])
|
||||||
|
|
||||||
for i, path in enumerate(input_paths):
|
for arg_type, path_opts in (('i', input_path_opts), ('o', output_path_opts)):
|
||||||
cmd += make_args(path, post=['-i'], exe='%s_i%d' % (self.basename, i + 1), use_default_arg=False)
|
cmd += [arg for i, o in enumerate(path_opts)
|
||||||
cmd += make_args(out_path, pre=opts, exe=self.basename)
|
for arg in make_args(o[0], o[1], arg_type, i + 1)]
|
||||||
|
|
||||||
self.write_debug('ffmpeg command line: %s' % shell_quote(cmd))
|
self.write_debug('ffmpeg command line: %s' % shell_quote(cmd))
|
||||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||||
@@ -262,6 +273,7 @@ class FFmpegPostProcessor(PostProcessor):
|
|||||||
if self.get_param('verbose', False):
|
if self.get_param('verbose', False):
|
||||||
self.report_error(stderr)
|
self.report_error(stderr)
|
||||||
raise FFmpegPostProcessorError(stderr.split('\n')[-1])
|
raise FFmpegPostProcessorError(stderr.split('\n')[-1])
|
||||||
|
for out_path, _ in output_path_opts:
|
||||||
self.try_utime(out_path, oldest_mtime, oldest_mtime)
|
self.try_utime(out_path, oldest_mtime, oldest_mtime)
|
||||||
return stderr.decode('utf-8', 'replace')
|
return stderr.decode('utf-8', 'replace')
|
||||||
|
|
||||||
@@ -758,3 +770,40 @@ class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return sub_filenames, info
|
return sub_filenames, info
|
||||||
|
|
||||||
|
|
||||||
|
class FFmpegSplitChaptersPP(FFmpegPostProcessor):
|
||||||
|
|
||||||
|
def _prepare_filename(self, number, chapter, info):
|
||||||
|
info = info.copy()
|
||||||
|
info.update({
|
||||||
|
'section_number': number,
|
||||||
|
'section_title': chapter.get('title'),
|
||||||
|
'section_start': chapter.get('start_time'),
|
||||||
|
'section_end': chapter.get('end_time'),
|
||||||
|
})
|
||||||
|
return self._downloader.prepare_filename(info, 'chapter')
|
||||||
|
|
||||||
|
def _ffmpeg_args_for_chapter(self, number, chapter, info):
|
||||||
|
destination = self._prepare_filename(number, chapter, info)
|
||||||
|
if not self._downloader._ensure_dir_exists(encodeFilename(destination)):
|
||||||
|
return
|
||||||
|
|
||||||
|
chapter['_filename'] = destination
|
||||||
|
self.to_screen('Chapter %03d; Destination: %s' % (number, destination))
|
||||||
|
return (
|
||||||
|
destination,
|
||||||
|
['-ss', compat_str(chapter['start_time']),
|
||||||
|
'-to', compat_str(chapter['end_time'])])
|
||||||
|
|
||||||
|
def run(self, info):
|
||||||
|
chapters = info.get('chapters') or []
|
||||||
|
if not chapters:
|
||||||
|
self.report_warning('There are no tracks to extract')
|
||||||
|
return [], info
|
||||||
|
|
||||||
|
self.to_screen('Splitting video by chapters; %d chapters found' % len(chapters))
|
||||||
|
for idx, chapter in enumerate(chapters):
|
||||||
|
destination, opts = self._ffmpeg_args_for_chapter(idx + 1, chapter, info)
|
||||||
|
self.real_run_ffmpeg([(info['filepath'], opts)], [(destination, ['-c', 'copy'])])
|
||||||
|
return [], info
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class SponSkrubPP(PostProcessor):
|
|||||||
if not self.cutout:
|
if not self.cutout:
|
||||||
cmd += ['-chapter']
|
cmd += ['-chapter']
|
||||||
cmd += compat_shlex_split(self.args) # For backward compatibility
|
cmd += compat_shlex_split(self.args) # For backward compatibility
|
||||||
cmd += self._configuration_args(exe=self._exe_name, use_default_arg='no_compat')
|
cmd += self._configuration_args(self._exe_name, use_compat=False)
|
||||||
cmd += ['--', information['id'], filename, temp_filename]
|
cmd += ['--', information['id'], filename, temp_filename]
|
||||||
cmd = [encodeArgument(i) for i in cmd]
|
cmd = [encodeArgument(i) for i in cmd]
|
||||||
|
|
||||||
|
|||||||
@@ -49,12 +49,16 @@ def update_self(to_screen, verbose, opener):
|
|||||||
h.update(mv[:n])
|
h.update(mv[:n])
|
||||||
return h.hexdigest()
|
return h.hexdigest()
|
||||||
|
|
||||||
to_screen('Current Build Hash %s' % calc_sha256sum(sys.executable))
|
|
||||||
|
|
||||||
if not isinstance(globals().get('__loader__'), zipimporter) and not hasattr(sys, 'frozen'):
|
if not isinstance(globals().get('__loader__'), zipimporter) and not hasattr(sys, 'frozen'):
|
||||||
to_screen('It looks like you installed yt-dlp with a package manager, pip, setup.py or a tarball. Please use that to update.')
|
to_screen('It looks like you installed yt-dlp with a package manager, pip, setup.py or a tarball. Please use that to update.')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# sys.executable is set to the full pathname of the exe-file for py2exe
|
||||||
|
# though symlinks are not followed so that we need to do this manually
|
||||||
|
# with help of realpath
|
||||||
|
filename = compat_realpath(sys.executable if hasattr(sys, 'frozen') else sys.argv[0])
|
||||||
|
to_screen('Current Build Hash %s' % calc_sha256sum(filename))
|
||||||
|
|
||||||
# Download and check versions info
|
# Download and check versions info
|
||||||
try:
|
try:
|
||||||
version_info = opener.open(JSON_URL).read().decode('utf-8')
|
version_info = opener.open(JSON_URL).read().decode('utf-8')
|
||||||
@@ -103,11 +107,6 @@ def update_self(to_screen, verbose, opener):
|
|||||||
(i[1] for i in hashes if i[0] == 'yt-dlp%s' % label),
|
(i[1] for i in hashes if i[0] == 'yt-dlp%s' % label),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
# sys.executable is set to the full pathname of the exe-file for py2exe
|
|
||||||
# though symlinks are not followed so that we need to do this manually
|
|
||||||
# with help of realpath
|
|
||||||
filename = compat_realpath(sys.executable if hasattr(sys, 'frozen') else sys.argv[0])
|
|
||||||
|
|
||||||
if not os.access(filename, os.W_OK):
|
if not os.access(filename, os.W_OK):
|
||||||
to_screen('ERROR: no write permissions on %s' % filename)
|
to_screen('ERROR: no write permissions on %s' % filename)
|
||||||
return
|
return
|
||||||
@@ -198,28 +197,18 @@ def update_self(to_screen, verbose, opener):
|
|||||||
to_screen('Visit https://github.com/yt-dlp/yt-dlp/releases/latest')
|
to_screen('Visit https://github.com/yt-dlp/yt-dlp/releases/latest')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
expected_sum = get_sha256sum('zip', py_ver)
|
||||||
|
if expected_sum and hashlib.sha256(newcontent).hexdigest() != expected_sum:
|
||||||
|
to_screen('ERROR: unable to verify the new zip')
|
||||||
|
to_screen('Visit https://github.com/yt-dlp/yt-dlp/releases/latest')
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(filename + '.new', 'wb') as outf:
|
with open(filename, 'wb') as outf:
|
||||||
outf.write(newcontent)
|
outf.write(newcontent)
|
||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
if verbose:
|
if verbose:
|
||||||
to_screen(encode_compat_str(traceback.format_exc()))
|
to_screen(encode_compat_str(traceback.format_exc()))
|
||||||
to_screen('ERROR: unable to write the new version')
|
|
||||||
return
|
|
||||||
|
|
||||||
expected_sum = get_sha256sum('zip', py_ver)
|
|
||||||
if expected_sum and calc_sha256sum(filename + '.new') != expected_sum:
|
|
||||||
to_screen('ERROR: unable to verify the new zip')
|
|
||||||
to_screen('Visit https://github.com/yt-dlp/yt-dlp/releases/latest')
|
|
||||||
try:
|
|
||||||
os.remove(filename + '.new')
|
|
||||||
except OSError:
|
|
||||||
to_screen('ERROR: unable to remove corrupt zip')
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.rename(filename + '.new', filename)
|
|
||||||
except OSError:
|
|
||||||
to_screen('ERROR: unable to overwrite current version')
|
to_screen('ERROR: unable to overwrite current version')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -4182,8 +4182,10 @@ def qualities(quality_ids):
|
|||||||
|
|
||||||
DEFAULT_OUTTMPL = {
|
DEFAULT_OUTTMPL = {
|
||||||
'default': '%(title)s [%(id)s].%(ext)s',
|
'default': '%(title)s [%(id)s].%(ext)s',
|
||||||
|
'chapter': '%(title)s - %(section_number)03d %(section_title)s [%(id)s].%(ext)s',
|
||||||
}
|
}
|
||||||
OUTTMPL_TYPES = {
|
OUTTMPL_TYPES = {
|
||||||
|
'chapter': None,
|
||||||
'subtitle': None,
|
'subtitle': None,
|
||||||
'thumbnail': None,
|
'thumbnail': None,
|
||||||
'description': 'description',
|
'description': 'description',
|
||||||
@@ -4692,36 +4694,26 @@ def cli_valueless_option(params, command_option, param, expected_value=True):
|
|||||||
return [command_option] if param == expected_value else []
|
return [command_option] if param == expected_value else []
|
||||||
|
|
||||||
|
|
||||||
def cli_configuration_args(argdict, key, default=[], exe=None, use_default_arg=True):
|
def cli_configuration_args(argdict, keys, default=[], use_compat=True):
|
||||||
# use_default_arg can be True, False, or 'no_compat'
|
|
||||||
if isinstance(argdict, (list, tuple)): # for backward compatibility
|
if isinstance(argdict, (list, tuple)): # for backward compatibility
|
||||||
if use_default_arg is True:
|
if use_compat:
|
||||||
return argdict
|
return argdict
|
||||||
else:
|
else:
|
||||||
argdict = None
|
argdict = None
|
||||||
|
|
||||||
if argdict is None:
|
if argdict is None:
|
||||||
return default
|
return default
|
||||||
assert isinstance(argdict, dict)
|
assert isinstance(argdict, dict)
|
||||||
|
|
||||||
key = key.lower()
|
assert isinstance(keys, (list, tuple))
|
||||||
args = exe_args = None
|
for key_list in keys:
|
||||||
if exe is not None:
|
if isinstance(key_list, compat_str):
|
||||||
assert isinstance(exe, compat_str)
|
key_list = (key_list,)
|
||||||
exe = exe.lower()
|
arg_list = list(filter(
|
||||||
args = argdict.get('%s+%s' % (key, exe))
|
lambda x: x is not None,
|
||||||
if args is None:
|
[argdict.get(key.lower()) for key in key_list]))
|
||||||
exe_args = argdict.get(exe)
|
if arg_list:
|
||||||
|
return [arg for args in arg_list for arg in args]
|
||||||
if args is None:
|
return default
|
||||||
args = argdict.get(key) if key != exe else None
|
|
||||||
if args is None and exe_args is None:
|
|
||||||
args = argdict.get('default', default) if use_default_arg else default
|
|
||||||
|
|
||||||
args, exe_args = args or [], exe_args or []
|
|
||||||
assert isinstance(args, (list, tuple))
|
|
||||||
assert isinstance(exe_args, (list, tuple))
|
|
||||||
return args + exe_args
|
|
||||||
|
|
||||||
|
|
||||||
class ISO639Utils(object):
|
class ISO639Utils(object):
|
||||||
@@ -5945,9 +5937,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,3 +1,3 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
__version__ = '2021.02.19'
|
__version__ = '2021.03.07'
|
||||||
|
|||||||
Reference in New Issue
Block a user