mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2025-12-08 15:12:47 +01:00
Compare commits
110 Commits
2021.01.24
...
2021.02.19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4524baf056 | ||
|
|
bc2ca1bb75 | ||
|
|
5e41dca334 | ||
|
|
2a86f3da07 | ||
|
|
a40258a259 | ||
|
|
ba7bf12d89 | ||
|
|
f983b87567 | ||
|
|
dca3ff4a5e | ||
|
|
da9be05edf | ||
|
|
155d2b48c5 | ||
|
|
54f37eeabd | ||
|
|
9ba5705ac0 | ||
|
|
c2934512c2 | ||
|
|
55e36f035c | ||
|
|
c86d5023d0 | ||
|
|
42bb0c59f8 | ||
|
|
c3e1f0c4f2 | ||
|
|
6b027907ce | ||
|
|
f3b7c69377 | ||
|
|
46261325be | ||
|
|
78b9a616cc | ||
|
|
55b53b338b | ||
|
|
d16ab6ef1c | ||
|
|
aa837ddf06 | ||
|
|
a718ef84c8 | ||
|
|
44f705d001 | ||
|
|
47930b73a5 | ||
|
|
1de75fa129 | ||
|
|
6285297795 | ||
|
|
e5813e53f0 | ||
|
|
273762c8d0 | ||
|
|
7620cd46c3 | ||
|
|
068693675e | ||
|
|
1ea2412927 | ||
|
|
63ad4d43eb | ||
|
|
584bab3766 | ||
|
|
fc2119f210 | ||
|
|
5d25607a3a | ||
|
|
a96c6d154a | ||
|
|
cc2db87805 | ||
|
|
539d158c50 | ||
|
|
fb198a8a9c | ||
|
|
8d801631cf | ||
|
|
ba9f36d732 | ||
|
|
cffab0eefc | ||
|
|
2e339f59c3 | ||
|
|
6c4fd172de | ||
|
|
deaec5afc2 | ||
|
|
69184e4152 | ||
|
|
a1b535bd75 | ||
|
|
b3943b2f33 | ||
|
|
3dd264bf42 | ||
|
|
efabc16165 | ||
|
|
5219cb3e75 | ||
|
|
ff84930c86 | ||
|
|
06ff212d64 | ||
|
|
1bf540d28b | ||
|
|
df692c5a7a | ||
|
|
ecc97af344 | ||
|
|
8a0b932258 | ||
|
|
4d608b522f | ||
|
|
885d36d4e4 | ||
|
|
0fd1a2b0bf | ||
|
|
c25228e5da | ||
|
|
de6000d913 | ||
|
|
ff88a05cff | ||
|
|
8a784c74d1 | ||
|
|
545cc85d11 | ||
|
|
c10d0213fc | ||
|
|
2181983a0c | ||
|
|
e29663c644 | ||
|
|
9c3fe2ef80 | ||
|
|
b60419c51a | ||
|
|
18590cecdb | ||
|
|
9f888147de | ||
|
|
e8be92f9d6 | ||
|
|
b9d973bef1 | ||
|
|
c55256c5a3 | ||
|
|
82e3f6ebda | ||
|
|
af819c216f | ||
|
|
e3b771a898 | ||
|
|
cac96421d9 | ||
|
|
7c245ce877 | ||
|
|
eabce90175 | ||
|
|
29b6000e35 | ||
|
|
e38df8f9fa | ||
|
|
caa15a7b57 | ||
|
|
105b0b700e | ||
|
|
66c935fb16 | ||
|
|
64c0d954e5 | ||
|
|
bf330f5f29 | ||
|
|
f6d7624f57 | ||
|
|
ece8a2a1b6 | ||
|
|
8d0ea5f955 | ||
|
|
0748b3317b | ||
|
|
6b591b2925 | ||
|
|
179122495b | ||
|
|
02fd60d305 | ||
|
|
06167fbbd3 | ||
|
|
4ff5e98991 | ||
|
|
e4172ac903 | ||
|
|
5bfa486205 | ||
|
|
9882064024 | ||
|
|
2d6921210d | ||
|
|
f137c99e9f | ||
|
|
6b8eb0c024 | ||
|
|
5b328c97d7 | ||
|
|
b5d265633d | ||
|
|
a392adf56c | ||
|
|
0bc0a32290 |
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 youtube-dlc:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
|
||||||
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.20. If it's not, see https://github.com/pukkandan/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 `youtube-dlc --version` and ensure your version is 2021.02.16. If it's not, see https://github.com/pukkandan/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/pukkandan/yt-dlp.
|
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/pukkandan/yt-dlp.
|
||||||
- Search the bugtracker for similar issues: https://github.com/pukkandan/yt-dlp. DO NOT post duplicates.
|
- Search the bugtracker for similar issues: https://github.com/pukkandan/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.01.20**
|
- [ ] I've verified that I'm running yt-dlp version **2021.02.16**
|
||||||
- [ ] 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 youtube-dlc with (`youtube-dlc -v
|
|||||||
[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.01.20
|
[debug] yt-dlp version 2021.02.16
|
||||||
[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 youtube-dlc:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
|
||||||
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.20. If it's not, see https://github.com/pukkandan/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 `youtube-dlc --version` and ensure your version is 2021.02.16. If it's not, see https://github.com/pukkandan/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/pukkandan/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/pukkandan/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/pukkandan/yt-dlp. DO NOT post duplicates.
|
- Search the bugtracker for similar site support requests: https://github.com/pukkandan/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.01.20**
|
- [ ] I've verified that I'm running yt-dlp version **2021.02.16**
|
||||||
- [ ] 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 youtube-dlc:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
|
||||||
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.20. If it's not, see https://github.com/pukkandan/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 `youtube-dlc --version` and ensure your version is 2021.02.16. If it's not, see https://github.com/pukkandan/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||||
- Search the bugtracker for similar site feature requests: https://github.com/pukkandan/yt-dlp. DO NOT post duplicates.
|
- Search the bugtracker for similar site feature requests: https://github.com/pukkandan/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.01.20**
|
- [ ] I've verified that I'm running yt-dlp version **2021.02.16**
|
||||||
- [ ] 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 youtube-dlc:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
|
||||||
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.20. If it's not, see https://github.com/pukkandan/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 `youtube-dlc --version` and ensure your version is 2021.02.16. If it's not, see https://github.com/pukkandan/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/pukkandan/yt-dlp.
|
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/pukkandan/yt-dlp.
|
||||||
- Search the bugtracker for similar issues: https://github.com/pukkandan/yt-dlp. DO NOT post duplicates.
|
- Search the bugtracker for similar issues: https://github.com/pukkandan/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.01.20**
|
- [ ] I've verified that I'm running yt-dlp version **2021.02.16**
|
||||||
- [ ] 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 youtube-dlc with (`youtube-dlc -v
|
|||||||
[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.01.20
|
[debug] yt-dlp version 2021.02.16
|
||||||
[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 youtube-dlc:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
|
||||||
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.20. If it's not, see https://github.com/pukkandan/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 `youtube-dlc --version` and ensure your version is 2021.02.16. If it's not, see https://github.com/pukkandan/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||||
- Search the bugtracker for similar feature requests: https://github.com/pukkandan/yt-dlp. DO NOT post duplicates.
|
- Search the bugtracker for similar feature requests: https://github.com/pukkandan/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.01.20**
|
- [ ] I've verified that I'm running yt-dlp version **2021.02.16**
|
||||||
- [ ] I've searched the bugtracker for similar feature requests including closed ones
|
- [ ] I've searched the bugtracker for similar feature requests including closed ones
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
64
.github/workflows/build.yml
vendored
64
.github/workflows/build.yml
vendored
@@ -25,8 +25,8 @@ jobs:
|
|||||||
run: sudo apt-get -y install zip pandoc man
|
run: sudo apt-get -y install zip pandoc man
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
id: bump_version
|
id: bump_version
|
||||||
run: python scripts/update-version-workflow.py
|
run: python devscripts/update-version.py
|
||||||
- name: Check the output from My action
|
- name: Print version
|
||||||
run: echo "${{ steps.bump_version.outputs.ytdlc_version }}"
|
run: echo "${{ steps.bump_version.outputs.ytdlc_version }}"
|
||||||
- name: Run Make
|
- name: Run Make
|
||||||
run: make
|
run: make
|
||||||
@@ -55,10 +55,11 @@ jobs:
|
|||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
- name: Get SHA2-256SUMS for youtube-dlc
|
- name: Get SHA2-256SUMS for youtube-dlc
|
||||||
id: sha2_file
|
id: sha2_file
|
||||||
env:
|
run: echo "::set-output name=sha2_unix::$(sha256sum youtube-dlc | awk '{print $1}')"
|
||||||
SHA2: ${{ hashFiles('youtube-dlc') }}
|
|
||||||
run: echo "::set-output name=sha2_unix::$SHA2"
|
|
||||||
- name: Install dependencies for pypi
|
- name: Install dependencies for pypi
|
||||||
|
env:
|
||||||
|
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||||
|
if: "env.PYPI_TOKEN != ''"
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install setuptools wheel twine
|
pip install setuptools wheel twine
|
||||||
@@ -66,6 +67,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
TWINE_USERNAME: __token__
|
TWINE_USERNAME: __token__
|
||||||
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
||||||
|
if: "env.TWINE_PASSWORD != ''"
|
||||||
run: |
|
run: |
|
||||||
rm -rf dist/*
|
rm -rf dist/*
|
||||||
python setup.py sdist bdist_wheel
|
python setup.py sdist bdist_wheel
|
||||||
@@ -75,6 +77,9 @@ jobs:
|
|||||||
|
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
sha2_windows: ${{ steps.sha2_file_win.outputs.sha2_windows }}
|
||||||
|
|
||||||
needs: build_unix
|
needs: build_unix
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -83,12 +88,17 @@ jobs:
|
|||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: '3.8'
|
python-version: '3.8'
|
||||||
|
- name: Upgrade pip and enable wheel support
|
||||||
|
run: python -m pip install --upgrade pip setuptools wheel
|
||||||
- name: Install Requirements
|
- name: Install Requirements
|
||||||
run: pip install pyinstaller
|
run: pip install pyinstaller mutagen pycryptodome
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
run: python scripts/update-version-workflow.py
|
id: bump_version
|
||||||
|
run: python devscripts/update-version.py
|
||||||
|
- name: Print version
|
||||||
|
run: echo "${{ steps.bump_version.outputs.ytdlc_version }}"
|
||||||
- name: Run PyInstaller Script
|
- name: Run PyInstaller Script
|
||||||
run: python pyinst.py
|
run: python pyinst.py 64
|
||||||
- name: Upload youtube-dlc.exe Windows binary
|
- name: Upload youtube-dlc.exe Windows binary
|
||||||
id: upload-release-windows
|
id: upload-release-windows
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
@@ -101,14 +111,15 @@ jobs:
|
|||||||
asset_content_type: application/vnd.microsoft.portable-executable
|
asset_content_type: application/vnd.microsoft.portable-executable
|
||||||
- name: Get SHA2-256SUMS for youtube-dlc.exe
|
- name: Get SHA2-256SUMS for youtube-dlc.exe
|
||||||
id: sha2_file_win
|
id: sha2_file_win
|
||||||
env:
|
run: echo "::set-output name=sha2_windows::$((Get-FileHash dist\youtube-dlc.exe -Algorithm SHA256).Hash.ToLower())"
|
||||||
SHA2_win: ${{ hashFiles('dist/youtube-dlc.exe') }}
|
|
||||||
run: echo "::set-output name=sha2_windows::$SHA2_win"
|
|
||||||
|
|
||||||
build_windows32:
|
build_windows32:
|
||||||
|
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
sha2_windows32: ${{ steps.sha2_file_win32.outputs.sha2_windows32 }}
|
||||||
|
|
||||||
needs: [build_unix, build_windows]
|
needs: [build_unix, build_windows]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -118,12 +129,17 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: '3.4.4'
|
python-version: '3.4.4'
|
||||||
architecture: 'x86'
|
architecture: 'x86'
|
||||||
|
- name: Upgrade pip and enable wheel support
|
||||||
|
run: python -m pip install pip==19.1.1 setuptools==43.0.0 wheel==0.33.6
|
||||||
- name: Install Requirements for 32 Bit
|
- name: Install Requirements for 32 Bit
|
||||||
run: pip install pyinstaller==3.5
|
run: pip install pyinstaller==3.5 mutagen==1.42.0 pycryptodome==3.9.4
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
run: python scripts/update-version-workflow.py
|
id: bump_version
|
||||||
|
run: python devscripts/update-version.py
|
||||||
|
- name: Print version
|
||||||
|
run: echo "${{ steps.bump_version.outputs.ytdlc_version }}"
|
||||||
- name: Run PyInstaller Script for 32 Bit
|
- name: Run PyInstaller Script for 32 Bit
|
||||||
run: python pyinst32.py
|
run: python pyinst.py 32
|
||||||
- name: Upload Executable youtube-dlc_x86.exe
|
- name: Upload Executable youtube-dlc_x86.exe
|
||||||
id: upload-release-windows32
|
id: upload-release-windows32
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
@@ -136,9 +152,7 @@ jobs:
|
|||||||
asset_content_type: application/vnd.microsoft.portable-executable
|
asset_content_type: application/vnd.microsoft.portable-executable
|
||||||
- name: Get SHA2-256SUMS for youtube-dlc_x86.exe
|
- name: Get SHA2-256SUMS for youtube-dlc_x86.exe
|
||||||
id: sha2_file_win32
|
id: sha2_file_win32
|
||||||
env:
|
run: echo "::set-output name=sha2_windows32::$((Get-FileHash dist\youtube-dlc_x86.exe -Algorithm SHA256).Hash.ToLower())"
|
||||||
SHA2_win32: ${{ hashFiles('dist/youtube-dlc_x86.exe') }}
|
|
||||||
run: echo "::set-output name=sha2_windows32::$SHA2_win32"
|
|
||||||
- name: Make SHA2-256SUMS file
|
- name: Make SHA2-256SUMS file
|
||||||
env:
|
env:
|
||||||
SHA2_WINDOWS: ${{ needs.build_windows.outputs.sha2_windows }}
|
SHA2_WINDOWS: ${{ needs.build_windows.outputs.sha2_windows }}
|
||||||
@@ -161,19 +175,3 @@ jobs:
|
|||||||
asset_path: ./SHA2-256SUMS
|
asset_path: ./SHA2-256SUMS
|
||||||
asset_name: SHA2-256SUMS
|
asset_name: SHA2-256SUMS
|
||||||
asset_content_type: text/plain
|
asset_content_type: text/plain
|
||||||
|
|
||||||
update_version_badge:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
needs: build_unix
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Create Version Badge
|
|
||||||
uses: schneegans/dynamic-badges-action@v1.0.0
|
|
||||||
with:
|
|
||||||
auth: ${{ secrets.GIST_TOKEN }}
|
|
||||||
gistID: c69cb23c3c5b3316248e52022790aa57
|
|
||||||
filename: version.json
|
|
||||||
label: Version
|
|
||||||
message: ${{ needs.build_unix.outputs.ytdlc_version }}
|
|
||||||
|
|||||||
2
.github/workflows/quick-test.yml
vendored
2
.github/workflows/quick-test.yml
vendored
@@ -2,7 +2,7 @@ name: Quick Test
|
|||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
name: Core Tests
|
name: Core Test
|
||||||
if: "!contains(github.event.head_commit.message, 'ci skip all')"
|
if: "!contains(github.event.head_commit.message, 'ci skip all')"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
99
.gitignore
vendored
99
.gitignore
vendored
@@ -1,35 +1,45 @@
|
|||||||
|
# Python
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
*.class
|
|
||||||
*~
|
|
||||||
*.DS_Store
|
|
||||||
wine-py2exe/
|
wine-py2exe/
|
||||||
py2exe.log
|
py2exe.log
|
||||||
*.kate-swp
|
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
zip/
|
zip/
|
||||||
|
tmp/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*~
|
||||||
|
*.DS_Store
|
||||||
|
*.kate-swp
|
||||||
MANIFEST
|
MANIFEST
|
||||||
README.txt
|
test/local_parameters.json
|
||||||
youtube-dl.1
|
|
||||||
youtube-dlc.1
|
|
||||||
youtube-dl.bash-completion
|
|
||||||
youtube-dlc.bash-completion
|
|
||||||
youtube-dl.fish
|
|
||||||
youtube-dlc.fish
|
|
||||||
youtube_dl/extractor/lazy_extractors.py
|
|
||||||
youtube_dlc/extractor/lazy_extractors.py
|
|
||||||
youtube-dl
|
|
||||||
youtube-dlc
|
|
||||||
youtube-dl.exe
|
|
||||||
youtube-dlc.exe
|
|
||||||
youtube-dl.tar.gz
|
|
||||||
youtube-dlc.tar.gz
|
|
||||||
youtube-dlc.spec
|
|
||||||
.coverage
|
.coverage
|
||||||
cover/
|
cover/
|
||||||
|
secrets/
|
||||||
updates_key.pem
|
updates_key.pem
|
||||||
*.egg-info
|
*.egg-info
|
||||||
|
.tox
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Generated
|
||||||
|
README.txt
|
||||||
|
*.1
|
||||||
|
*.bash-completion
|
||||||
|
*.fish
|
||||||
|
*.exe
|
||||||
|
*.tar.gz
|
||||||
|
*.zsh
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Binary
|
||||||
|
youtube-dl
|
||||||
|
youtube-dlc
|
||||||
|
youtube-dlc.zip
|
||||||
|
*.exe
|
||||||
|
|
||||||
|
# Downloaded
|
||||||
*.srt
|
*.srt
|
||||||
*.ttml
|
*.ttml
|
||||||
*.sbv
|
*.sbv
|
||||||
@@ -46,32 +56,39 @@ updates_key.pem
|
|||||||
*.swf
|
*.swf
|
||||||
*.part
|
*.part
|
||||||
*.ytdl
|
*.ytdl
|
||||||
*.conf
|
*.frag
|
||||||
|
*.frag.urls
|
||||||
|
*.aria2
|
||||||
*.swp
|
*.swp
|
||||||
|
*.ogg
|
||||||
|
*.opus
|
||||||
|
*.info.json
|
||||||
|
*.live_chat.json
|
||||||
|
*.jpg
|
||||||
|
*.png
|
||||||
|
*.webp
|
||||||
|
*.annotations.xml
|
||||||
|
*.description
|
||||||
|
|
||||||
|
# Config
|
||||||
|
*.conf
|
||||||
*.spec
|
*.spec
|
||||||
*.exe
|
|
||||||
test/local_parameters.json
|
|
||||||
.tox
|
|
||||||
youtube-dl.zsh
|
|
||||||
youtube-dlc.zsh
|
|
||||||
|
|
||||||
# IntelliJ related files
|
|
||||||
.idea
|
|
||||||
*.iml
|
|
||||||
|
|
||||||
tmp/
|
|
||||||
venv/
|
|
||||||
|
|
||||||
# VS Code related files
|
|
||||||
.vscode
|
|
||||||
|
|
||||||
# SublimeText files
|
|
||||||
*.sublime-workspace
|
|
||||||
|
|
||||||
# Cookies
|
|
||||||
cookies
|
cookies
|
||||||
cookies.txt
|
cookies.txt
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Text Editor / IDE
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
.vscode
|
||||||
|
*.sublime-workspace
|
||||||
|
*.sublime-project
|
||||||
|
!yt-dlp.sublime-project
|
||||||
|
|
||||||
|
# Lazy extractors
|
||||||
|
*/extractor/lazy_extractors.py
|
||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
ytdlp_plugins/extractor/*
|
ytdlp_plugins/extractor/*
|
||||||
!ytdlp_plugins/extractor/__init__.py
|
!ytdlp_plugins/extractor/__init__.py
|
||||||
|
|||||||
248
AUTHORS
248
AUTHORS
@@ -1,248 +0,0 @@
|
|||||||
Ricardo Garcia Gonzalez
|
|
||||||
Danny Colligan
|
|
||||||
Benjamin Johnson
|
|
||||||
Vasyl' Vavrychuk
|
|
||||||
Witold Baryluk
|
|
||||||
Paweł Paprota
|
|
||||||
Gergely Imreh
|
|
||||||
Rogério Brito
|
|
||||||
Philipp Hagemeister
|
|
||||||
Sören Schulze
|
|
||||||
Kevin Ngo
|
|
||||||
Ori Avtalion
|
|
||||||
shizeeg
|
|
||||||
Filippo Valsorda
|
|
||||||
Christian Albrecht
|
|
||||||
Dave Vasilevsky
|
|
||||||
Jaime Marquínez Ferrándiz
|
|
||||||
Jeff Crouse
|
|
||||||
Osama Khalid
|
|
||||||
Michael Walter
|
|
||||||
M. Yasoob Ullah Khalid
|
|
||||||
Julien Fraichard
|
|
||||||
Johny Mo Swag
|
|
||||||
Axel Noack
|
|
||||||
Albert Kim
|
|
||||||
Pierre Rudloff
|
|
||||||
Huarong Huo
|
|
||||||
Ismael Mejía
|
|
||||||
Steffan Donal
|
|
||||||
Andras Elso
|
|
||||||
Jelle van der Waa
|
|
||||||
Marcin Cieślak
|
|
||||||
Anton Larionov
|
|
||||||
Takuya Tsuchida
|
|
||||||
Sergey M.
|
|
||||||
Michael Orlitzky
|
|
||||||
Chris Gahan
|
|
||||||
Saimadhav Heblikar
|
|
||||||
Mike Col
|
|
||||||
Oleg Prutz
|
|
||||||
pulpe
|
|
||||||
Andreas Schmitz
|
|
||||||
Michael Kaiser
|
|
||||||
Niklas Laxström
|
|
||||||
David Triendl
|
|
||||||
Anthony Weems
|
|
||||||
David Wagner
|
|
||||||
Juan C. Olivares
|
|
||||||
Mattias Harrysson
|
|
||||||
phaer
|
|
||||||
Sainyam Kapoor
|
|
||||||
Nicolas Évrard
|
|
||||||
Jason Normore
|
|
||||||
Hoje Lee
|
|
||||||
Adam Thalhammer
|
|
||||||
Georg Jähnig
|
|
||||||
Ralf Haring
|
|
||||||
Koki Takahashi
|
|
||||||
Ariset Llerena
|
|
||||||
Adam Malcontenti-Wilson
|
|
||||||
Tobias Bell
|
|
||||||
Naglis Jonaitis
|
|
||||||
Charles Chen
|
|
||||||
Hassaan Ali
|
|
||||||
Dobrosław Żybort
|
|
||||||
David Fabijan
|
|
||||||
Sebastian Haas
|
|
||||||
Alexander Kirk
|
|
||||||
Erik Johnson
|
|
||||||
Keith Beckman
|
|
||||||
Ole Ernst
|
|
||||||
Aaron McDaniel (mcd1992)
|
|
||||||
Magnus Kolstad
|
|
||||||
Hari Padmanaban
|
|
||||||
Carlos Ramos
|
|
||||||
5moufl
|
|
||||||
lenaten
|
|
||||||
Dennis Scheiba
|
|
||||||
Damon Timm
|
|
||||||
winwon
|
|
||||||
Xavier Beynon
|
|
||||||
Gabriel Schubiner
|
|
||||||
xantares
|
|
||||||
Jan Matějka
|
|
||||||
Mauroy Sébastien
|
|
||||||
William Sewell
|
|
||||||
Dao Hoang Son
|
|
||||||
Oskar Jauch
|
|
||||||
Matthew Rayfield
|
|
||||||
t0mm0
|
|
||||||
Tithen-Firion
|
|
||||||
Zack Fernandes
|
|
||||||
cryptonaut
|
|
||||||
Adrian Kretz
|
|
||||||
Mathias Rav
|
|
||||||
Petr Kutalek
|
|
||||||
Will Glynn
|
|
||||||
Max Reimann
|
|
||||||
Cédric Luthi
|
|
||||||
Thijs Vermeir
|
|
||||||
Joel Leclerc
|
|
||||||
Christopher Krooss
|
|
||||||
Ondřej Caletka
|
|
||||||
Dinesh S
|
|
||||||
Johan K. Jensen
|
|
||||||
Yen Chi Hsuan
|
|
||||||
Enam Mijbah Noor
|
|
||||||
David Luhmer
|
|
||||||
Shaya Goldberg
|
|
||||||
Paul Hartmann
|
|
||||||
Frans de Jonge
|
|
||||||
Robin de Rooij
|
|
||||||
Ryan Schmidt
|
|
||||||
Leslie P. Polzer
|
|
||||||
Duncan Keall
|
|
||||||
Alexander Mamay
|
|
||||||
Devin J. Pohly
|
|
||||||
Eduardo Ferro Aldama
|
|
||||||
Jeff Buchbinder
|
|
||||||
Amish Bhadeshia
|
|
||||||
Joram Schrijver
|
|
||||||
Will W.
|
|
||||||
Mohammad Teimori Pabandi
|
|
||||||
Roman Le Négrate
|
|
||||||
Matthias Küch
|
|
||||||
Julian Richen
|
|
||||||
Ping O.
|
|
||||||
Mister Hat
|
|
||||||
Peter Ding
|
|
||||||
jackyzy823
|
|
||||||
George Brighton
|
|
||||||
Remita Amine
|
|
||||||
Aurélio A. Heckert
|
|
||||||
Bernhard Minks
|
|
||||||
sceext
|
|
||||||
Zach Bruggeman
|
|
||||||
Tjark Saul
|
|
||||||
slangangular
|
|
||||||
Behrouz Abbasi
|
|
||||||
ngld
|
|
||||||
nyuszika7h
|
|
||||||
Shaun Walbridge
|
|
||||||
Lee Jenkins
|
|
||||||
Anssi Hannula
|
|
||||||
Lukáš Lalinský
|
|
||||||
Qijiang Fan
|
|
||||||
Rémy Léone
|
|
||||||
Marco Ferragina
|
|
||||||
reiv
|
|
||||||
Muratcan Simsek
|
|
||||||
Evan Lu
|
|
||||||
flatgreen
|
|
||||||
Brian Foley
|
|
||||||
Vignesh Venkat
|
|
||||||
Tom Gijselinck
|
|
||||||
Founder Fang
|
|
||||||
Andrew Alexeyew
|
|
||||||
Saso Bezlaj
|
|
||||||
Erwin de Haan
|
|
||||||
Jens Wille
|
|
||||||
Robin Houtevelts
|
|
||||||
Patrick Griffis
|
|
||||||
Aidan Rowe
|
|
||||||
mutantmonkey
|
|
||||||
Ben Congdon
|
|
||||||
Kacper Michajłow
|
|
||||||
José Joaquín Atria
|
|
||||||
Viťas Strádal
|
|
||||||
Kagami Hiiragi
|
|
||||||
Philip Huppert
|
|
||||||
blahgeek
|
|
||||||
Kevin Deldycke
|
|
||||||
inondle
|
|
||||||
Tomáš Čech
|
|
||||||
Déstin Reed
|
|
||||||
Roman Tsiupa
|
|
||||||
Artur Krysiak
|
|
||||||
Jakub Adam Wieczorek
|
|
||||||
Aleksandar Topuzović
|
|
||||||
Nehal Patel
|
|
||||||
Rob van Bekkum
|
|
||||||
Petr Zvoníček
|
|
||||||
Pratyush Singh
|
|
||||||
Aleksander Nitecki
|
|
||||||
Sebastian Blunt
|
|
||||||
Matěj Cepl
|
|
||||||
Xie Yanbo
|
|
||||||
Philip Xu
|
|
||||||
John Hawkinson
|
|
||||||
Rich Leeper
|
|
||||||
Zhong Jianxin
|
|
||||||
Thor77
|
|
||||||
Mattias Wadman
|
|
||||||
Arjan Verwer
|
|
||||||
Costy Petrisor
|
|
||||||
Logan B
|
|
||||||
Alex Seiler
|
|
||||||
Vijay Singh
|
|
||||||
Paul Hartmann
|
|
||||||
Stephen Chen
|
|
||||||
Fabian Stahl
|
|
||||||
Bagira
|
|
||||||
Odd Stråbø
|
|
||||||
Philip Herzog
|
|
||||||
Thomas Christlieb
|
|
||||||
Marek Rusinowski
|
|
||||||
Tobias Gruetzmacher
|
|
||||||
Olivier Bilodeau
|
|
||||||
Lars Vierbergen
|
|
||||||
Juanjo Benages
|
|
||||||
Xiao Di Guan
|
|
||||||
Thomas Winant
|
|
||||||
Daniel Twardowski
|
|
||||||
Jeremie Jarosh
|
|
||||||
Gerard Rovira
|
|
||||||
Marvin Ewald
|
|
||||||
Frédéric Bournival
|
|
||||||
Timendum
|
|
||||||
gritstub
|
|
||||||
Adam Voss
|
|
||||||
Mike Fährmann
|
|
||||||
Jan Kundrát
|
|
||||||
Giuseppe Fabiano
|
|
||||||
Örn Guðjónsson
|
|
||||||
Parmjit Virk
|
|
||||||
Genki Sky
|
|
||||||
Ľuboš Katrinec
|
|
||||||
Corey Nicholson
|
|
||||||
Ashutosh Chaudhary
|
|
||||||
John Dong
|
|
||||||
Tatsuyuki Ishi
|
|
||||||
Daniel Weber
|
|
||||||
Kay Bouché
|
|
||||||
Yang Hongbo
|
|
||||||
Lei Wang
|
|
||||||
Petr Novák
|
|
||||||
Leonardo Taccari
|
|
||||||
Martin Weinelt
|
|
||||||
Surya Oktafendri
|
|
||||||
TingPing
|
|
||||||
Alexandre Macabies
|
|
||||||
Bastian de Groot
|
|
||||||
Niklas Haas
|
|
||||||
András Veres-Szentkirályi
|
|
||||||
Enes Solak
|
|
||||||
Nathan Rossi
|
|
||||||
Thomas van der Berg
|
|
||||||
Luca Cherubin
|
|
||||||
@@ -16,3 +16,8 @@ samiksome
|
|||||||
alxnull
|
alxnull
|
||||||
FelixFrog
|
FelixFrog
|
||||||
Zocker1999NET
|
Zocker1999NET
|
||||||
|
nao20010128nao
|
||||||
|
shirt-dev
|
||||||
|
kurumigi
|
||||||
|
tsukumi
|
||||||
|
bbepis
|
||||||
192
Changelog.md
192
Changelog.md
@@ -4,17 +4,161 @@
|
|||||||
# Instuctions for creating release
|
# Instuctions for creating release
|
||||||
|
|
||||||
* Run `make doc`
|
* Run `make doc`
|
||||||
* Update Changelog.md and Authors-Fork
|
* Update Changelog.md and CONTRIBUTORS
|
||||||
|
* Change "Merged with youtube-dl" version in Readme.md if needed
|
||||||
* Commit to master as `Release <version>`
|
* Commit to master as `Release <version>`
|
||||||
* Push to origin/release - build task will now run
|
* Push to origin/release - build task will now run
|
||||||
* Update version.py and run `make issuetemplates`
|
* Update version.py using devscripts\update-version.py
|
||||||
* Commit to master as `[version] update :skip ci all`
|
* Run `make issuetemplates`
|
||||||
|
* Commit to master as `[version] update :ci skip all`
|
||||||
* Push to origin/master
|
* Push to origin/master
|
||||||
* Update changelog in /releases
|
* Update changelog in /releases
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
### 2021.02.19
|
||||||
|
* **Merge youtube-dl:** Upto [commit/cf2dbec](https://github.com/ytdl-org/youtube-dl/commit/cf2dbec6301177a1fddf72862de05fa912d9869d) (except kakao)
|
||||||
|
* [viki] Fix extractor
|
||||||
|
* [niconico] Extract `channel` and `channel_id` by [kurumigi](https://github.com/kurumigi)
|
||||||
|
* [youtube] Multiple page support for hashtag URLs
|
||||||
|
* [youtube] Add more invidious instances
|
||||||
|
* [youtube] Fix comment extraction when comment text is empty
|
||||||
|
* Option `--windows-filenames` to force use of windows compatible filenames
|
||||||
|
* [ExtractAudio] Bugfix
|
||||||
|
* Don't raise `parser.error` when exiting for update
|
||||||
|
* [MoveFiles] Fix for when merger can't run
|
||||||
|
* Changed `--trim-file-name` to `--trim-filenames` to be similar to related options
|
||||||
|
* Format Sort improvements:
|
||||||
|
* Prefer `vp9.2` more than other `vp9` codecs
|
||||||
|
* Remove forced priority of `quality`
|
||||||
|
* Remove unnecessary `field_preference` and misuse of `preference` from extractors
|
||||||
|
* Build improvements:
|
||||||
|
* Fix hash output by [shirt](https://github.com/shirt-dev)
|
||||||
|
* Lock python package versions for x86 and use `wheels` by [shirt](https://github.com/shirt-dev)
|
||||||
|
* Exclude `vcruntime140.dll` from UPX by [jbruchon](https://github.com/jbruchon)
|
||||||
|
* Set version number based on UTC time, not local time
|
||||||
|
* Publish on PyPi only if token is set
|
||||||
|
* [documentation] Better document `--prefer-free-formats` and add `--no-prefer-free-format`
|
||||||
|
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
* [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)
|
||||||
|
* [youtube_live_chat] Fix by using POST API by [siikamiika](https://github.com/siikamiika)
|
||||||
|
* [rumble] Add support for video page
|
||||||
|
* Option `--allow-unplayable-formats` to allow downloading unplayable video formats
|
||||||
|
* [ExtractAudio] Don't re-encode when file is already in a common audio format
|
||||||
|
* [youtube] Fix search continuations
|
||||||
|
* [youtube] Fix for new accounts
|
||||||
|
* Improve build/updater: by [pukkandan](https://github.com/pukkandan) and [shirt](https://github.com/shirt-dev)
|
||||||
|
* Fix SHA256 calculation in build and implement hash checking for updater
|
||||||
|
* Exit immediately in windows once the update process starts
|
||||||
|
* Fix updater for `x86.exe`
|
||||||
|
* Updater looks for both `yt-dlp` and `youtube-dlc` in releases for future-proofing
|
||||||
|
* Change optional dependency to `pycryptodome`
|
||||||
|
* Fix issue with unicode filenames in aria2c by [shirt](https://github.com/shirt-dev)
|
||||||
|
* Fix `allow_playlist_files` not being correctly passed through
|
||||||
|
* Fix for empty HTTP head requests by [shirt](https://github.com/shirt-dev)
|
||||||
|
* Fix `get_executable_path` in UNIX
|
||||||
|
* [sponskrub] Print ffmpeg output and errors to terminal
|
||||||
|
* `__real_download` should be false when ffmpeg unavailable and no download
|
||||||
|
* Show `exe`/`zip`/`source` and 32/64bit in verbose message
|
||||||
|
|
||||||
|
|
||||||
|
### 2021.02.09
|
||||||
|
* **aria2c support for DASH/HLS**: by [shirt](https://github.com/shirt-dev)
|
||||||
|
* **Implement Updater** (`-U`) by [shirt](https://github.com/shirt-dev)
|
||||||
|
* [youtube] Fix comment extraction
|
||||||
|
* [youtube_live_chat] Improve extraction
|
||||||
|
* [youtube] Fix for channel URLs sometimes not downloading all pages
|
||||||
|
* [aria2c] Changed default arguments to `--console-log-level=warn --summary-interval=0 --file-allocation=none -x16 -j16 -s16`
|
||||||
|
* Add fallback for thumbnails
|
||||||
|
* [embedthumbnail] Keep original thumbnail after conversion if write_thumbnail given
|
||||||
|
* [embedsubtitle] Keep original subtitle after conversion if write_subtitles given
|
||||||
|
* [pyinst.py] Move back to root dir
|
||||||
|
* [youtube] Simplified renderer parsing and bugfixes
|
||||||
|
* [movefiles] Fix compatibility with python2
|
||||||
|
* [remuxvideo] Fix validation of conditional remux
|
||||||
|
* [sponskrub] Don't raise error when the video does not exist
|
||||||
|
* [documentation] Crypto is an optional dependency
|
||||||
|
|
||||||
|
|
||||||
|
### 2021.02.04
|
||||||
|
* **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:**
|
||||||
|
* 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:**
|
||||||
|
* Separate output templates can be given for the different metadata files by using `-o TYPE:TEMPLATE`
|
||||||
|
* The allowed types are: `subtitle|thumbnail|description|annotation|infojson|pl_description|pl_infojson`
|
||||||
|
* [youtube] More metadata extraction for channel/playlist URLs (channel, uploader, thumbnail, tags)
|
||||||
|
* New option `--no-write-playlist-metafiles` to prevent writing playlist metadata files
|
||||||
|
* [audius] Fix extractor
|
||||||
|
* [youtube_live_chat] Fix `parse_yt_initial_data` and add `fragment_retries`
|
||||||
|
* [postprocessor] Raise errors correctly
|
||||||
|
* [metadatafromtitle] Fix bug when extracting data from numeric fields
|
||||||
|
* Fix issue with overwriting files
|
||||||
|
* Fix "Default format spec" appearing in quiet mode
|
||||||
|
* [FormatSort] Allow user to prefer av01 over vp9 (The default is still vp9)
|
||||||
|
* [FormatSort] fix bug where `quality` had more priority than `hasvid`
|
||||||
|
* [pyinst] Automatically detect python architecture and working directory
|
||||||
|
* Strip out internal fields such as `_filename` from infojson
|
||||||
|
|
||||||
|
|
||||||
|
### 2021.01.29
|
||||||
|
* **Features from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl)**: by [animelover1984](https://github.com/animelover1984) and [bbepis](https://github.com/bbepis)
|
||||||
|
* Add `--get-comments`
|
||||||
|
* [youtube] Extract comments
|
||||||
|
* [billibilli] Added BiliBiliSearchIE, BilibiliChannelIE
|
||||||
|
* [billibilli] Extract comments
|
||||||
|
* [billibilli] Better video extraction
|
||||||
|
* Write playlist data to infojson
|
||||||
|
* [FFmpegMetadata] Embed infojson inside the video
|
||||||
|
* [EmbedThumbnail] Try embedding in mp4 using ffprobe and `-disposition`
|
||||||
|
* [EmbedThumbnail] Treat mka like mkv and mov like mp4
|
||||||
|
* [EmbedThumbnail] Embed in ogg/opus
|
||||||
|
* [VideoRemuxer] Conditionally remux video
|
||||||
|
* [VideoRemuxer] Add `-movflags +faststart` when remuxing to mp4
|
||||||
|
* [ffmpeg] Print entire stderr in verbose when there is error
|
||||||
|
* [EmbedSubtitle] Warn when embedding ass in mp4
|
||||||
|
* [anvato] Use NFLTokenGenerator if possible
|
||||||
|
* **Parse additional metadata**: New option `--parse-metadata` to extract additional metadata from existing fields
|
||||||
|
* The extracted fields can be used in `--output`
|
||||||
|
* Deprecated `--metadata-from-title`
|
||||||
|
* [Audius] Add extractor
|
||||||
|
* [youtube] Extract playlist description and write it to `.description` file
|
||||||
|
* Detect existing files even when using `recode`/`remux` (`extract-audio` is partially fixed)
|
||||||
|
* Fix wrong user config from v2021.01.24
|
||||||
|
* [youtube] Report error message from youtube as error instead of warning
|
||||||
|
* [FormatSort] Fix some fields not sorting from v2021.01.24
|
||||||
|
* [postprocessor] Deprecate `avconv`/`avprobe`. All current functionality is left untouched. But don't expect any new features to work with avconv
|
||||||
|
* [postprocessor] fix `write_debug` to not throw error when there is no `_downloader`
|
||||||
|
* [movefiles] Don't give "cant find" warning when move is unnecessary
|
||||||
|
* Refactor `update-version`, `pyinst.py` and related files
|
||||||
|
* [ffmpeg] Document more formats that are supported for remux/recode
|
||||||
|
|
||||||
|
|
||||||
|
### 2021.01.24
|
||||||
|
* **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/pukkandan/yt-dlp#plugins))
|
||||||
|
* **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/pukkandan/yt-dlp#:~:text=-P,%20--paths%20TYPE:PATH))
|
||||||
|
* Valid types are: home, temp, description, annotation, subtitle, infojson, thumbnail
|
||||||
|
* Additionally, configuration file is taken from home directory or current directory ([documentation](https://github.com/pukkandan/yt-dlp#:~:text=Home%20Configuration))
|
||||||
|
* Allow passing different arguments to different external downloaders ([documentation](https://github.com/pukkandan/yt-dlp#:~:text=--downloader-args%20NAME:ARGS))
|
||||||
|
* [mildom] Add extractor by [nao20010128nao](https://github.com/nao20010128nao)
|
||||||
|
* Warn when using old style `--external-downloader-args` and `--post-processor-args`
|
||||||
|
* Fix `--no-overwrite` when using `--write-link`
|
||||||
|
* [sponskrub] Output `unrecognized argument` error message correctly
|
||||||
|
* [cbs] Make failure to extract title non-fatal
|
||||||
|
* Fix typecasting when pre-checking archive
|
||||||
|
* Fix issue with setting title on UNIX
|
||||||
|
* Deprecate redundant aliases in `formatSort`. The aliases remain functional for backward compatibility, but will be left undocumented
|
||||||
|
* [tests] Fix test_post_hooks
|
||||||
|
* [tests] Split core and download tests
|
||||||
|
|
||||||
|
|
||||||
### 2021.01.20
|
### 2021.01.20
|
||||||
* [TrovoLive] Add extractor (only VODs)
|
* [TrovoLive] Add extractor (only VODs)
|
||||||
* [pokemon] Add `/#/player` URLs
|
* [pokemon] Add `/#/player` URLs
|
||||||
@@ -34,7 +178,7 @@
|
|||||||
|
|
||||||
### 2021.01.14
|
### 2021.01.14
|
||||||
* Added option `--break-on-reject`
|
* Added option `--break-on-reject`
|
||||||
* [roosterteeth.com] Fix for bonus episodes by @Zocker1999NET
|
* [roosterteeth.com] Fix for bonus episodes by [Zocker1999NET](https://github.com/Zocker1999NET)
|
||||||
* [tiktok] Fix for when share_info is empty
|
* [tiktok] Fix for when share_info is empty
|
||||||
* [EmbedThumbnail] Fix bug due to incorrect function name
|
* [EmbedThumbnail] Fix bug due to incorrect function name
|
||||||
* [documentation] Changed sponskrub links to point to [pukkandan/sponskrub](https://github.com/pukkandan/SponSkrub) since I am now providing both linux and windows releases
|
* [documentation] Changed sponskrub links to point to [pukkandan/sponskrub](https://github.com/pukkandan/SponSkrub) since I am now providing both linux and windows releases
|
||||||
@@ -43,18 +187,18 @@
|
|||||||
|
|
||||||
|
|
||||||
### 2021.01.12
|
### 2021.01.12
|
||||||
* [roosterteeth.com] Add subtitle support by @samiksome
|
* [roosterteeth.com] Add subtitle support by [samiksome](https://github.com/samiksome)
|
||||||
* Added `--force-overwrites`, `--no-force-overwrites` by @alxnull
|
* Added `--force-overwrites`, `--no-force-overwrites` by [alxnull](https://github.com/alxnull)
|
||||||
* Changed fork name to `yt-dlp`
|
* Changed fork name to `yt-dlp`
|
||||||
* Fix typos by @FelixFrog
|
* Fix typos by [FelixFrog](https://github.com/FelixFrog)
|
||||||
* [ci] Option to skip
|
* [ci] Option to skip
|
||||||
* [changelog] Added unreleased changes in blackjack4494/yt-dlc
|
* [changelog] Added unreleased changes in blackjack4494/yt-dlc
|
||||||
|
|
||||||
|
|
||||||
### 2021.01.10
|
### 2021.01.10
|
||||||
* [archive.org] Fix extractor and add support for audio and playlists by @wporr
|
* [archive.org] Fix extractor and add support for audio and playlists by [wporr](https://github.com/wporr)
|
||||||
* [Animelab] Added by @mariuszskon
|
* [Animelab] Added by [mariuszskon](https://github.com/mariuszskon)
|
||||||
* [youtube:search] Fix view_count by @ohnonot
|
* [youtube:search] Fix view_count by [ohnonot](https://github.com/ohnonot)
|
||||||
* [youtube] Show if video is embeddable in info
|
* [youtube] Show if video is embeddable in info
|
||||||
* Update version badge automatically in README
|
* Update version badge automatically in README
|
||||||
* Enable `test_youtube_search_matching`
|
* Enable `test_youtube_search_matching`
|
||||||
@@ -63,11 +207,11 @@
|
|||||||
|
|
||||||
### 2021.01.09
|
### 2021.01.09
|
||||||
* [youtube] Fix bug in automatic caption extraction
|
* [youtube] Fix bug in automatic caption extraction
|
||||||
* Add `post_hooks` to YoutubeDL by @alexmerkel
|
* Add `post_hooks` to YoutubeDL by [alexmerkel](https://github.com/alexmerkel)
|
||||||
* Batch file enumeration improvements by @glenn-slayden
|
* Batch file enumeration improvements by [glenn-slayden](https://github.com/glenn-slayden)
|
||||||
* Stop immediately when reaching `--max-downloads` by @glenn-slayden
|
* Stop immediately when reaching `--max-downloads` by [glenn-slayden](https://github.com/glenn-slayden)
|
||||||
* Fix incorrect ANSI sequence for restoring console-window title by @glenn-slayden
|
* Fix incorrect ANSI sequence for restoring console-window title by [glenn-slayden](https://github.com/glenn-slayden)
|
||||||
* Kill child processes when yt-dlc is killed by @Unrud
|
* Kill child processes when yt-dlc is killed by [Unrud](https://github.com/Unrud)
|
||||||
|
|
||||||
|
|
||||||
### 2021.01.08
|
### 2021.01.08
|
||||||
@@ -77,11 +221,11 @@
|
|||||||
|
|
||||||
|
|
||||||
### 2021.01.07-1
|
### 2021.01.07-1
|
||||||
* [Akamai] fix by @nixxo
|
* [Akamai] fix by [nixxo](https://github.com/nixxo)
|
||||||
* [Tiktok] merge youtube-dl tiktok extractor by @GreyAlien502
|
* [Tiktok] merge youtube-dl tiktok extractor by [GreyAlien502](https://github.com/GreyAlien502)
|
||||||
* [vlive] add support for playlists by @kyuyeunk
|
* [vlive] add support for playlists by [kyuyeunk](https://github.com/kyuyeunk)
|
||||||
* [youtube_live_chat] make sure playerOffsetMs is positive by @siikamiika
|
* [youtube_live_chat] make sure playerOffsetMs is positive by [siikamiika](https://github.com/siikamiika)
|
||||||
* Ignore extra data streams in ffmpeg by @jbruchon
|
* Ignore extra data streams in ffmpeg by [jbruchon](https://github.com/jbruchon)
|
||||||
* Allow passing different arguments to different postprocessors using `--postprocessor-args`
|
* Allow passing different arguments to different postprocessors using `--postprocessor-args`
|
||||||
* Deprecated `--sponskrub-args`. The same can now be done using `--postprocessor-args "sponskrub:<args>"`
|
* Deprecated `--sponskrub-args`. The same can now be done using `--postprocessor-args "sponskrub:<args>"`
|
||||||
* [CI] Split tests into core-test and full-test
|
* [CI] Split tests into core-test and full-test
|
||||||
@@ -111,15 +255,15 @@
|
|||||||
* 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 - 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-options-sponsorblock) for details
|
||||||
* Added `--force-download-archive` (`--force-write-archive`) by @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
|
||||||
* Added `--no-ignore-dynamic-mpd`, `--no-allow-dynamic-mpd`, `--allow-dynamic-mpd`, `--youtube-include-hls-manifest`, `--no-youtube-include-hls-manifest`, `--no-youtube-skip-hls-manifest`, `--no-download`, `--no-download-archive`, `--resize-buffer`, `--part`, `--mtime`, `--no-keep-fragments`, `--no-cookies`, `--no-write-annotations`, `--no-write-info-json`, `--no-write-description`, `--no-write-thumbnail`, `--youtube-include-dash-manifest`, `--post-overwrites`, `--no-keep-video`, `--no-embed-subs`, `--no-embed-thumbnail`, `--no-add-metadata`, `--no-include-ads`, `--no-write-sub`, `--no-write-auto-sub`, `--no-playlist-reverse`, `--no-restrict-filenames`, `--youtube-include-dash-manifest`, `--no-format-sort-force`, `--flat-videos`, `--no-list-formats-as-table`, `--no-sponskrub`, `--no-sponskrub-cut`, `--no-sponskrub-force`
|
* Added `--no-ignore-dynamic-mpd`, `--no-allow-dynamic-mpd`, `--allow-dynamic-mpd`, `--youtube-include-hls-manifest`, `--no-youtube-include-hls-manifest`, `--no-youtube-skip-hls-manifest`, `--no-download`, `--no-download-archive`, `--resize-buffer`, `--part`, `--mtime`, `--no-keep-fragments`, `--no-cookies`, `--no-write-annotations`, `--no-write-info-json`, `--no-write-description`, `--no-write-thumbnail`, `--youtube-include-dash-manifest`, `--post-overwrites`, `--no-keep-video`, `--no-embed-subs`, `--no-embed-thumbnail`, `--no-add-metadata`, `--no-include-ads`, `--no-write-sub`, `--no-write-auto-sub`, `--no-playlist-reverse`, `--no-restrict-filenames`, `--youtube-include-dash-manifest`, `--no-format-sort-force`, `--flat-videos`, `--no-list-formats-as-table`, `--no-sponskrub`, `--no-sponskrub-cut`, `--no-sponskrub-force`
|
||||||
* Renamed: `--write-subs`, `--no-write-subs`, `--no-write-auto-subs`, `--write-auto-subs`. Note that these can still be used without the ending "s"
|
* Renamed: `--write-subs`, `--no-write-subs`, `--no-write-auto-subs`, `--write-auto-subs`. Note that these can still be used without the ending "s"
|
||||||
* 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
|
* 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
|
||||||
@@ -136,7 +280,7 @@
|
|||||||
* 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
|
* Multiple pages are handled better for feeds
|
||||||
* Add --break-on-existing by @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
|
||||||
* [bitwave.tv] New extractor
|
* [bitwave.tv] New extractor
|
||||||
* [Gedi] Add extractor
|
* [Gedi] Add extractor
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -4,7 +4,7 @@ man: README.txt youtube-dlc.1 youtube-dlc.bash-completion youtube-dlc.zsh youtub
|
|||||||
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf youtube-dlc.1.temp.md youtube-dlc.1 youtube-dlc.bash-completion README.txt MANIFEST build/ dist/ .coverage cover/ youtube-dlc.tar.gz youtube-dlc.zsh youtube-dlc.fish youtube_dlc/extractor/lazy_extractors.py *.dump *.part* *.ytdl *.info.json *.mp4 *.m4a *.flv *.mp3 *.avi *.mkv *.webm *.3gp *.wav *.ape *.swf *.jpg *.png *.spec CONTRIBUTING.md.tmp youtube-dlc youtube-dlc.exe
|
rm -rf youtube-dlc.1.temp.md youtube-dlc.1 youtube-dlc.bash-completion README.txt MANIFEST build/ dist/ .coverage cover/ youtube-dlc.tar.gz youtube-dlc.zsh youtube-dlc.fish youtube_dlc/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 youtube-dlc youtube-dlc.exe
|
||||||
find . -name "*.pyc" -delete
|
find . -name "*.pyc" -delete
|
||||||
find . -name "*.class" -delete
|
find . -name "*.class" -delete
|
||||||
|
|
||||||
|
|||||||
257
README.md
257
README.md
@@ -1,9 +1,14 @@
|
|||||||
# YT-DLP
|
# YT-DLP
|
||||||
|
|
||||||
<!-- See: https://github.com/marketplace/actions/dynamic-badges -->
|
[](https://github.com/pukkandan/yt-dlp/releases/latest)
|
||||||
[](https://github.com/pukkandan/yt-dlp/releases/latest)
|
[](LICENSE)
|
||||||
[](https://github.com/pukkandan/yt-dlp/blob/master/LICENSE)
|
|
||||||
[](https://github.com/pukkandan/yt-dlp/actions)
|
[](https://github.com/pukkandan/yt-dlp/actions)
|
||||||
|
[](https://discord.gg/S75JaBna)
|
||||||
|
|
||||||
|
[](https://github.com/pukkandan/yt-dlp/commits)
|
||||||
|
[](https://github.com/pukkandan/yt-dlp/commits)
|
||||||
|
[](https://github.com/pukkandan/yt-dlp/releases/latest)
|
||||||
|
[](https://pypi.org/project/yt-dlp)
|
||||||
|
|
||||||
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](docs/supportedsites.md)
|
||||||
|
|
||||||
@@ -51,20 +56,35 @@ 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.01.16**: 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.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 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/pukkandan/yt-dlp/pull/31) for details.
|
||||||
|
|
||||||
* **Youtube improvements**:
|
* **Youtube improvements**:
|
||||||
* All Youtube Feeds (`:ytfav`, `:ytwatchlater`, `:ytsubs`, `:ythistory`, `:ytrec`) works correctly and support downloading multiple pages of content
|
* All Youtube Feeds (`:ytfav`, `:ytwatchlater`, `:ytsubs`, `:ythistory`, `:ytrec`) works correctly and supports downloading multiple pages of content
|
||||||
* Youtube search works correctly (`ytsearch:`, `ytsearchdate:`) along with Search URLs
|
* 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
|
||||||
|
|
||||||
* **New extractors**: Trovo.live, AnimeLab, Philo MSO, Rcs, Gedi, bitwave.tv
|
* **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
|
||||||
|
|
||||||
* **Fixed extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, tiktok, akamai, ina
|
* **New extractors**: AnimeLab, Philo MSO, Rcs, Gedi, bitwave.tv, mildom, audius
|
||||||
|
|
||||||
* **New options**: `--list-formats-as-table`, `--write-link`, `--force-download-archive`, `--force-overwrites`, `--break-on-reject` etc
|
* **Fixed extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, tiktok, akamai, ina, rumble
|
||||||
|
|
||||||
|
* **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/pukkandan/yt-dlp/#:~:text=-P,%20--paths%20TYPE:PATH) for details
|
||||||
|
|
||||||
|
<!-- Relative link doesn't work for "#:~:text=" -->
|
||||||
|
|
||||||
|
* **Portable Configuration**: Configuration files are automatically loaded from the home and root directories. See [configuration](#configuration) for details
|
||||||
|
|
||||||
|
* **Other new options**: `--parse-metadata`, `--list-formats-as-table`, `--write-link`, `--force-download-archive`, `--force-overwrites`, `--break-on-reject` etc
|
||||||
|
|
||||||
|
* **Improvements**: Multiple `--postprocessor-args` and `--external-downloader-args`, Date/time formatting in `-o`, faster archive checking, more [format selection options](#format-selection) etc
|
||||||
|
|
||||||
|
* **Self-updater**: The releases can be updated using `youtube-dlc -U`
|
||||||
|
|
||||||
* **Improvements**: Multiple `--postprocessor-args`, `%(duration_string)s` in `-o`, faster archive checking, more [format selection options](#format-selection) etc
|
|
||||||
|
|
||||||
See [changelog](Changelog.md) or [commits](https://github.com/pukkandan/yt-dlp/commits) for the full list of changes
|
See [changelog](Changelog.md) or [commits](https://github.com/pukkandan/yt-dlp/commits) for the full list of changes
|
||||||
|
|
||||||
@@ -77,28 +97,28 @@ If you are coming from [youtube-dl](https://github.com/ytdl-org/youtube-dl), the
|
|||||||
# INSTALLATION
|
# INSTALLATION
|
||||||
|
|
||||||
You can install yt-dlp using one of the following methods:
|
You can install yt-dlp using one of the following methods:
|
||||||
* Use [PyPI package](https://pypi.org/project/yt-dlp/): `python -m pip install --upgrade yt-dlp`
|
* Download the binary from the [latest release](https://github.com/pukkandan/yt-dlp/releases/latest) (recommended method)
|
||||||
* Download the binary from the [latest release](https://github.com/pukkandan/yt-dlp/releases/latest)
|
* Use [PyPI package](https://pypi.org/project/yt-dlp): `python -m pip install --upgrade yt-dlp`
|
||||||
* Use pip+git: `python -m pip install --upgrade git+https://github.com/pukkandan/yt-dlp.git@release`
|
* Use pip+git: `python -m pip install --upgrade git+https://github.com/pukkandan/yt-dlp.git@release`
|
||||||
* Install master branch: `python -m pip install --upgrade git+https://github.com/pukkandan/yt-dlp`
|
* Install master branch: `python -m pip install --upgrade git+https://github.com/pukkandan/yt-dlp`
|
||||||
|
|
||||||
### UPDATE
|
### UPDATE
|
||||||
`-U` does not work. Simply repeat the install process to update.
|
Starting from version `2021.02.09`, you can use `youtube-dlc -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.
|
||||||
|
|
||||||
### COMPILE
|
### COMPILE
|
||||||
|
|
||||||
**For Windows**:
|
**For Windows**:
|
||||||
To build the Windows executable yourself (without version info!)
|
To build the Windows executable, you must have pyinstaller (and optionally mutagen and pycryptodome)
|
||||||
|
|
||||||
|
python -m pip install --upgrade pyinstaller mutagen pycryptodome
|
||||||
|
|
||||||
|
Once you have all the necessary dependancies installed, just run `py pyinst.py`. The executable will be built for the same architecture (32/64 bit) as the python used to build it. It is strongly reccomended to use python3 although python2.6+ is supported.
|
||||||
|
|
||||||
|
You can also build the executable without any version info or metadata by using:
|
||||||
|
|
||||||
python -m pip install --upgrade pyinstaller
|
|
||||||
pyinstaller.exe youtube_dlc\__main__.py --onefile --name youtube-dlc
|
pyinstaller.exe youtube_dlc\__main__.py --onefile --name youtube-dlc
|
||||||
|
|
||||||
Or simply execute the `make_win.bat` if pyinstaller is installed.
|
|
||||||
There will be a `youtube-dlc.exe` in `/dist`
|
|
||||||
|
|
||||||
New way to build Windows is to use `python pyinst.py` (please use python3 64Bit)
|
|
||||||
For 32Bit Version use a 32Bit Version of python (3 preferred here as well) and run `python pyinst32.py`
|
|
||||||
|
|
||||||
**For Unix**:
|
**For Unix**:
|
||||||
You will need the required build tools
|
You will need the required build tools
|
||||||
python, make (GNU), pandoc, zip, nosetests
|
python, make (GNU), pandoc, zip, nosetests
|
||||||
@@ -106,6 +126,7 @@ Then simply type this
|
|||||||
|
|
||||||
make
|
make
|
||||||
|
|
||||||
|
**Note**: In either platform, `devscripts\update-version.py` can be used to automatically update the version number
|
||||||
|
|
||||||
# DESCRIPTION
|
# DESCRIPTION
|
||||||
**youtube-dlc** 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.
|
**youtube-dlc** 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.
|
||||||
@@ -120,9 +141,9 @@ Then simply type this
|
|||||||
## General Options:
|
## General Options:
|
||||||
-h, --help Print this help text and exit
|
-h, --help Print this help text and exit
|
||||||
--version Print program version and exit
|
--version Print program version and exit
|
||||||
-U, --update [BROKEN] Update this program to latest
|
-U, --update Update this program to latest version. Make
|
||||||
version. Make sure that you have sufficient
|
sure that you have sufficient permissions
|
||||||
permissions (run with sudo if needed)
|
(run with sudo if needed)
|
||||||
-i, --ignore-errors Continue on download errors, for example to
|
-i, --ignore-errors Continue on download errors, for example to
|
||||||
skip unavailable videos in a playlist
|
skip unavailable videos in a playlist
|
||||||
(default) (Alias: --no-abort-on-error)
|
(default) (Alias: --no-abort-on-error)
|
||||||
@@ -156,7 +177,6 @@ Then simply type this
|
|||||||
containing directory
|
containing directory
|
||||||
--flat-playlist Do not extract the videos of a playlist,
|
--flat-playlist Do not extract the videos of a playlist,
|
||||||
only list them
|
only list them
|
||||||
--flat-videos Do not resolve the video urls
|
|
||||||
--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
|
||||||
@@ -309,8 +329,8 @@ Then simply type this
|
|||||||
--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
|
||||||
can use this option multiple times (Alias:
|
can use this option multiple times
|
||||||
--external-downloader-args)
|
(Alias: --external-downloader-args)
|
||||||
|
|
||||||
## Filesystem Options:
|
## Filesystem Options:
|
||||||
-a, --batch-file FILE File containing URLs to download ('-' for
|
-a, --batch-file FILE File containing URLs to download ('-' for
|
||||||
@@ -319,17 +339,20 @@ Then simply type this
|
|||||||
comments and ignored
|
comments and ignored
|
||||||
-P, --paths TYPE:PATH The paths where the files should be
|
-P, --paths TYPE:PATH The paths where the files should be
|
||||||
downloaded. Specify the type of file and
|
downloaded. Specify the type of file and
|
||||||
the path separated by a colon ":"
|
the path separated by a colon ":". All the
|
||||||
(supported: description|annotation|subtitle
|
same types as --output are supported.
|
||||||
|infojson|thumbnail). Additionally, you can
|
Additionally, you can also provide "home"
|
||||||
also provide "home" and "temp" paths. All
|
and "temp" paths. All intermediary files
|
||||||
intermediary files are first downloaded to
|
are first downloaded to the temp path and
|
||||||
the temp path and then the final files are
|
then the final files are moved over to the
|
||||||
moved over to the home path after download
|
home path after download is finished. This
|
||||||
is finished. Note that this option is
|
option is ignored if --output is an
|
||||||
ignored if --output is an absolute path
|
absolute path
|
||||||
-o, --output TEMPLATE Output filename template, see "OUTPUT
|
-o, --output [TYPE:]TEMPLATE Output filename template, see "OUTPUT
|
||||||
TEMPLATE" for details
|
TEMPLATE" for details
|
||||||
|
--output-na-placeholder TEXT Placeholder value for unavailable meta
|
||||||
|
fields in output filename template
|
||||||
|
(default: "NA")
|
||||||
--autonumber-start NUMBER Specify the start value for %(autonumber)s
|
--autonumber-start NUMBER Specify the start value for %(autonumber)s
|
||||||
(default is 1)
|
(default is 1)
|
||||||
--restrict-filenames Restrict filenames to only ASCII
|
--restrict-filenames Restrict filenames to only ASCII
|
||||||
@@ -337,14 +360,22 @@ Then simply type this
|
|||||||
filenames
|
filenames
|
||||||
--no-restrict-filenames Allow Unicode characters, "&" and spaces in
|
--no-restrict-filenames Allow Unicode characters, "&" and spaces in
|
||||||
filenames (default)
|
filenames (default)
|
||||||
|
--windows-filenames Force filenames to be windows compatible
|
||||||
|
--no-windows-filenames Make filenames windows compatible only if
|
||||||
|
using windows (default)
|
||||||
|
--trim-filenames LENGTH Limit the filename length (excluding
|
||||||
|
extension) to the specified number of
|
||||||
|
characters
|
||||||
-w, --no-overwrites Do not overwrite any files
|
-w, --no-overwrites Do not overwrite any files
|
||||||
--force-overwrites Overwrite all video and metadata files.
|
--force-overwrites Overwrite all video and metadata files.
|
||||||
This option includes --no-continue
|
This option includes --no-continue
|
||||||
--no-force-overwrites Do not overwrite the video, but overwrite
|
--no-force-overwrites Do not overwrite the video, but overwrite
|
||||||
related files (default)
|
related files (default)
|
||||||
-c, --continue Resume partially downloaded files (default)
|
-c, --continue Resume partially downloaded files/fragments
|
||||||
--no-continue Restart download of partially downloaded
|
(default)
|
||||||
files from beginning
|
--no-continue Do not resume partially downloaded
|
||||||
|
fragments. If the file is unfragmented,
|
||||||
|
restart download of the entire file
|
||||||
--part Use .part files instead of writing directly
|
--part Use .part files instead of writing directly
|
||||||
into output file (default)
|
into output file (default)
|
||||||
--no-part Do not use .part files - write directly
|
--no-part Do not use .part files - write directly
|
||||||
@@ -357,10 +388,18 @@ Then simply type this
|
|||||||
file
|
file
|
||||||
--no-write-description Do not write video description (default)
|
--no-write-description Do not write video description (default)
|
||||||
--write-info-json Write video metadata to a .info.json file
|
--write-info-json Write video metadata to a .info.json file
|
||||||
|
(this may contain personal information)
|
||||||
--no-write-info-json Do not write video metadata (default)
|
--no-write-info-json Do not write video metadata (default)
|
||||||
--write-annotations Write video annotations to a
|
--write-annotations Write video annotations to a
|
||||||
.annotations.xml file
|
.annotations.xml file
|
||||||
--no-write-annotations Do not write video annotations (default)
|
--no-write-annotations Do not write video annotations (default)
|
||||||
|
--write-playlist-metafiles Write playlist metadata in addition to the
|
||||||
|
video metadata when using --write-info-json,
|
||||||
|
--write-description etc. (default)
|
||||||
|
--no-write-playlist-metafiles Do not write playlist metadata when using
|
||||||
|
--write-info-json, --write-description etc.
|
||||||
|
--get-comments Retrieve video comments to be placed in the
|
||||||
|
.info.json file
|
||||||
--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)
|
||||||
@@ -377,8 +416,6 @@ Then simply type this
|
|||||||
may change
|
may change
|
||||||
--no-cache-dir Disable filesystem caching
|
--no-cache-dir Disable filesystem caching
|
||||||
--rm-cache-dir Delete all filesystem cache files
|
--rm-cache-dir Delete all filesystem cache files
|
||||||
--trim-file-name LENGTH Limit the filename length (extension
|
|
||||||
excluded)
|
|
||||||
|
|
||||||
## Thumbnail Images:
|
## Thumbnail Images:
|
||||||
--write-thumbnail Write thumbnail image to disk
|
--write-thumbnail Write thumbnail image to disk
|
||||||
@@ -434,10 +471,6 @@ Then simply type this
|
|||||||
files in the current directory to debug
|
files in the current directory to debug
|
||||||
problems
|
problems
|
||||||
--print-traffic Display sent and read HTTP traffic
|
--print-traffic Display sent and read HTTP traffic
|
||||||
-C, --call-home [Broken] Contact the youtube-dlc server for
|
|
||||||
debugging
|
|
||||||
--no-call-home Do not contact the youtube-dlc server for
|
|
||||||
debugging (default)
|
|
||||||
|
|
||||||
## Workarounds:
|
## Workarounds:
|
||||||
--encoding ENCODING Force the specified encoding (experimental)
|
--encoding ENCODING Force the specified encoding (experimental)
|
||||||
@@ -486,8 +519,12 @@ Then simply type this
|
|||||||
--no-audio-multistreams Only one audio stream is downloaded for
|
--no-audio-multistreams Only one audio stream is downloaded for
|
||||||
each output file (default)
|
each output file (default)
|
||||||
--all-formats Download all available video formats
|
--all-formats Download all available video formats
|
||||||
--prefer-free-formats Prefer free video formats over non-free
|
--prefer-free-formats Prefer video formats with free containers
|
||||||
formats of same quality
|
over non-free ones of same quality. Use
|
||||||
|
with "-S ext" to strictly prefer free
|
||||||
|
containers irrespective of quality
|
||||||
|
--no-prefer-free-formats Don't give any special preference to free
|
||||||
|
containers (default)
|
||||||
-F, --list-formats List all available formats of requested
|
-F, --list-formats List all available formats of requested
|
||||||
videos
|
videos
|
||||||
--list-formats-as-table Present the output of -F in tabular form
|
--list-formats-as-table Present the output of -F in tabular form
|
||||||
@@ -495,21 +532,26 @@ Then simply type this
|
|||||||
--list-formats-old Present the output of -F in the old form
|
--list-formats-old Present the output of -F in the old form
|
||||||
(Alias: --no-list-formats-as-table)
|
(Alias: --no-list-formats-as-table)
|
||||||
--youtube-include-dash-manifest Download the DASH manifests and related
|
--youtube-include-dash-manifest Download the DASH manifests and related
|
||||||
data on YouTube videos (default) (Alias:
|
data on YouTube videos (default)
|
||||||
--no-youtube-skip-dash-manifest)
|
(Alias: --no-youtube-skip-dash-manifest)
|
||||||
--youtube-skip-dash-manifest Do not download the DASH manifests and
|
--youtube-skip-dash-manifest Do not download the DASH manifests and
|
||||||
related data on YouTube videos (Alias:
|
related data on YouTube videos
|
||||||
--no-youtube-include-dash-manifest)
|
(Alias: --no-youtube-include-dash-manifest)
|
||||||
--youtube-include-hls-manifest Download the HLS manifests and related data
|
--youtube-include-hls-manifest Download the HLS manifests and related data
|
||||||
on YouTube videos (default) (Alias:
|
on YouTube videos (default)
|
||||||
--no-youtube-skip-hls-manifest)
|
(Alias: --no-youtube-skip-hls-manifest)
|
||||||
--youtube-skip-hls-manifest Do not download the HLS manifests and
|
--youtube-skip-hls-manifest Do not download the HLS manifests and
|
||||||
related data on YouTube videos (Alias:
|
related data on YouTube videos
|
||||||
--no-youtube-include-hls-manifest)
|
(Alias: --no-youtube-include-hls-manifest)
|
||||||
--merge-output-format FORMAT If a merge is required (e.g.
|
--merge-output-format FORMAT If a merge is required (e.g.
|
||||||
bestvideo+bestaudio), output to given
|
bestvideo+bestaudio), output to given
|
||||||
container format. One of mkv, mp4, ogg,
|
container format. One of mkv, mp4, ogg,
|
||||||
webm, flv. Ignored if no merge is required
|
webm, flv. Ignored if no merge is required
|
||||||
|
--allow-unplayable-formats Allow unplayable formats to be listed and
|
||||||
|
downloaded. All video postprocessing will
|
||||||
|
also be turned off
|
||||||
|
--no-allow-unplayable-formats Do not allow unplayable formats to be
|
||||||
|
listed or downloaded (default)
|
||||||
|
|
||||||
## Subtitle Options:
|
## Subtitle Options:
|
||||||
--write-subs Write subtitle file
|
--write-subs Write subtitle file
|
||||||
@@ -549,23 +591,26 @@ Then simply type this
|
|||||||
|
|
||||||
## Post-Processing Options:
|
## Post-Processing Options:
|
||||||
-x, --extract-audio Convert video files to audio-only files
|
-x, --extract-audio Convert video files to audio-only files
|
||||||
(requires ffmpeg or avconv and ffprobe or
|
(requires ffmpeg and ffprobe)
|
||||||
avprobe)
|
|
||||||
--audio-format FORMAT Specify audio format: "best", "aac",
|
--audio-format FORMAT Specify audio format: "best", "aac",
|
||||||
"flac", "mp3", "m4a", "opus", "vorbis", or
|
"flac", "mp3", "m4a", "opus", "vorbis", or
|
||||||
"wav"; "best" by default; No effect without
|
"wav"; "best" by default; No effect without
|
||||||
-x
|
-x
|
||||||
--audio-quality QUALITY Specify ffmpeg/avconv audio quality, insert
|
--audio-quality QUALITY Specify ffmpeg audio quality, insert a
|
||||||
a value between 0 (better) and 9 (worse)
|
value between 0 (better) and 9 (worse) for
|
||||||
for VBR or a specific bitrate like 128K
|
VBR or a specific bitrate like 128K
|
||||||
(default 5)
|
(default 5)
|
||||||
--remux-video FORMAT Remux the video into another container if
|
--remux-video FORMAT Remux the video into another container if
|
||||||
necessary (currently supported: mp4|mkv).
|
necessary (currently supported: mp4|mkv|flv
|
||||||
If target container does not support the
|
|webm|mov|avi|mp3|mka|m4a|ogg|opus). If
|
||||||
video/audio codec, remuxing will fail
|
target container does not support the
|
||||||
|
video/audio codec, remuxing will fail. You
|
||||||
|
can specify multiple rules; eg.
|
||||||
|
"aac>m4a/mov>mp4/mkv" will remux aac to
|
||||||
|
m4a, mov to mp4 and anything else to mkv.
|
||||||
--recode-video FORMAT Re-encode the video into another format if
|
--recode-video FORMAT Re-encode the video into another format if
|
||||||
re-encoding is necessary (currently
|
re-encoding is necessary. The supported
|
||||||
supported: mp4|flv|ogg|webm|mkv|avi)
|
formats are the same as --remux-video
|
||||||
--postprocessor-args NAME:ARGS Give these arguments to the postprocessors.
|
--postprocessor-args NAME:ARGS Give these arguments to the postprocessors.
|
||||||
Specify the postprocessor/executable name
|
Specify the postprocessor/executable name
|
||||||
and the arguments separated by a colon ":"
|
and the arguments separated by a colon ":"
|
||||||
@@ -577,15 +622,14 @@ Then simply type this
|
|||||||
FixupStretched, FixupM4a, FixupM3u8,
|
FixupStretched, FixupM4a, FixupM3u8,
|
||||||
SubtitlesConvertor and EmbedThumbnail. The
|
SubtitlesConvertor and EmbedThumbnail. The
|
||||||
supported executables are: SponSkrub,
|
supported executables are: SponSkrub,
|
||||||
FFmpeg, FFprobe, avconf, avprobe and
|
FFmpeg, FFprobe, and AtomicParsley. You can
|
||||||
AtomicParsley. You can use this option
|
use this option multiple times to give
|
||||||
multiple times to give different arguments
|
different arguments to different
|
||||||
to different postprocessors. You can also
|
postprocessors. You can also specify
|
||||||
specify "PP+EXE:ARGS" to give the arguments
|
"PP+EXE:ARGS" to give the arguments to the
|
||||||
to the specified executable only when being
|
specified executable only when being used
|
||||||
used by the specified postprocessor. You
|
by the specified postprocessor. You can use
|
||||||
can use this option multiple times (Alias:
|
this option multiple times (Alias: --ppa)
|
||||||
--ppa)
|
|
||||||
-k, --keep-video Keep the intermediate video file on disk
|
-k, --keep-video Keep the intermediate video file on disk
|
||||||
after post-processing
|
after post-processing
|
||||||
--no-keep-video Delete the intermediate video file after
|
--no-keep-video Delete the intermediate video file after
|
||||||
@@ -599,16 +643,20 @@ Then simply type this
|
|||||||
--no-embed-thumbnail Do not embed thumbnail (default)
|
--no-embed-thumbnail Do not embed thumbnail (default)
|
||||||
--add-metadata Write metadata to the video file
|
--add-metadata Write metadata to the video file
|
||||||
--no-add-metadata Do not write metadata (default)
|
--no-add-metadata Do not write metadata (default)
|
||||||
--metadata-from-title FORMAT Parse additional metadata like song title /
|
--parse-metadata FIELD:FORMAT Parse additional metadata like title/artist
|
||||||
artist from the video title. The format
|
from other fields. Give field name to
|
||||||
syntax is the same as --output. Regular
|
extract data from, and format of the field
|
||||||
expression with named capture groups may
|
seperated by a ":". Either regular
|
||||||
|
expression with named capture groups or a
|
||||||
|
similar syntax to the output template can
|
||||||
also be used. The parsed parameters replace
|
also be used. The parsed parameters replace
|
||||||
existing values. Example: --metadata-from-
|
any existing values and can be use in
|
||||||
title "%(artist)s - %(title)s" matches a
|
output templateThis option can be used
|
||||||
|
multiple times. Example: --parse-metadata
|
||||||
|
"title:%(artist)s - %(title)s" matches a
|
||||||
title like "Coldplay - Paradise". Example
|
title like "Coldplay - Paradise". Example
|
||||||
(regex): --metadata-from-title
|
(regex): --parse-metadata
|
||||||
"(?P<artist>.+?) - (?P<title>.+)"
|
"description:Artist - (?P<artist>.+?)"
|
||||||
--xattrs Write metadata to the video file's xattrs
|
--xattrs Write metadata to the video file's xattrs
|
||||||
(using dublin core and xdg standards)
|
(using dublin core and xdg standards)
|
||||||
--fixup POLICY Automatically correct known faults of the
|
--fixup POLICY Automatically correct known faults of the
|
||||||
@@ -616,15 +664,9 @@ Then simply type this
|
|||||||
emit a warning), detect_or_warn (the
|
emit a warning), detect_or_warn (the
|
||||||
default; fix file if we can, warn
|
default; fix file if we can, warn
|
||||||
otherwise)
|
otherwise)
|
||||||
--prefer-avconv Prefer avconv over ffmpeg for running the
|
--ffmpeg-location PATH Location of the ffmpeg binary; either the
|
||||||
postprocessors (Alias: --no-prefer-ffmpeg)
|
path to the binary or its containing
|
||||||
--prefer-ffmpeg Prefer ffmpeg over avconv for running the
|
directory
|
||||||
postprocessors (default)
|
|
||||||
(Alias: --no-prefer-avconv)
|
|
||||||
--ffmpeg-location PATH Location of the ffmpeg/avconv binary;
|
|
||||||
either the path to the binary or its
|
|
||||||
containing directory
|
|
||||||
(Alias: --avconv-location)
|
|
||||||
--exec CMD Execute a command on the file after
|
--exec CMD Execute a command on the file after
|
||||||
downloading and post-processing, similar to
|
downloading and post-processing, similar to
|
||||||
find's -exec syntax. Example: --exec 'adb
|
find's -exec syntax. Example: --exec 'adb
|
||||||
@@ -727,7 +769,11 @@ 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 is not to set any template arguments when downloading a single file, like in `youtube-dlc -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. Allowed names along with sequence type are:
|
The basic usage of `-o` is not to set any template arguments when downloading a single file, like in `youtube-dlc -o funny_video.flv "https://some/video"`. However, it may contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [python string formatting operations](https://docs.python.org/2/library/stdtypes.html#string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations. Date/time fields can also be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it inside the parantheses seperated from the field name using a `>`. For example, `%(duration>%H-%M-%S)s`.
|
||||||
|
|
||||||
|
Additionally, you can set different output templates for the various metadata files seperately from the general output template by specifying the type of file followed by the template seperated by a colon ":". The different filetypes supported are `subtitle|thumbnail|description|annotation|infojson|pl_description|pl_infojson`. 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:
|
||||||
|
|
||||||
- `id` (string): Video identifier
|
- `id` (string): Video identifier
|
||||||
- `title` (string): Video title
|
- `title` (string): Video title
|
||||||
@@ -834,7 +880,7 @@ If you are using an output template inside a Windows batch file then you must es
|
|||||||
|
|
||||||
#### Output template examples
|
#### Output template examples
|
||||||
|
|
||||||
Note that on Windows you may need to use double quotes instead of single.
|
Note that on Windows you need to use double quotes instead of single.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ youtube-dlc --get-filename -o '%(title)s.%(ext)s' BaW_jenozKc
|
$ youtube-dlc --get-filename -o '%(title)s.%(ext)s' BaW_jenozKc
|
||||||
@@ -846,14 +892,17 @@ youtube-dlc_test_video_.mp4 # A simple file name
|
|||||||
# Download YouTube playlist videos in separate directory indexed by video order in a playlist
|
# Download YouTube playlist videos in separate directory indexed by video order in a playlist
|
||||||
$ youtube-dlc -o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re
|
$ youtube-dlc -o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re
|
||||||
|
|
||||||
|
# Download YouTube playlist videos in seperate directories according to their uploaded year
|
||||||
|
$ youtube-dlc -o '%(upload_date>%Y)s/%(title)s.%(ext)s' https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re
|
||||||
|
|
||||||
# Download all playlists of YouTube channel/user keeping each playlist in separate directory:
|
# Download all playlists of YouTube channel/user keeping each playlist in separate directory:
|
||||||
$ youtube-dlc -o '%(uploader)s/%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' https://www.youtube.com/user/TheLinuxFoundation/playlists
|
$ youtube-dlc -o '%(uploader)s/%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s' https://www.youtube.com/user/TheLinuxFoundation/playlists
|
||||||
|
|
||||||
# Download Udemy course keeping each chapter in separate directory under MyVideos directory in your home
|
# Download Udemy course keeping each chapter in separate directory under MyVideos directory in your home
|
||||||
$ youtube-dlc -u user -p password -o '~/MyVideos/%(playlist)s/%(chapter_number)s - %(chapter)s/%(title)s.%(ext)s' https://www.udemy.com/java-tutorial/
|
$ youtube-dlc -u user -p password -P '~/MyVideos' -o '%(playlist)s/%(chapter_number)s - %(chapter)s/%(title)s.%(ext)s' https://www.udemy.com/java-tutorial/
|
||||||
|
|
||||||
# Download entire series season keeping each series and each season in separate directory under C:/MyVideos
|
# Download entire series season keeping each series and each season in separate directory under C:/MyVideos
|
||||||
$ youtube-dlc -o "C:/MyVideos/%(series)s/%(season_number)s - %(season)s/%(episode_number)s - %(episode)s.%(ext)s" https://videomore.ru/kino_v_detalayah/5_sezon/367617
|
$ youtube-dlc -P "C:/MyVideos" -o "%(series)s/%(season_number)s - %(season)s/%(episode_number)s - %(episode)s.%(ext)s" https://videomore.ru/kino_v_detalayah/5_sezon/367617
|
||||||
|
|
||||||
# Stream the video being downloaded to stdout
|
# Stream the video being downloaded to stdout
|
||||||
$ youtube-dlc -o - BaW_jenozKc
|
$ youtube-dlc -o - BaW_jenozKc
|
||||||
@@ -862,7 +911,7 @@ $ youtube-dlc -o - BaW_jenozKc
|
|||||||
# FORMAT SELECTION
|
# FORMAT SELECTION
|
||||||
|
|
||||||
By default, youtube-dlc tries to download the best available quality if you **don't** pass any options.
|
By default, youtube-dlc tries to download the best available quality if you **don't** pass any options.
|
||||||
This is generally equivalent to using `-f bestvideo*+bestaudio/best`. However, if multiple audiostreams is enabled (`--audio-multistreams`), the default format changes to `-f bestvideo+bestaudio/best`. Similarly, if ffmpeg and avconv are unavailable, or if you use youtube-dlc to stream to `stdout` (`-o -`), the default becomes `-f best/bestvideo+bestaudio`.
|
This is generally equivalent to using `-f bestvideo*+bestaudio/best`. However, if multiple audiostreams is enabled (`--audio-multistreams`), the default format changes to `-f bestvideo+bestaudio/best`. Similarly, if ffmpeg is unavailable, or if you use youtube-dlc to stream to `stdout` (`-o -`), the default becomes `-f best/bestvideo+bestaudio`.
|
||||||
|
|
||||||
The general syntax for format selection is `--f FORMAT` (or `--format FORMAT`) where `FORMAT` is a *selector expression*, i.e. an expression that describes format or formats you would like to download.
|
The general syntax for format selection is `--f FORMAT` (or `--format FORMAT`) where `FORMAT` is a *selector expression*, i.e. an expression that describes format or formats you would like to download.
|
||||||
|
|
||||||
@@ -893,7 +942,7 @@ If you want to download multiple videos and they don't have the same formats ava
|
|||||||
|
|
||||||
If you want to download several formats of the same video use a comma as a separator, e.g. `-f 22,17,18` will download all these three formats, of course if they are available. Or a more sophisticated example combined with the precedence feature: `-f 136/137/mp4/bestvideo,140/m4a/bestaudio`.
|
If you want to download several formats of the same video use a comma as a separator, e.g. `-f 22,17,18` will download all these three formats, of course if they are available. Or a more sophisticated example combined with the precedence feature: `-f 136/137/mp4/bestvideo,140/m4a/bestaudio`.
|
||||||
|
|
||||||
You can merge the video and audio of multiple formats into a single file using `-f <format1>+<format2>+...` (requires ffmpeg or avconv installed), for example `-f bestvideo+bestaudio` will download the best video-only format, the best audio-only format and mux them together with ffmpeg/avconv. If `--no-video-multistreams` is used, all formats with a video stream except the first one are ignored. Similarly, if `--no-audio-multistreams` is used, all formats with an audio stream except the first one are ignored. For example, `-f bestvideo+best+bestaudio` will download and merge all 3 given formats. The resulting file will have 2 video streams and 2 audio streams. But `-f bestvideo+best+bestaudio --no-video-multistreams` will download and merge only `bestvideo` and `bestaudio`. `best` is ignored since another format containing a video stream (`bestvideo`) has already been selected. The order of the formats is therefore important. `-f best+bestaudio --no-audio-multistreams` will download and merge both formats while `-f bestaudio+best --no-audio-multistreams` will ignore `best` and download only `bestaudio`.
|
You can merge the video and audio of multiple formats into a single file using `-f <format1>+<format2>+...` (requires ffmpeg installed), for example `-f bestvideo+bestaudio` will download the best video-only format, the best audio-only format and mux them together with ffmpeg. If `--no-video-multistreams` is used, all formats with a video stream except the first one are ignored. Similarly, if `--no-audio-multistreams` is used, all formats with an audio stream except the first one are ignored. For example, `-f bestvideo+best+bestaudio` will download and merge all 3 given formats. The resulting file will have 2 video streams and 2 audio streams. But `-f bestvideo+best+bestaudio --no-video-multistreams` will download and merge only `bestvideo` and `bestaudio`. `best` is ignored since another format containing a video stream (`bestvideo`) has already been selected. The order of the formats is therefore important. `-f best+bestaudio --no-audio-multistreams` will download and merge both formats while `-f bestaudio+best --no-audio-multistreams` will ignore `best` and download only `bestaudio`.
|
||||||
|
|
||||||
## Filtering Formats
|
## Filtering Formats
|
||||||
|
|
||||||
@@ -936,10 +985,10 @@ You can change the criteria for being considered the `best` by using `-S` (`--fo
|
|||||||
- `hasaud`: Gives priority to formats that has a audio stream
|
- `hasaud`: Gives priority to formats that has a audio stream
|
||||||
- `ie_pref`: The format preference as given by the extractor
|
- `ie_pref`: The format preference as given by the extractor
|
||||||
- `lang`: Language preference as given by the extractor
|
- `lang`: Language preference as given by the extractor
|
||||||
- `quality`: The quality of the format. This is a metadata field available in some websites
|
- `quality`: The quality of the format as given by the extractor
|
||||||
- `source`: Preference of the source as given by the extractor
|
- `source`: Preference of the source as given by the extractor
|
||||||
- `proto`: Protocol used for download (`https`/`ftps` > `http`/`ftp` > `m3u8-native` > `m3u8` > `http-dash-segments` > other > `mms`/`rtsp` > unknown > `f4f`/`f4m`)
|
- `proto`: Protocol used for download (`https`/`ftps` > `http`/`ftp` > `m3u8-native` > `m3u8` > `http-dash-segments` > other > `mms`/`rtsp` > unknown > `f4f`/`f4m`)
|
||||||
- `vcodec`: Video Codec (`vp9` > `h265` > `h264` > `vp8` > `h263` > `theora` > other > unknown)
|
- `vcodec`: Video Codec (`av01` > `vp9.2` > `vp9` > `h265` > `h264` > `vp8` > `h263` > `theora` > other > unknown)
|
||||||
- `acodec`: Audio Codec (`opus` > `vorbis` > `aac` > `mp4a` > `mp3` > `ac3` > `dts` > other > unknown)
|
- `acodec`: Audio Codec (`opus` > `vorbis` > `aac` > `mp4a` > `mp3` > `ac3` > `dts` > other > unknown)
|
||||||
- `codec`: Equivalent to `vcodec,acodec`
|
- `codec`: Equivalent to `vcodec,acodec`
|
||||||
- `vext`: Video Extension (`mp4` > `webm` > `flv` > other > unknown). If `--prefer-free-formats` is used, `webm` is prefered.
|
- `vext`: Video Extension (`mp4` > `webm` > `flv` > other > unknown). If `--prefer-free-formats` is used, `webm` is prefered.
|
||||||
@@ -958,9 +1007,9 @@ You can change the criteria for being considered the `best` by using `-S` (`--fo
|
|||||||
- `br`: Equivalent to using `tbr,vbr,abr`
|
- `br`: Equivalent to using `tbr,vbr,abr`
|
||||||
- `asr`: Audio sample rate in Hz
|
- `asr`: Audio sample rate in Hz
|
||||||
|
|
||||||
Note that any other **numerical** field made available by the extractor can also be used. All fields, unless specified otherwise, are sorted in decending order. To reverse this, prefix the field with a `+`. Eg: `+res` prefers format with the smallest resolution. Additionally, you can suffix a prefered value for the fields, seperated by a `:`. Eg: `res:720` prefers larger videos, but no larger than 720p and the smallest video if there are no videos less than 720p. For `codec` and `ext`, you can provide two prefered values, the first for video and the second for audio. Eg: `+codec:avc:m4a` (equivalent to `+vcodec:avc,+acodec:m4a`) sets the video codec preference to `h264` > `h265` > `vp9` > `vp8` > `h263` > `theora` and audio codec preference to `mp4a` > `aac` > `vorbis` > `opus` > `mp3` > `ac3` > `dts`. You can also make the sorting prefer the nearest values to the provided by using `~` as the delimiter. Eg: `filesize~1G` prefers the format with filesize closest to 1 GiB.
|
Note that any other **numerical** field made available by the extractor can also be used. All fields, unless specified otherwise, are sorted in decending order. To reverse this, prefix the field with a `+`. Eg: `+res` prefers format with the smallest resolution. Additionally, you can suffix a prefered value for the fields, seperated by a `:`. Eg: `res:720` prefers larger videos, but no larger than 720p and the smallest video if there are no videos less than 720p. For `codec` and `ext`, you can provide two prefered values, the first for video and the second for audio. Eg: `+codec:avc:m4a` (equivalent to `+vcodec:avc,+acodec:m4a`) sets the video codec preference to `h264` > `h265` > `vp9` > `vp9.2` > `av01` > `vp8` > `h263` > `theora` and audio codec preference to `mp4a` > `aac` > `vorbis` > `opus` > `mp3` > `ac3` > `dts`. You can also make the sorting prefer the nearest values to the provided by using `~` as the delimiter. Eg: `filesize~1G` prefers the format with filesize closest to 1 GiB.
|
||||||
|
|
||||||
The fields `hasvid`, `ie_pref`, `lang`, `quality` are always given highest priority in sorting, irrespective of the user-defined order. This behaviour can be changed by using `--force-format-sort`. Apart from these, the default order used is: `res,fps,codec,size,br,asr,proto,ext,hasaud,source,id`. Note that the extractors may override this default order, but they cannot override the user-provided order.
|
The fields `hasvid`, `ie_pref`, `lang` are always given highest priority in sorting, irrespective of the user-defined order. This behaviour can be changed by using `--force-format-sort`. Apart from these, the default order used is: `quality,res,fps,codec:vp9.2,size,br,asr,proto,ext,hasaud,source,id`. Note that the extractors may override this default order, but they cannot override the user-provided order.
|
||||||
|
|
||||||
If your format selector is `worst`, the last item is selected after sorting. This means it will select the format that is worst in all repects. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps`.
|
If your format selector is `worst`, the last item is selected after sorting. This means it will select the format that is worst in all repects. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps`.
|
||||||
|
|
||||||
@@ -1087,7 +1136,7 @@ $ youtube-dlc -S '+res:480,codec,br'
|
|||||||
|
|
||||||
Plugins are loaded from `<root-dir>/ytdlp_plugins/<type>/__init__.py`. Currently only `extractor` plugins are supported. Support for `downloader` and `postprocessor` plugins may be added in the future. See [ytdlp_plugins](ytdlp_plugins) for example.
|
Plugins are loaded from `<root-dir>/ytdlp_plugins/<type>/__init__.py`. Currently only `extractor` plugins are supported. Support for `downloader` and `postprocessor` plugins may be added in the future. See [ytdlp_plugins](ytdlp_plugins) for example.
|
||||||
|
|
||||||
**Note**: `<root-dir>` is the directory of the binary (`<root-dir>/youtube-dlc`), or the root directory of the module if you are running directly from source-code ((`<root dir>/youtube_dlc/__main__.py`)
|
**Note**: `<root-dir>` is the directory of the binary (`<root-dir>/youtube-dlc`), or the root directory of the module if you are running directly from source-code (`<root dir>/youtube_dlc/__main__.py`)
|
||||||
|
|
||||||
# MORE
|
# MORE
|
||||||
For FAQ, Developer Instructions etc., see the [original README](https://github.com/ytdl-org/youtube-dl)
|
For FAQ, Developer Instructions etc., see the [original README](https://github.com/ytdl-org/youtube-dl#faq)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
31
devscripts/update-version.py
Normal file
31
devscripts/update-version.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from datetime import datetime
|
||||||
|
# import urllib.request
|
||||||
|
|
||||||
|
# response = urllib.request.urlopen('https://blackjack4494.github.io/youtube-dlc/update/LATEST_VERSION')
|
||||||
|
# old_version = response.read().decode('utf-8')
|
||||||
|
|
||||||
|
exec(compile(open('youtube_dlc/version.py').read(), 'youtube_dlc/version.py', 'exec'))
|
||||||
|
old_version = locals()['__version__']
|
||||||
|
|
||||||
|
old_version_list = old_version.split(".", 4)
|
||||||
|
|
||||||
|
old_ver = '.'.join(old_version_list[:3])
|
||||||
|
old_rev = old_version_list[3] if len(old_version_list) > 3 else ''
|
||||||
|
|
||||||
|
ver = datetime.utcnow().strftime("%Y.%m.%d")
|
||||||
|
rev = str(int(old_rev or 0) + 1) if old_ver == ver else ''
|
||||||
|
|
||||||
|
VERSION = '.'.join((ver, rev)) if rev else ver
|
||||||
|
# VERSION_LIST = [(int(v) for v in ver.split(".") + [rev or 0])]
|
||||||
|
|
||||||
|
print('::set-output name=ytdlc_version::' + VERSION)
|
||||||
|
|
||||||
|
file_version_py = open('youtube_dlc/version.py', 'rt')
|
||||||
|
data = file_version_py.read()
|
||||||
|
data = data.replace(old_version, VERSION)
|
||||||
|
file_version_py.close()
|
||||||
|
|
||||||
|
file_version_py = open('youtube_dlc/version.py', 'wt')
|
||||||
|
file_version_py.write(data)
|
||||||
|
file_version_py.close()
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
# Supported sites
|
# Supported sites
|
||||||
- **1tv**: Первый канал
|
- **1tv**: Первый канал
|
||||||
- **1up.com**
|
|
||||||
- **20min**
|
- **20min**
|
||||||
- **220.ro**
|
- **220.ro**
|
||||||
- **23video**
|
- **23video**
|
||||||
- **24video**
|
- **24video**
|
||||||
- **3qsdn**: 3Q SDN
|
- **3qsdn**: 3Q SDN
|
||||||
|
- **3sat**
|
||||||
- **4tube**
|
- **4tube**
|
||||||
- **56.com**
|
- **56.com**
|
||||||
- **5min**
|
- **5min**
|
||||||
@@ -47,12 +47,13 @@
|
|||||||
- **Amara**
|
- **Amara**
|
||||||
- **AMCNetworks**
|
- **AMCNetworks**
|
||||||
- **AmericasTestKitchen**
|
- **AmericasTestKitchen**
|
||||||
|
- **AmericasTestKitchenSeason**
|
||||||
- **anderetijden**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
|
- **anderetijden**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
|
||||||
- **AnimeLab**
|
- **AnimeLab**
|
||||||
- **AnimeLabShows**
|
- **AnimeLabShows**
|
||||||
- **AnimeOnDemand**
|
- **AnimeOnDemand**
|
||||||
- **Anvato**
|
- **Anvato**
|
||||||
- **aol.com**
|
- **aol.com**: Yahoo screen and movies
|
||||||
- **APA**
|
- **APA**
|
||||||
- **Aparat**
|
- **Aparat**
|
||||||
- **AppleConnect**
|
- **AppleConnect**
|
||||||
@@ -79,6 +80,9 @@
|
|||||||
- **AudioBoom**
|
- **AudioBoom**
|
||||||
- **audiomack**
|
- **audiomack**
|
||||||
- **audiomack:album**
|
- **audiomack:album**
|
||||||
|
- **Audius**: Audius.co
|
||||||
|
- **audius:playlist**: Audius.co playlists
|
||||||
|
- **audius:track**: Audius track ID or API link. Prepend with "audius:"
|
||||||
- **AWAAN**
|
- **AWAAN**
|
||||||
- **awaan:live**
|
- **awaan:live**
|
||||||
- **awaan:season**
|
- **awaan:season**
|
||||||
@@ -111,7 +115,9 @@
|
|||||||
- **BiliBili**
|
- **BiliBili**
|
||||||
- **BilibiliAudio**
|
- **BilibiliAudio**
|
||||||
- **BilibiliAudioAlbum**
|
- **BilibiliAudioAlbum**
|
||||||
|
- **BilibiliChannel**
|
||||||
- **BiliBiliPlayer**
|
- **BiliBiliPlayer**
|
||||||
|
- **BiliBiliSearch**: Bilibili video search, "bilisearch" keyword
|
||||||
- **BioBioChileTV**
|
- **BioBioChileTV**
|
||||||
- **Biography**
|
- **Biography**
|
||||||
- **BIQLE**
|
- **BIQLE**
|
||||||
@@ -197,8 +203,6 @@
|
|||||||
- **CNNArticle**
|
- **CNNArticle**
|
||||||
- **CNNBlogs**
|
- **CNNBlogs**
|
||||||
- **ComedyCentral**
|
- **ComedyCentral**
|
||||||
- **ComedyCentralFullEpisodes**
|
|
||||||
- **ComedyCentralShortname**
|
|
||||||
- **ComedyCentralTV**
|
- **ComedyCentralTV**
|
||||||
- **CondeNast**: Condé Nast media group: Allure, Architectural Digest, Ars Technica, Bon Appétit, Brides, Condé Nast, Condé Nast Traveler, Details, Epicurious, GQ, Glamour, Golf Digest, SELF, Teen Vogue, The New Yorker, Vanity Fair, Vogue, W Magazine, WIRED
|
- **CondeNast**: Condé Nast media group: Allure, Architectural Digest, Ars Technica, Bon Appétit, Brides, Condé Nast, Condé Nast Traveler, Details, Epicurious, GQ, Glamour, Golf Digest, SELF, Teen Vogue, The New Yorker, Vanity Fair, Vogue, W Magazine, WIRED
|
||||||
- **CONtv**
|
- **CONtv**
|
||||||
@@ -219,6 +223,7 @@
|
|||||||
- **curiositystream**
|
- **curiositystream**
|
||||||
- **curiositystream:collection**
|
- **curiositystream:collection**
|
||||||
- **CWTV**
|
- **CWTV**
|
||||||
|
- **DagelijkseKost**: dagelijksekost.een.be
|
||||||
- **DailyMail**
|
- **DailyMail**
|
||||||
- **dailymotion**
|
- **dailymotion**
|
||||||
- **dailymotion:playlist**
|
- **dailymotion:playlist**
|
||||||
@@ -241,6 +246,7 @@
|
|||||||
- **DiscoveryGo**
|
- **DiscoveryGo**
|
||||||
- **DiscoveryGoPlaylist**
|
- **DiscoveryGoPlaylist**
|
||||||
- **DiscoveryNetworksDe**
|
- **DiscoveryNetworksDe**
|
||||||
|
- **DiscoveryPlus**
|
||||||
- **DiscoveryVR**
|
- **DiscoveryVR**
|
||||||
- **Disney**
|
- **Disney**
|
||||||
- **dlive:stream**
|
- **dlive:stream**
|
||||||
@@ -367,6 +373,7 @@
|
|||||||
- **HentaiStigma**
|
- **HentaiStigma**
|
||||||
- **hetklokhuis**
|
- **hetklokhuis**
|
||||||
- **hgtv.com:show**
|
- **hgtv.com:show**
|
||||||
|
- **HGTVDe**
|
||||||
- **HiDive**
|
- **HiDive**
|
||||||
- **HistoricFilms**
|
- **HistoricFilms**
|
||||||
- **history:player**
|
- **history:player**
|
||||||
@@ -390,6 +397,8 @@
|
|||||||
- **HungamaSong**
|
- **HungamaSong**
|
||||||
- **Hypem**
|
- **Hypem**
|
||||||
- **ign.com**
|
- **ign.com**
|
||||||
|
- **IGNArticle**
|
||||||
|
- **IGNVideo**
|
||||||
- **IHeartRadio**
|
- **IHeartRadio**
|
||||||
- **iheartradio:podcast**
|
- **iheartradio:podcast**
|
||||||
- **imdb**: Internet Movie Database trailers
|
- **imdb**: Internet Movie Database trailers
|
||||||
@@ -520,6 +529,12 @@
|
|||||||
- **Mgoon**
|
- **Mgoon**
|
||||||
- **MGTV**: 芒果TV
|
- **MGTV**: 芒果TV
|
||||||
- **MiaoPai**
|
- **MiaoPai**
|
||||||
|
- **mildom**: Record ongoing live by specific user in Mildom
|
||||||
|
- **mildom:user:vod**: Download all VODs from specific user in Mildom
|
||||||
|
- **mildom:vod**: Download a VOD in Mildom
|
||||||
|
- **minds**
|
||||||
|
- **minds:channel**
|
||||||
|
- **minds:group**
|
||||||
- **MinistryGrid**
|
- **MinistryGrid**
|
||||||
- **Minoto**
|
- **Minoto**
|
||||||
- **miomio.tv**
|
- **miomio.tv**
|
||||||
@@ -549,6 +564,7 @@
|
|||||||
- **mtv:video**
|
- **mtv:video**
|
||||||
- **mtvjapan**
|
- **mtvjapan**
|
||||||
- **mtvservices:embedded**
|
- **mtvservices:embedded**
|
||||||
|
- **MTVUutisetArticle**
|
||||||
- **MuenchenTV**: münchen.tv
|
- **MuenchenTV**: münchen.tv
|
||||||
- **mva**: Microsoft Virtual Academy videos
|
- **mva**: Microsoft Virtual Academy videos
|
||||||
- **mva:course**: Microsoft Virtual Academy courses
|
- **mva:course**: Microsoft Virtual Academy courses
|
||||||
@@ -690,7 +706,6 @@
|
|||||||
- **parliamentlive.tv**: UK parliament videos
|
- **parliamentlive.tv**: UK parliament videos
|
||||||
- **Patreon**
|
- **Patreon**
|
||||||
- **pbs**: Public Broadcasting Service (PBS) and member stations: PBS: Public Broadcasting Service, APT - Alabama Public Television (WBIQ), GPB/Georgia Public Broadcasting (WGTV), Mississippi Public Broadcasting (WMPN), Nashville Public Television (WNPT), WFSU-TV (WFSU), WSRE (WSRE), WTCI (WTCI), WPBA/Channel 30 (WPBA), Alaska Public Media (KAKM), Arizona PBS (KAET), KNME-TV/Channel 5 (KNME), Vegas PBS (KLVX), AETN/ARKANSAS ETV NETWORK (KETS), KET (WKLE), WKNO/Channel 10 (WKNO), LPB/LOUISIANA PUBLIC BROADCASTING (WLPB), OETA (KETA), Ozarks Public Television (KOZK), WSIU Public Broadcasting (WSIU), KEET TV (KEET), KIXE/Channel 9 (KIXE), KPBS San Diego (KPBS), KQED (KQED), KVIE Public Television (KVIE), PBS SoCal/KOCE (KOCE), ValleyPBS (KVPT), CONNECTICUT PUBLIC TELEVISION (WEDH), KNPB Channel 5 (KNPB), SOPTV (KSYS), Rocky Mountain PBS (KRMA), KENW-TV3 (KENW), KUED Channel 7 (KUED), Wyoming PBS (KCWC), Colorado Public Television / KBDI 12 (KBDI), KBYU-TV (KBYU), Thirteen/WNET New York (WNET), WGBH/Channel 2 (WGBH), WGBY (WGBY), NJTV Public Media NJ (WNJT), WLIW21 (WLIW), mpt/Maryland Public Television (WMPB), WETA Television and Radio (WETA), WHYY (WHYY), PBS 39 (WLVT), WVPT - Your Source for PBS and More! (WVPT), Howard University Television (WHUT), WEDU PBS (WEDU), WGCU Public Media (WGCU), WPBT2 (WPBT), WUCF TV (WUCF), WUFT/Channel 5 (WUFT), WXEL/Channel 42 (WXEL), WLRN/Channel 17 (WLRN), WUSF Public Broadcasting (WUSF), ETV (WRLK), UNC-TV (WUNC), PBS Hawaii - Oceanic Cable Channel 10 (KHET), Idaho Public Television (KAID), KSPS (KSPS), OPB (KOPB), KWSU/Channel 10 & KTNW/Channel 31 (KWSU), WILL-TV (WILL), Network Knowledge - WSEC/Springfield (WSEC), WTTW11 (WTTW), Iowa Public Television/IPTV (KDIN), Nine Network (KETC), PBS39 Fort Wayne (WFWA), WFYI Indianapolis (WFYI), Milwaukee Public Television (WMVS), WNIN (WNIN), WNIT Public Television (WNIT), WPT (WPNE), WVUT/Channel 22 (WVUT), WEIU/Channel 51 (WEIU), WQPT-TV (WQPT), WYCC PBS Chicago (WYCC), WIPB-TV (WIPB), WTIU (WTIU), CET (WCET), ThinkTVNetwork (WPTD), WBGU-TV (WBGU), WGVU TV (WGVU), NET1 (KUON), Pioneer Public Television (KWCM), SDPB Television (KUSD), TPT (KTCA), KSMQ (KSMQ), KPTS/Channel 8 (KPTS), KTWU/Channel 11 (KTWU), East Tennessee PBS (WSJK), WCTE-TV (WCTE), WLJT, Channel 11 (WLJT), WOSU TV (WOSU), WOUB/WOUC (WOUB), WVPB (WVPB), WKYU-PBS (WKYU), KERA 13 (KERA), MPBN (WCBB), Mountain Lake PBS (WCFE), NHPTV (WENH), Vermont PBS (WETK), witf (WITF), WQED Multimedia (WQED), WMHT Educational Telecommunications (WMHT), Q-TV (WDCQ), WTVS Detroit Public TV (WTVS), CMU Public Television (WCMU), WKAR-TV (WKAR), WNMU-TV Public TV 13 (WNMU), WDSE - WRPT (WDSE), WGTE TV (WGTE), Lakeland Public Television (KAWE), KMOS-TV - Channels 6.1, 6.2 and 6.3 (KMOS), MontanaPBS (KUSM), KRWG/Channel 22 (KRWG), KACV (KACV), KCOS/Channel 13 (KCOS), WCNY/Channel 24 (WCNY), WNED (WNED), WPBS (WPBS), WSKG Public TV (WSKG), WXXI (WXXI), WPSU (WPSU), WVIA Public Media Studios (WVIA), WTVI (WTVI), Western Reserve PBS (WNEO), WVIZ/PBS ideastream (WVIZ), KCTS 9 (KCTS), Basin PBS (KPBT), KUHT / Channel 8 (KUHT), KLRN (KLRN), KLRU (KLRU), WTJX Channel 12 (WTJX), WCVE PBS (WCVE), KBTC Public Television (KBTC)
|
- **pbs**: Public Broadcasting Service (PBS) and member stations: PBS: Public Broadcasting Service, APT - Alabama Public Television (WBIQ), GPB/Georgia Public Broadcasting (WGTV), Mississippi Public Broadcasting (WMPN), Nashville Public Television (WNPT), WFSU-TV (WFSU), WSRE (WSRE), WTCI (WTCI), WPBA/Channel 30 (WPBA), Alaska Public Media (KAKM), Arizona PBS (KAET), KNME-TV/Channel 5 (KNME), Vegas PBS (KLVX), AETN/ARKANSAS ETV NETWORK (KETS), KET (WKLE), WKNO/Channel 10 (WKNO), LPB/LOUISIANA PUBLIC BROADCASTING (WLPB), OETA (KETA), Ozarks Public Television (KOZK), WSIU Public Broadcasting (WSIU), KEET TV (KEET), KIXE/Channel 9 (KIXE), KPBS San Diego (KPBS), KQED (KQED), KVIE Public Television (KVIE), PBS SoCal/KOCE (KOCE), ValleyPBS (KVPT), CONNECTICUT PUBLIC TELEVISION (WEDH), KNPB Channel 5 (KNPB), SOPTV (KSYS), Rocky Mountain PBS (KRMA), KENW-TV3 (KENW), KUED Channel 7 (KUED), Wyoming PBS (KCWC), Colorado Public Television / KBDI 12 (KBDI), KBYU-TV (KBYU), Thirteen/WNET New York (WNET), WGBH/Channel 2 (WGBH), WGBY (WGBY), NJTV Public Media NJ (WNJT), WLIW21 (WLIW), mpt/Maryland Public Television (WMPB), WETA Television and Radio (WETA), WHYY (WHYY), PBS 39 (WLVT), WVPT - Your Source for PBS and More! (WVPT), Howard University Television (WHUT), WEDU PBS (WEDU), WGCU Public Media (WGCU), WPBT2 (WPBT), WUCF TV (WUCF), WUFT/Channel 5 (WUFT), WXEL/Channel 42 (WXEL), WLRN/Channel 17 (WLRN), WUSF Public Broadcasting (WUSF), ETV (WRLK), UNC-TV (WUNC), PBS Hawaii - Oceanic Cable Channel 10 (KHET), Idaho Public Television (KAID), KSPS (KSPS), OPB (KOPB), KWSU/Channel 10 & KTNW/Channel 31 (KWSU), WILL-TV (WILL), Network Knowledge - WSEC/Springfield (WSEC), WTTW11 (WTTW), Iowa Public Television/IPTV (KDIN), Nine Network (KETC), PBS39 Fort Wayne (WFWA), WFYI Indianapolis (WFYI), Milwaukee Public Television (WMVS), WNIN (WNIN), WNIT Public Television (WNIT), WPT (WPNE), WVUT/Channel 22 (WVUT), WEIU/Channel 51 (WEIU), WQPT-TV (WQPT), WYCC PBS Chicago (WYCC), WIPB-TV (WIPB), WTIU (WTIU), CET (WCET), ThinkTVNetwork (WPTD), WBGU-TV (WBGU), WGVU TV (WGVU), NET1 (KUON), Pioneer Public Television (KWCM), SDPB Television (KUSD), TPT (KTCA), KSMQ (KSMQ), KPTS/Channel 8 (KPTS), KTWU/Channel 11 (KTWU), East Tennessee PBS (WSJK), WCTE-TV (WCTE), WLJT, Channel 11 (WLJT), WOSU TV (WOSU), WOUB/WOUC (WOUB), WVPB (WVPB), WKYU-PBS (WKYU), KERA 13 (KERA), MPBN (WCBB), Mountain Lake PBS (WCFE), NHPTV (WENH), Vermont PBS (WETK), witf (WITF), WQED Multimedia (WQED), WMHT Educational Telecommunications (WMHT), Q-TV (WDCQ), WTVS Detroit Public TV (WTVS), CMU Public Television (WCMU), WKAR-TV (WKAR), WNMU-TV Public TV 13 (WNMU), WDSE - WRPT (WDSE), WGTE TV (WGTE), Lakeland Public Television (KAWE), KMOS-TV - Channels 6.1, 6.2 and 6.3 (KMOS), MontanaPBS (KUSM), KRWG/Channel 22 (KRWG), KACV (KACV), KCOS/Channel 13 (KCOS), WCNY/Channel 24 (WCNY), WNED (WNED), WPBS (WPBS), WSKG Public TV (WSKG), WXXI (WXXI), WPSU (WPSU), WVIA Public Media Studios (WVIA), WTVI (WTVI), Western Reserve PBS (WNEO), WVIZ/PBS ideastream (WVIZ), KCTS 9 (KCTS), Basin PBS (KPBT), KUHT / Channel 8 (KUHT), KLRN (KLRN), KLRU (KLRU), WTJX Channel 12 (WTJX), WCVE PBS (WCVE), KBTC Public Television (KBTC)
|
||||||
- **pcmag**
|
|
||||||
- **PearVideo**
|
- **PearVideo**
|
||||||
- **PeerTube**
|
- **PeerTube**
|
||||||
- **People**
|
- **People**
|
||||||
@@ -843,6 +858,9 @@
|
|||||||
- **ShahidShow**
|
- **ShahidShow**
|
||||||
- **Shared**: shared.sx
|
- **Shared**: shared.sx
|
||||||
- **ShowRoomLive**
|
- **ShowRoomLive**
|
||||||
|
- **simplecast**
|
||||||
|
- **simplecast:episode**
|
||||||
|
- **simplecast:podcast**
|
||||||
- **Sina**
|
- **Sina**
|
||||||
- **sky.it**
|
- **sky.it**
|
||||||
- **sky:news**
|
- **sky:news**
|
||||||
@@ -880,6 +898,8 @@
|
|||||||
- **Sport5**
|
- **Sport5**
|
||||||
- **SportBox**
|
- **SportBox**
|
||||||
- **SportDeutschland**
|
- **SportDeutschland**
|
||||||
|
- **spotify**
|
||||||
|
- **spotify:show**
|
||||||
- **Spreaker**
|
- **Spreaker**
|
||||||
- **SpreakerPage**
|
- **SpreakerPage**
|
||||||
- **SpreakerShow**
|
- **SpreakerShow**
|
||||||
@@ -962,13 +982,13 @@
|
|||||||
- **TNAFlixNetworkEmbed**
|
- **TNAFlixNetworkEmbed**
|
||||||
- **toggle**
|
- **toggle**
|
||||||
- **ToonGoggles**
|
- **ToonGoggles**
|
||||||
- **Tosh**: Tosh.0
|
|
||||||
- **tou.tv**
|
- **tou.tv**
|
||||||
- **Toypics**: Toypics video
|
- **Toypics**: Toypics video
|
||||||
- **ToypicsUser**: Toypics user profile
|
- **ToypicsUser**: Toypics user profile
|
||||||
- **TrailerAddict** (Currently broken)
|
- **TrailerAddict** (Currently broken)
|
||||||
- **Trilulilu**
|
- **Trilulilu**
|
||||||
- **TrovoLive**
|
- **Trovo**
|
||||||
|
- **TrovoVod**
|
||||||
- **TruNews**
|
- **TruNews**
|
||||||
- **TruTV**
|
- **TruTV**
|
||||||
- **Tube8**
|
- **Tube8**
|
||||||
@@ -1078,7 +1098,6 @@
|
|||||||
- **vidme**
|
- **vidme**
|
||||||
- **vidme:user**
|
- **vidme:user**
|
||||||
- **vidme:user:likes**
|
- **vidme:user:likes**
|
||||||
- **Vidzi**
|
|
||||||
- **vier**: vier.be and vijf.be
|
- **vier**: vier.be and vijf.be
|
||||||
- **vier:videos**
|
- **vier:videos**
|
||||||
- **viewlift**
|
- **viewlift**
|
||||||
@@ -1123,6 +1142,7 @@
|
|||||||
- **vrv**
|
- **vrv**
|
||||||
- **vrv:series**
|
- **vrv:series**
|
||||||
- **VShare**
|
- **VShare**
|
||||||
|
- **VTM**
|
||||||
- **VTXTV**
|
- **VTXTV**
|
||||||
- **vube**: Vube.com
|
- **vube**: Vube.com
|
||||||
- **VuClip**
|
- **VuClip**
|
||||||
@@ -1205,9 +1225,9 @@
|
|||||||
- **youtube:history**: Youtube watch history, ":ythistory" for short (requires authentication)
|
- **youtube:history**: Youtube watch history, ":ythistory" for short (requires authentication)
|
||||||
- **youtube:playlist**: YouTube.com playlists
|
- **youtube:playlist**: YouTube.com playlists
|
||||||
- **youtube:recommended**: YouTube.com recommended videos, ":ytrec" for short (requires authentication)
|
- **youtube:recommended**: YouTube.com recommended videos, ":ytrec" for short (requires authentication)
|
||||||
- **youtube:search**: YouTube.com searches
|
- **youtube:search**: YouTube.com searches, "ytsearch" keyword
|
||||||
- **youtube:search:date**: YouTube.com searches, newest videos first, "ytsearchdate" keyword
|
- **youtube:search:date**: YouTube.com searches, newest videos first, "ytsearchdate" keyword
|
||||||
- **youtube:search_url**: YouTube.com searches, "ytsearch" keyword
|
- **youtube:search_url**: YouTube.com search URLs
|
||||||
- **youtube:subscriptions**: YouTube.com subscriptions feed, ":ytsubs" for short (requires authentication)
|
- **youtube:subscriptions**: YouTube.com subscriptions feed, ":ytsubs" for short (requires authentication)
|
||||||
- **youtube:tab**: YouTube.com tab
|
- **youtube:tab**: YouTube.com tab
|
||||||
- **youtube:watchlater**: Youtube watch later list, ":ytwatchlater" for short (requires authentication)
|
- **youtube:watchlater**: Youtube watch later list, ":ytwatchlater" for short (requires authentication)
|
||||||
@@ -1218,6 +1238,7 @@
|
|||||||
- **ZattooLive**
|
- **ZattooLive**
|
||||||
- **ZDF-3sat**
|
- **ZDF-3sat**
|
||||||
- **ZDFChannel**
|
- **ZDFChannel**
|
||||||
|
- **Zhihu**
|
||||||
- **zingmp3**: mp3.zing.vn
|
- **zingmp3**: mp3.zing.vn
|
||||||
- **zoom**
|
- **zoom**
|
||||||
- **Zype**
|
- **Zype**
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
py -m PyInstaller youtube_dlc\__main__.py --onefile --name youtube-dlc --version-file win\ver.txt --icon win\icon\cloud.ico --upx-exclude=vcruntime140.dll --exclude-module ytdlp_plugins
|
|
||||||
107
pyinst.py
107
pyinst.py
@@ -1,55 +1,41 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
import sys
|
||||||
|
# import os
|
||||||
|
import platform
|
||||||
|
|
||||||
from PyInstaller.utils.win32.versioninfo import (
|
from PyInstaller.utils.win32.versioninfo import (
|
||||||
VarStruct, VarFileInfo, StringStruct, StringTable,
|
VarStruct, VarFileInfo, StringStruct, StringTable,
|
||||||
StringFileInfo, FixedFileInfo, VSVersionInfo, SetVersion,
|
StringFileInfo, FixedFileInfo, VSVersionInfo, SetVersion,
|
||||||
)
|
)
|
||||||
import PyInstaller.__main__
|
import PyInstaller.__main__
|
||||||
|
|
||||||
from datetime import datetime
|
arch = sys.argv[1] if len(sys.argv) > 1 else platform.architecture()[0][:2]
|
||||||
|
assert arch in ('32', '64')
|
||||||
|
print('Building %sbit version' % arch)
|
||||||
|
_x86 = '_x86' if arch == '32' else ''
|
||||||
|
|
||||||
FILE_DESCRIPTION = 'Media Downloader'
|
FILE_DESCRIPTION = 'Media Downloader%s' % (' (32 Bit)' if _x86 else '')
|
||||||
|
|
||||||
|
# root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
# print('Changing working directory to %s' % root_dir)
|
||||||
|
# os.chdir(root_dir)
|
||||||
|
|
||||||
exec(compile(open('youtube_dlc/version.py').read(), 'youtube_dlc/version.py', 'exec'))
|
exec(compile(open('youtube_dlc/version.py').read(), 'youtube_dlc/version.py', 'exec'))
|
||||||
|
VERSION = locals()['__version__']
|
||||||
|
|
||||||
_LATEST_VERSION = locals()['__version__']
|
VERSION_LIST = VERSION.split('.')
|
||||||
|
VERSION_LIST = list(map(int, VERSION_LIST)) + [0] * (4 - len(VERSION_LIST))
|
||||||
|
|
||||||
_OLD_VERSION = _LATEST_VERSION.rsplit("-", 1)
|
print('Version: %s%s' % (VERSION, _x86))
|
||||||
|
print('Remember to update the version using devscipts\\update-version.py')
|
||||||
|
|
||||||
if len(_OLD_VERSION) > 0:
|
VERSION_FILE = VSVersionInfo(
|
||||||
old_ver = _OLD_VERSION[0]
|
|
||||||
|
|
||||||
old_rev = ''
|
|
||||||
if len(_OLD_VERSION) > 1:
|
|
||||||
old_rev = _OLD_VERSION[1]
|
|
||||||
|
|
||||||
now = datetime.now()
|
|
||||||
# ver = f'{datetime.today():%Y.%m.%d}'
|
|
||||||
ver = now.strftime("%Y.%m.%d")
|
|
||||||
rev = ''
|
|
||||||
|
|
||||||
if old_ver == ver:
|
|
||||||
if old_rev:
|
|
||||||
rev = int(old_rev) + 1
|
|
||||||
else:
|
|
||||||
rev = 1
|
|
||||||
|
|
||||||
_SEPARATOR = '-'
|
|
||||||
|
|
||||||
version = _SEPARATOR.join(filter(None, [ver, str(rev)]))
|
|
||||||
|
|
||||||
print(version)
|
|
||||||
|
|
||||||
version_list = ver.split(".")
|
|
||||||
_year, _month, _day = [int(value) for value in version_list]
|
|
||||||
_rev = 0
|
|
||||||
if rev:
|
|
||||||
_rev = rev
|
|
||||||
_ver_tuple = _year, _month, _day, _rev
|
|
||||||
|
|
||||||
version_file = VSVersionInfo(
|
|
||||||
ffi=FixedFileInfo(
|
ffi=FixedFileInfo(
|
||||||
filevers=_ver_tuple,
|
filevers=VERSION_LIST,
|
||||||
prodvers=_ver_tuple,
|
prodvers=VERSION_LIST,
|
||||||
mask=0x3F,
|
mask=0x3F,
|
||||||
flags=0x0,
|
flags=0x0,
|
||||||
OS=0x4,
|
OS=0x4,
|
||||||
@@ -58,35 +44,36 @@ version_file = VSVersionInfo(
|
|||||||
date=(0, 0),
|
date=(0, 0),
|
||||||
),
|
),
|
||||||
kids=[
|
kids=[
|
||||||
StringFileInfo(
|
StringFileInfo([
|
||||||
[
|
|
||||||
StringTable(
|
StringTable(
|
||||||
"040904B0",
|
'040904B0', [
|
||||||
[
|
StringStruct('Comments', 'Youtube-dlc%s Command Line Interface.' % _x86),
|
||||||
StringStruct("Comments", "Youtube-dlc Command Line Interface."),
|
StringStruct('CompanyName', 'https://github.com/pukkandan/yt-dlp'),
|
||||||
StringStruct("CompanyName", "theidel@uni-bremen.de"),
|
StringStruct('FileDescription', FILE_DESCRIPTION),
|
||||||
StringStruct("FileDescription", FILE_DESCRIPTION),
|
StringStruct('FileVersion', VERSION),
|
||||||
StringStruct("FileVersion", version),
|
StringStruct('InternalName', 'youtube-dlc%s' % _x86),
|
||||||
StringStruct("InternalName", "youtube-dlc"),
|
|
||||||
StringStruct(
|
StringStruct(
|
||||||
"LegalCopyright",
|
'LegalCopyright',
|
||||||
"theidel@uni-bremen.de | UNLICENSE",
|
'pukkandan@gmail.com | UNLICENSE',
|
||||||
),
|
),
|
||||||
StringStruct("OriginalFilename", "youtube-dlc.exe"),
|
StringStruct('OriginalFilename', 'youtube-dlc%s.exe' % _x86),
|
||||||
StringStruct("ProductName", "Youtube-dlc"),
|
StringStruct('ProductName', 'Youtube-dlc%s' % _x86),
|
||||||
StringStruct("ProductVersion", version + " | git.io/JLh7K"),
|
StringStruct('ProductVersion', '%s%s' % (VERSION, _x86)),
|
||||||
],
|
])]),
|
||||||
)
|
VarFileInfo([VarStruct('Translation', [0, 1200])])
|
||||||
]
|
|
||||||
),
|
|
||||||
VarFileInfo([VarStruct("Translation", [0, 1200])])
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
PyInstaller.__main__.run([
|
PyInstaller.__main__.run([
|
||||||
'--name=youtube-dlc',
|
'--name=youtube-dlc%s' % _x86,
|
||||||
'--onefile',
|
'--onefile',
|
||||||
'--icon=win/icon/cloud.ico',
|
'--icon=devscripts/cloud.ico',
|
||||||
|
'--exclude-module=youtube_dl',
|
||||||
|
'--exclude-module=test',
|
||||||
|
'--exclude-module=ytdlp_plugins',
|
||||||
|
'--hidden-import=mutagen',
|
||||||
|
'--hidden-import=Crypto',
|
||||||
|
'--upx-exclude=vcruntime140.dll',
|
||||||
'youtube_dlc/__main__.py',
|
'youtube_dlc/__main__.py',
|
||||||
])
|
])
|
||||||
SetVersion('dist/youtube-dlc.exe', version_file)
|
SetVersion('dist/youtube-dlc%s.exe' % _x86, VERSION_FILE)
|
||||||
|
|||||||
92
pyinst32.py
92
pyinst32.py
@@ -1,92 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
from PyInstaller.utils.win32.versioninfo import (
|
|
||||||
VarStruct, VarFileInfo, StringStruct, StringTable,
|
|
||||||
StringFileInfo, FixedFileInfo, VSVersionInfo, SetVersion,
|
|
||||||
)
|
|
||||||
import PyInstaller.__main__
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
FILE_DESCRIPTION = 'Media Downloader 32 Bit Version'
|
|
||||||
|
|
||||||
exec(compile(open('youtube_dlc/version.py').read(), 'youtube_dlc/version.py', 'exec'))
|
|
||||||
|
|
||||||
_LATEST_VERSION = locals()['__version__']
|
|
||||||
|
|
||||||
_OLD_VERSION = _LATEST_VERSION.rsplit("-", 1)
|
|
||||||
|
|
||||||
if len(_OLD_VERSION) > 0:
|
|
||||||
old_ver = _OLD_VERSION[0]
|
|
||||||
|
|
||||||
old_rev = ''
|
|
||||||
if len(_OLD_VERSION) > 1:
|
|
||||||
old_rev = _OLD_VERSION[1]
|
|
||||||
|
|
||||||
now = datetime.now()
|
|
||||||
# ver = f'{datetime.today():%Y.%m.%d}'
|
|
||||||
ver = now.strftime("%Y.%m.%d")
|
|
||||||
rev = ''
|
|
||||||
|
|
||||||
if old_ver == ver:
|
|
||||||
if old_rev:
|
|
||||||
rev = int(old_rev) + 1
|
|
||||||
else:
|
|
||||||
rev = 1
|
|
||||||
|
|
||||||
_SEPARATOR = '-'
|
|
||||||
|
|
||||||
version = _SEPARATOR.join(filter(None, [ver, str(rev)]))
|
|
||||||
|
|
||||||
print(version)
|
|
||||||
|
|
||||||
version_list = ver.split(".")
|
|
||||||
_year, _month, _day = [int(value) for value in version_list]
|
|
||||||
_rev = 0
|
|
||||||
if rev:
|
|
||||||
_rev = rev
|
|
||||||
_ver_tuple = _year, _month, _day, _rev
|
|
||||||
|
|
||||||
version_file = VSVersionInfo(
|
|
||||||
ffi=FixedFileInfo(
|
|
||||||
filevers=_ver_tuple,
|
|
||||||
prodvers=_ver_tuple,
|
|
||||||
mask=0x3F,
|
|
||||||
flags=0x0,
|
|
||||||
OS=0x4,
|
|
||||||
fileType=0x1,
|
|
||||||
subtype=0x0,
|
|
||||||
date=(0, 0),
|
|
||||||
),
|
|
||||||
kids=[
|
|
||||||
StringFileInfo(
|
|
||||||
[
|
|
||||||
StringTable(
|
|
||||||
"040904B0",
|
|
||||||
[
|
|
||||||
StringStruct("Comments", "Youtube-dlc_x86 Command Line Interface."),
|
|
||||||
StringStruct("CompanyName", "theidel@uni-bremen.de"),
|
|
||||||
StringStruct("FileDescription", FILE_DESCRIPTION),
|
|
||||||
StringStruct("FileVersion", version),
|
|
||||||
StringStruct("InternalName", "youtube-dlc_x86"),
|
|
||||||
StringStruct(
|
|
||||||
"LegalCopyright",
|
|
||||||
"theidel@uni-bremen.de | UNLICENSE",
|
|
||||||
),
|
|
||||||
StringStruct("OriginalFilename", "youtube-dlc_x86.exe"),
|
|
||||||
StringStruct("ProductName", "Youtube-dlc_x86"),
|
|
||||||
StringStruct("ProductVersion", version + "_x86 | git.io/JUGsM"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
VarFileInfo([VarStruct("Translation", [0, 1200])])
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
PyInstaller.__main__.run([
|
|
||||||
'--name=youtube-dlc_x86',
|
|
||||||
'--onefile',
|
|
||||||
'--icon=win/icon/cloud.ico',
|
|
||||||
'youtube_dlc/__main__.py',
|
|
||||||
])
|
|
||||||
SetVersion('dist/youtube-dlc_x86.exe', version_file)
|
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mutagen
|
||||||
|
pycryptodome
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
from datetime import datetime
|
|
||||||
# import urllib.request
|
|
||||||
|
|
||||||
# response = urllib.request.urlopen('https://blackjack4494.github.io/youtube-dlc/update/LATEST_VERSION')
|
|
||||||
# _LATEST_VERSION = response.read().decode('utf-8')
|
|
||||||
|
|
||||||
exec(compile(open('youtube_dlc/version.py').read(), 'youtube_dlc/version.py', 'exec'))
|
|
||||||
|
|
||||||
_LATEST_VERSION = locals()['__version__']
|
|
||||||
|
|
||||||
_OLD_VERSION = _LATEST_VERSION.rsplit("-", 1)
|
|
||||||
|
|
||||||
if len(_OLD_VERSION) > 0:
|
|
||||||
old_ver = _OLD_VERSION[0]
|
|
||||||
|
|
||||||
old_rev = ''
|
|
||||||
if len(_OLD_VERSION) > 1:
|
|
||||||
old_rev = _OLD_VERSION[1]
|
|
||||||
|
|
||||||
now = datetime.now()
|
|
||||||
# ver = f'{datetime.today():%Y.%m.%d}'
|
|
||||||
ver = now.strftime("%Y.%m.%d")
|
|
||||||
rev = ''
|
|
||||||
|
|
||||||
if old_ver == ver:
|
|
||||||
if old_rev:
|
|
||||||
rev = int(old_rev) + 1
|
|
||||||
else:
|
|
||||||
rev = 1
|
|
||||||
|
|
||||||
_SEPARATOR = '-'
|
|
||||||
|
|
||||||
version = _SEPARATOR.join(filter(None, [ver, str(rev)]))
|
|
||||||
|
|
||||||
print('::set-output name=ytdlc_version::' + version)
|
|
||||||
|
|
||||||
file_version_py = open('youtube_dlc/version.py', 'rt')
|
|
||||||
data = file_version_py.read()
|
|
||||||
data = data.replace(locals()['__version__'], version)
|
|
||||||
file_version_py.close()
|
|
||||||
file_version_py = open('youtube_dlc/version.py', 'wt')
|
|
||||||
file_version_py.write(data)
|
|
||||||
file_version_py.close()
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Unused
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
from datetime import datetime
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
response = urllib.request.urlopen('https://blackjack4494.github.io/youtube-dlc/update/LATEST_VERSION')
|
|
||||||
|
|
||||||
_LATEST_VERSION = response.read().decode('utf-8')
|
|
||||||
|
|
||||||
_OLD_VERSION = _LATEST_VERSION.rsplit("-", 1)
|
|
||||||
|
|
||||||
if len(_OLD_VERSION) > 0:
|
|
||||||
old_ver = _OLD_VERSION[0]
|
|
||||||
|
|
||||||
old_rev = ''
|
|
||||||
if len(_OLD_VERSION) > 1:
|
|
||||||
old_rev = _OLD_VERSION[1]
|
|
||||||
|
|
||||||
now = datetime.now()
|
|
||||||
# ver = f'{datetime.today():%Y.%m.%d}'
|
|
||||||
ver = now.strftime("%Y.%m.%d")
|
|
||||||
rev = ''
|
|
||||||
|
|
||||||
if old_ver == ver:
|
|
||||||
if old_rev:
|
|
||||||
rev = int(old_rev) + 1
|
|
||||||
else:
|
|
||||||
rev = 1
|
|
||||||
|
|
||||||
_SEPARATOR = '-'
|
|
||||||
|
|
||||||
version = _SEPARATOR.join(filter(None, [ver, str(rev)]))
|
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
universal = True
|
universal = True
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
exclude = youtube_dlc/extractor/__init__.py,devscripts/buildserver.py,devscripts/lazy_load_template.py,devscripts/make_issue_template.py,setup.py,build,.git,venv,devscripts/create-github-release.py,devscripts/release.sh,devscripts/show-downloads-statistics.py,scripts/update-version.py
|
exclude = youtube_dlc/extractor/__init__.py,devscripts/buildserver.py,devscripts/lazy_load_template.py,devscripts/make_issue_template.py,setup.py,build,.git,venv,devscripts/create-github-release.py,devscripts/release.sh,devscripts/show-downloads-statistics.py
|
||||||
ignore = E402,E501,E731,E741,W503
|
ignore = E402,E501,E731,E741,W503
|
||||||
15
setup.py
15
setup.py
@@ -7,10 +7,12 @@ import warnings
|
|||||||
import sys
|
import sys
|
||||||
from distutils.spawn import spawn
|
from distutils.spawn import spawn
|
||||||
|
|
||||||
|
|
||||||
# Get the version from youtube_dlc/version.py without importing the package
|
# Get the version from youtube_dlc/version.py without importing the package
|
||||||
exec(compile(open('youtube_dlc/version.py').read(),
|
exec(compile(open('youtube_dlc/version.py').read(),
|
||||||
'youtube_dlc/version.py', 'exec'))
|
'youtube_dlc/version.py', 'exec'))
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTION = 'Command-line program to download videos from YouTube.com and many other other video platforms.'
|
DESCRIPTION = 'Command-line program to download videos from YouTube.com and many other other video platforms.'
|
||||||
|
|
||||||
LONG_DESCRIPTION = '\n\n'.join((
|
LONG_DESCRIPTION = '\n\n'.join((
|
||||||
@@ -18,6 +20,9 @@ LONG_DESCRIPTION = '\n\n'.join((
|
|||||||
'**PS**: Many links in this document will not work since this is a copy of the README.md from Github',
|
'**PS**: Many links in this document will not work since this is a copy of the README.md from Github',
|
||||||
open("README.md", "r", encoding="utf-8").read()))
|
open("README.md", "r", encoding="utf-8").read()))
|
||||||
|
|
||||||
|
REQUIREMENTS = ['mutagen', 'pycryptodome']
|
||||||
|
|
||||||
|
|
||||||
if len(sys.argv) >= 2 and sys.argv[1] == 'py2exe':
|
if len(sys.argv) >= 2 and sys.argv[1] == 'py2exe':
|
||||||
print("inv")
|
print("inv")
|
||||||
else:
|
else:
|
||||||
@@ -41,10 +46,8 @@ else:
|
|||||||
params = {
|
params = {
|
||||||
'data_files': data_files,
|
'data_files': data_files,
|
||||||
}
|
}
|
||||||
#if setuptools_available:
|
|
||||||
params['entry_points'] = {'console_scripts': ['youtube-dlc = youtube_dlc:main']}
|
params['entry_points'] = {'console_scripts': ['youtube-dlc = youtube_dlc:main']}
|
||||||
#else:
|
|
||||||
# params['scripts'] = ['bin/youtube-dlc']
|
|
||||||
|
|
||||||
class build_lazy_extractors(Command):
|
class build_lazy_extractors(Command):
|
||||||
description = 'Build the extractor lazy loading module'
|
description = 'Build the extractor lazy loading module'
|
||||||
@@ -62,6 +65,9 @@ class build_lazy_extractors(Command):
|
|||||||
dry_run=self.dry_run,
|
dry_run=self.dry_run,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
packages = find_packages(exclude=("youtube_dl", "test", "ytdlp_plugins"))
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="yt-dlp",
|
name="yt-dlp",
|
||||||
version=__version__,
|
version=__version__,
|
||||||
@@ -71,7 +77,8 @@ setup(
|
|||||||
long_description=LONG_DESCRIPTION,
|
long_description=LONG_DESCRIPTION,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
url="https://github.com/pukkandan/yt-dlp",
|
url="https://github.com/pukkandan/yt-dlp",
|
||||||
packages=find_packages(exclude=("youtube_dl","test",)),
|
packages=packages,
|
||||||
|
install_requires=REQUIREMENTS,
|
||||||
project_urls={
|
project_urls={
|
||||||
'Documentation': 'https://github.com/pukkandan/yt-dlp#yt-dlp',
|
'Documentation': 'https://github.com/pukkandan/yt-dlp#yt-dlp',
|
||||||
'Source': 'https://github.com/pukkandan/yt-dlp',
|
'Source': 'https://github.com/pukkandan/yt-dlp',
|
||||||
|
|||||||
@@ -8,10 +8,16 @@ import sys
|
|||||||
import unittest
|
import unittest
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from youtube_dlc.postprocessor import MetadataFromTitlePP
|
from youtube_dlc.postprocessor import MetadataFromFieldPP, MetadataFromTitlePP
|
||||||
|
|
||||||
|
|
||||||
|
class TestMetadataFromField(unittest.TestCase):
|
||||||
|
def test_format_to_regex(self):
|
||||||
|
pp = MetadataFromFieldPP(None, ['title:%(title)s - %(artist)s'])
|
||||||
|
self.assertEqual(pp._data[0]['regex'], r'(?P<title>[^\r\n]+)\ \-\ (?P<artist>[^\r\n]+)')
|
||||||
|
|
||||||
|
|
||||||
class TestMetadataFromTitle(unittest.TestCase):
|
class TestMetadataFromTitle(unittest.TestCase):
|
||||||
def test_format_to_regex(self):
|
def test_format_to_regex(self):
|
||||||
pp = MetadataFromTitlePP(None, '%(title)s - %(artist)s')
|
pp = MetadataFromTitlePP(None, '%(title)s - %(artist)s')
|
||||||
self.assertEqual(pp._titleregex, r'(?P<title>.+)\ \-\ (?P<artist>.+)')
|
self.assertEqual(pp._titleregex, r'(?P<title>[^\r\n]+)\ \-\ (?P<artist>[^\r\n]+)')
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ IGNORED_FILES = [
|
|||||||
'setup.py', # http://bugs.python.org/issue13943
|
'setup.py', # http://bugs.python.org/issue13943
|
||||||
'conf.py',
|
'conf.py',
|
||||||
'buildserver.py',
|
'buildserver.py',
|
||||||
'pyinst.py',
|
|
||||||
'pyinst32.py',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
IGNORED_DIRS = [
|
IGNORED_DIRS = [
|
||||||
|
|||||||
@@ -1,275 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
# Allow direct execution
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from test.helper import expect_value
|
|
||||||
from youtube_dlc.extractor import YoutubeIE
|
|
||||||
|
|
||||||
|
|
||||||
class TestYoutubeChapters(unittest.TestCase):
|
|
||||||
|
|
||||||
_TEST_CASES = [
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=A22oy8dFjqc
|
|
||||||
# pattern: 00:00 - <title>
|
|
||||||
'''This is the absolute ULTIMATE experience of Queen's set at LIVE AID, this is the best video mixed to the absolutely superior stereo radio broadcast. This vastly superior audio mix takes a huge dump on all of the official mixes. Best viewed in 1080p. ENJOY! ***MAKE SURE TO READ THE DESCRIPTION***<br /><a href="#" onclick="yt.www.watch.player.seekTo(00*60+36);return false;">00:36</a> - Bohemian Rhapsody<br /><a href="#" onclick="yt.www.watch.player.seekTo(02*60+42);return false;">02:42</a> - Radio Ga Ga<br /><a href="#" onclick="yt.www.watch.player.seekTo(06*60+53);return false;">06:53</a> - Ay Oh!<br /><a href="#" onclick="yt.www.watch.player.seekTo(07*60+34);return false;">07:34</a> - Hammer To Fall<br /><a href="#" onclick="yt.www.watch.player.seekTo(12*60+08);return false;">12:08</a> - Crazy Little Thing Called Love<br /><a href="#" onclick="yt.www.watch.player.seekTo(16*60+03);return false;">16:03</a> - We Will Rock You<br /><a href="#" onclick="yt.www.watch.player.seekTo(17*60+18);return false;">17:18</a> - We Are The Champions<br /><a href="#" onclick="yt.www.watch.player.seekTo(21*60+12);return false;">21:12</a> - Is This The World We Created...?<br /><br />Short song analysis:<br /><br />- "Bohemian Rhapsody": Although it's a short medley version, it's one of the best performances of the ballad section, with Freddie nailing the Bb4s with the correct studio phrasing (for the first time ever!).<br /><br />- "Radio Ga Ga": Although it's missing one chorus, this is one of - if not the best - the best versions ever, Freddie nails all the Bb4s and sounds very clean! Spike Edney's Roland Jupiter 8 also really shines through on this mix, compared to the DVD releases!<br /><br />- "Audience Improv": A great improv, Freddie sounds strong and confident. You gotta love when he sustains that A4 for 4 seconds!<br /><br />- "Hammer To Fall": Despite missing a verse and a chorus, it's a strong version (possibly the best ever). Freddie sings the song amazingly, and even ad-libs a C#5 and a C5! Also notice how heavy Brian's guitar sounds compared to the thin DVD mixes - it roars!<br /><br />- "Crazy Little Thing Called Love": A great version, the crowd loves the song, the jam is great as well! Only downside to this is the slight feedback issues.<br /><br />- "We Will Rock You": Although cut down to the 1st verse and chorus, Freddie sounds strong. He nails the A4, and the solo from Dr. May is brilliant!<br /><br />- "We Are the Champions": Perhaps the high-light of the performance - Freddie is very daring on this version, he sustains the pre-chorus Bb4s, nails the 1st C5, belts great A4s, but most importantly: He nails the chorus Bb4s, in all 3 choruses! This is the only time he has ever done so! It has to be said though, the last one sounds a bit rough, but that's a side effect of belting high notes for the past 18 minutes, with nodules AND laryngitis!<br /><br />- "Is This The World We Created... ?": Freddie and Brian perform a beautiful version of this, and it is one of the best versions ever. It's both sad and hilarious that a couple of BBC engineers are talking over the song, one of them being completely oblivious of the fact that he is interrupting the performance, on live television... Which was being televised to almost 2 billion homes.<br /><br /><br />All rights go to their respective owners!<br />-----Copyright Disclaimer Under Section 107 of the Copyright Act 1976, allowance is made for fair use for purposes such as criticism, comment, news reporting, teaching, scholarship, and research. Fair use is a use permitted by copyright statute that might otherwise be infringing. Non-profit, educational or personal use tips the balance in favor of fair use''',
|
|
||||||
1477,
|
|
||||||
[{
|
|
||||||
'start_time': 36,
|
|
||||||
'end_time': 162,
|
|
||||||
'title': 'Bohemian Rhapsody',
|
|
||||||
}, {
|
|
||||||
'start_time': 162,
|
|
||||||
'end_time': 413,
|
|
||||||
'title': 'Radio Ga Ga',
|
|
||||||
}, {
|
|
||||||
'start_time': 413,
|
|
||||||
'end_time': 454,
|
|
||||||
'title': 'Ay Oh!',
|
|
||||||
}, {
|
|
||||||
'start_time': 454,
|
|
||||||
'end_time': 728,
|
|
||||||
'title': 'Hammer To Fall',
|
|
||||||
}, {
|
|
||||||
'start_time': 728,
|
|
||||||
'end_time': 963,
|
|
||||||
'title': 'Crazy Little Thing Called Love',
|
|
||||||
}, {
|
|
||||||
'start_time': 963,
|
|
||||||
'end_time': 1038,
|
|
||||||
'title': 'We Will Rock You',
|
|
||||||
}, {
|
|
||||||
'start_time': 1038,
|
|
||||||
'end_time': 1272,
|
|
||||||
'title': 'We Are The Champions',
|
|
||||||
}, {
|
|
||||||
'start_time': 1272,
|
|
||||||
'end_time': 1477,
|
|
||||||
'title': 'Is This The World We Created...?',
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=ekYlRhALiRQ
|
|
||||||
# pattern: <num>. <title> 0:00
|
|
||||||
'1. Those Beaten Paths of Confusion <a href="#" onclick="yt.www.watch.player.seekTo(0*60+00);return false;">0:00</a><br />2. Beyond the Shadows of Emptiness & Nothingness <a href="#" onclick="yt.www.watch.player.seekTo(11*60+47);return false;">11:47</a><br />3. Poison Yourself...With Thought <a href="#" onclick="yt.www.watch.player.seekTo(26*60+30);return false;">26:30</a><br />4. The Agents of Transformation <a href="#" onclick="yt.www.watch.player.seekTo(35*60+57);return false;">35:57</a><br />5. Drowning in the Pain of Consciousness <a href="#" onclick="yt.www.watch.player.seekTo(44*60+32);return false;">44:32</a><br />6. Deny the Disease of Life <a href="#" onclick="yt.www.watch.player.seekTo(53*60+07);return false;">53:07</a><br /><br />More info/Buy: http://crepusculonegro.storenvy.com/products/257645-cn-03-arizmenda-within-the-vacuum-of-infinity<br /><br />No copyright is intended. The rights to this video are assumed by the owner and its affiliates.',
|
|
||||||
4009,
|
|
||||||
[{
|
|
||||||
'start_time': 0,
|
|
||||||
'end_time': 707,
|
|
||||||
'title': '1. Those Beaten Paths of Confusion',
|
|
||||||
}, {
|
|
||||||
'start_time': 707,
|
|
||||||
'end_time': 1590,
|
|
||||||
'title': '2. Beyond the Shadows of Emptiness & Nothingness',
|
|
||||||
}, {
|
|
||||||
'start_time': 1590,
|
|
||||||
'end_time': 2157,
|
|
||||||
'title': '3. Poison Yourself...With Thought',
|
|
||||||
}, {
|
|
||||||
'start_time': 2157,
|
|
||||||
'end_time': 2672,
|
|
||||||
'title': '4. The Agents of Transformation',
|
|
||||||
}, {
|
|
||||||
'start_time': 2672,
|
|
||||||
'end_time': 3187,
|
|
||||||
'title': '5. Drowning in the Pain of Consciousness',
|
|
||||||
}, {
|
|
||||||
'start_time': 3187,
|
|
||||||
'end_time': 4009,
|
|
||||||
'title': '6. Deny the Disease of Life',
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=WjL4pSzog9w
|
|
||||||
# pattern: 00:00 <title>
|
|
||||||
'<a href="https://arizmenda.bandcamp.com/merch/despairs-depths-descended-cd" class="yt-uix-servicelink " data-target-new-window="True" data-servicelink="CDAQ6TgYACITCNf1raqT2dMCFdRjGAod_o0CBSj4HQ" data-url="https://arizmenda.bandcamp.com/merch/despairs-depths-descended-cd" rel="nofollow noopener" target="_blank">https://arizmenda.bandcamp.com/merch/...</a><br /><br /><a href="#" onclick="yt.www.watch.player.seekTo(00*60+00);return false;">00:00</a> Christening Unborn Deformities <br /><a href="#" onclick="yt.www.watch.player.seekTo(07*60+08);return false;">07:08</a> Taste of Purity<br /><a href="#" onclick="yt.www.watch.player.seekTo(16*60+16);return false;">16:16</a> Sculpting Sins of a Universal Tongue<br /><a href="#" onclick="yt.www.watch.player.seekTo(24*60+45);return false;">24:45</a> Birth<br /><a href="#" onclick="yt.www.watch.player.seekTo(31*60+24);return false;">31:24</a> Neves<br /><a href="#" onclick="yt.www.watch.player.seekTo(37*60+55);return false;">37:55</a> Libations in Limbo',
|
|
||||||
2705,
|
|
||||||
[{
|
|
||||||
'start_time': 0,
|
|
||||||
'end_time': 428,
|
|
||||||
'title': 'Christening Unborn Deformities',
|
|
||||||
}, {
|
|
||||||
'start_time': 428,
|
|
||||||
'end_time': 976,
|
|
||||||
'title': 'Taste of Purity',
|
|
||||||
}, {
|
|
||||||
'start_time': 976,
|
|
||||||
'end_time': 1485,
|
|
||||||
'title': 'Sculpting Sins of a Universal Tongue',
|
|
||||||
}, {
|
|
||||||
'start_time': 1485,
|
|
||||||
'end_time': 1884,
|
|
||||||
'title': 'Birth',
|
|
||||||
}, {
|
|
||||||
'start_time': 1884,
|
|
||||||
'end_time': 2275,
|
|
||||||
'title': 'Neves',
|
|
||||||
}, {
|
|
||||||
'start_time': 2275,
|
|
||||||
'end_time': 2705,
|
|
||||||
'title': 'Libations in Limbo',
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=o3r1sn-t3is
|
|
||||||
# pattern: <title> 00:00 <note>
|
|
||||||
'Download this show in MP3: <a href="http://sh.st/njZKK" class="yt-uix-servicelink " data-url="http://sh.st/njZKK" data-target-new-window="True" data-servicelink="CDAQ6TgYACITCK3j8_6o2dMCFVDCGAoduVAKKij4HQ" rel="nofollow noopener" target="_blank">http://sh.st/njZKK</a><br /><br />Setlist:<br />I-E-A-I-A-I-O <a href="#" onclick="yt.www.watch.player.seekTo(00*60+45);return false;">00:45</a><br />Suite-Pee <a href="#" onclick="yt.www.watch.player.seekTo(4*60+26);return false;">4:26</a> (Incomplete)<br />Attack <a href="#" onclick="yt.www.watch.player.seekTo(5*60+31);return false;">5:31</a> (First live performance since 2011)<br />Prison Song <a href="#" onclick="yt.www.watch.player.seekTo(8*60+42);return false;">8:42</a><br />Know <a href="#" onclick="yt.www.watch.player.seekTo(12*60+32);return false;">12:32</a> (First live performance since 2011)<br />Aerials <a href="#" onclick="yt.www.watch.player.seekTo(15*60+32);return false;">15:32</a><br />Soldier Side - Intro <a href="#" onclick="yt.www.watch.player.seekTo(19*60+13);return false;">19:13</a><br />B.Y.O.B. <a href="#" onclick="yt.www.watch.player.seekTo(20*60+09);return false;">20:09</a><br />Soil <a href="#" onclick="yt.www.watch.player.seekTo(24*60+32);return false;">24:32</a><br />Darts <a href="#" onclick="yt.www.watch.player.seekTo(27*60+48);return false;">27:48</a><br />Radio/Video <a href="#" onclick="yt.www.watch.player.seekTo(30*60+38);return false;">30:38</a><br />Hypnotize <a href="#" onclick="yt.www.watch.player.seekTo(35*60+05);return false;">35:05</a><br />Temper <a href="#" onclick="yt.www.watch.player.seekTo(38*60+08);return false;">38:08</a> (First live performance since 1999)<br />CUBErt <a href="#" onclick="yt.www.watch.player.seekTo(41*60+00);return false;">41:00</a><br />Needles <a href="#" onclick="yt.www.watch.player.seekTo(42*60+57);return false;">42:57</a><br />Deer Dance <a href="#" onclick="yt.www.watch.player.seekTo(46*60+27);return false;">46:27</a><br />Bounce <a href="#" onclick="yt.www.watch.player.seekTo(49*60+38);return false;">49:38</a><br />Suggestions <a href="#" onclick="yt.www.watch.player.seekTo(51*60+25);return false;">51:25</a><br />Psycho <a href="#" onclick="yt.www.watch.player.seekTo(53*60+52);return false;">53:52</a><br />Chop Suey! <a href="#" onclick="yt.www.watch.player.seekTo(58*60+13);return false;">58:13</a><br />Lonely Day <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+01*60+15);return false;">1:01:15</a><br />Question! <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+04*60+14);return false;">1:04:14</a><br />Lost in Hollywood <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+08*60+10);return false;">1:08:10</a><br />Vicinity of Obscenity <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+13*60+40);return false;">1:13:40</a>(First live performance since 2012)<br />Forest <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+16*60+17);return false;">1:16:17</a><br />Cigaro <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+20*60+02);return false;">1:20:02</a><br />Toxicity <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+23*60+57);return false;">1:23:57</a>(with Chino Moreno)<br />Sugar <a href="#" onclick="yt.www.watch.player.seekTo(1*3600+27*60+53);return false;">1:27:53</a>',
|
|
||||||
5640,
|
|
||||||
[{
|
|
||||||
'start_time': 45,
|
|
||||||
'end_time': 266,
|
|
||||||
'title': 'I-E-A-I-A-I-O',
|
|
||||||
}, {
|
|
||||||
'start_time': 266,
|
|
||||||
'end_time': 331,
|
|
||||||
'title': 'Suite-Pee (Incomplete)',
|
|
||||||
}, {
|
|
||||||
'start_time': 331,
|
|
||||||
'end_time': 522,
|
|
||||||
'title': 'Attack (First live performance since 2011)',
|
|
||||||
}, {
|
|
||||||
'start_time': 522,
|
|
||||||
'end_time': 752,
|
|
||||||
'title': 'Prison Song',
|
|
||||||
}, {
|
|
||||||
'start_time': 752,
|
|
||||||
'end_time': 932,
|
|
||||||
'title': 'Know (First live performance since 2011)',
|
|
||||||
}, {
|
|
||||||
'start_time': 932,
|
|
||||||
'end_time': 1153,
|
|
||||||
'title': 'Aerials',
|
|
||||||
}, {
|
|
||||||
'start_time': 1153,
|
|
||||||
'end_time': 1209,
|
|
||||||
'title': 'Soldier Side - Intro',
|
|
||||||
}, {
|
|
||||||
'start_time': 1209,
|
|
||||||
'end_time': 1472,
|
|
||||||
'title': 'B.Y.O.B.',
|
|
||||||
}, {
|
|
||||||
'start_time': 1472,
|
|
||||||
'end_time': 1668,
|
|
||||||
'title': 'Soil',
|
|
||||||
}, {
|
|
||||||
'start_time': 1668,
|
|
||||||
'end_time': 1838,
|
|
||||||
'title': 'Darts',
|
|
||||||
}, {
|
|
||||||
'start_time': 1838,
|
|
||||||
'end_time': 2105,
|
|
||||||
'title': 'Radio/Video',
|
|
||||||
}, {
|
|
||||||
'start_time': 2105,
|
|
||||||
'end_time': 2288,
|
|
||||||
'title': 'Hypnotize',
|
|
||||||
}, {
|
|
||||||
'start_time': 2288,
|
|
||||||
'end_time': 2460,
|
|
||||||
'title': 'Temper (First live performance since 1999)',
|
|
||||||
}, {
|
|
||||||
'start_time': 2460,
|
|
||||||
'end_time': 2577,
|
|
||||||
'title': 'CUBErt',
|
|
||||||
}, {
|
|
||||||
'start_time': 2577,
|
|
||||||
'end_time': 2787,
|
|
||||||
'title': 'Needles',
|
|
||||||
}, {
|
|
||||||
'start_time': 2787,
|
|
||||||
'end_time': 2978,
|
|
||||||
'title': 'Deer Dance',
|
|
||||||
}, {
|
|
||||||
'start_time': 2978,
|
|
||||||
'end_time': 3085,
|
|
||||||
'title': 'Bounce',
|
|
||||||
}, {
|
|
||||||
'start_time': 3085,
|
|
||||||
'end_time': 3232,
|
|
||||||
'title': 'Suggestions',
|
|
||||||
}, {
|
|
||||||
'start_time': 3232,
|
|
||||||
'end_time': 3493,
|
|
||||||
'title': 'Psycho',
|
|
||||||
}, {
|
|
||||||
'start_time': 3493,
|
|
||||||
'end_time': 3675,
|
|
||||||
'title': 'Chop Suey!',
|
|
||||||
}, {
|
|
||||||
'start_time': 3675,
|
|
||||||
'end_time': 3854,
|
|
||||||
'title': 'Lonely Day',
|
|
||||||
}, {
|
|
||||||
'start_time': 3854,
|
|
||||||
'end_time': 4090,
|
|
||||||
'title': 'Question!',
|
|
||||||
}, {
|
|
||||||
'start_time': 4090,
|
|
||||||
'end_time': 4420,
|
|
||||||
'title': 'Lost in Hollywood',
|
|
||||||
}, {
|
|
||||||
'start_time': 4420,
|
|
||||||
'end_time': 4577,
|
|
||||||
'title': 'Vicinity of Obscenity (First live performance since 2012)',
|
|
||||||
}, {
|
|
||||||
'start_time': 4577,
|
|
||||||
'end_time': 4802,
|
|
||||||
'title': 'Forest',
|
|
||||||
}, {
|
|
||||||
'start_time': 4802,
|
|
||||||
'end_time': 5037,
|
|
||||||
'title': 'Cigaro',
|
|
||||||
}, {
|
|
||||||
'start_time': 5037,
|
|
||||||
'end_time': 5273,
|
|
||||||
'title': 'Toxicity (with Chino Moreno)',
|
|
||||||
}, {
|
|
||||||
'start_time': 5273,
|
|
||||||
'end_time': 5640,
|
|
||||||
'title': 'Sugar',
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=PkYLQbsqCE8
|
|
||||||
# pattern: <num> - <title> [<latinized title>] 0:00:00
|
|
||||||
'''Затемно (Zatemno) is an Obscure Black Metal Band from Russia.<br /><br />"Во прах (Vo prakh)'' Into The Ashes", Debut mini-album released may 6, 2016, by Death Knell Productions<br />Released on 6 panel digipak CD, limited to 100 copies only<br />And digital format on Bandcamp<br /><br />Tracklist<br /><br />1 - Во прах [Vo prakh] <a href="#" onclick="yt.www.watch.player.seekTo(0*3600+00*60+00);return false;">0:00:00</a><br />2 - Искупление [Iskupleniye] <a href="#" onclick="yt.www.watch.player.seekTo(0*3600+08*60+10);return false;">0:08:10</a><br />3 - Из серпов луны...[Iz serpov luny] <a href="#" onclick="yt.www.watch.player.seekTo(0*3600+14*60+30);return false;">0:14:30</a><br /><br />Links:<br /><a href="https://deathknellprod.bandcamp.com/album/--2" class="yt-uix-servicelink " data-target-new-window="True" data-url="https://deathknellprod.bandcamp.com/album/--2" data-servicelink="CC8Q6TgYACITCNP234Kr2dMCFcNxGAodQqsIwSj4HQ" target="_blank" rel="nofollow noopener">https://deathknellprod.bandcamp.com/a...</a><br /><a href="https://www.facebook.com/DeathKnellProd/" class="yt-uix-servicelink " data-target-new-window="True" data-url="https://www.facebook.com/DeathKnellProd/" data-servicelink="CC8Q6TgYACITCNP234Kr2dMCFcNxGAodQqsIwSj4HQ" target="_blank" rel="nofollow noopener">https://www.facebook.com/DeathKnellProd/</a><br /><br /><br />I don't have any right about this artifact, my only intention is to spread the music of the band, all rights are reserved to the Затемно (Zatemno) and his producers, Death Knell Productions.<br /><br />------------------------------------------------------------------<br /><br />Subscribe for more videos like this.<br />My link: <a href="https://web.facebook.com/AttackOfTheDragons" class="yt-uix-servicelink " data-target-new-window="True" data-url="https://web.facebook.com/AttackOfTheDragons" data-servicelink="CC8Q6TgYACITCNP234Kr2dMCFcNxGAodQqsIwSj4HQ" target="_blank" rel="nofollow noopener">https://web.facebook.com/AttackOfTheD...</a>''',
|
|
||||||
1138,
|
|
||||||
[{
|
|
||||||
'start_time': 0,
|
|
||||||
'end_time': 490,
|
|
||||||
'title': '1 - Во прах [Vo prakh]',
|
|
||||||
}, {
|
|
||||||
'start_time': 490,
|
|
||||||
'end_time': 870,
|
|
||||||
'title': '2 - Искупление [Iskupleniye]',
|
|
||||||
}, {
|
|
||||||
'start_time': 870,
|
|
||||||
'end_time': 1138,
|
|
||||||
'title': '3 - Из серпов луны...[Iz serpov luny]',
|
|
||||||
}]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# https://www.youtube.com/watch?v=xZW70zEasOk
|
|
||||||
# time point more than duration
|
|
||||||
'''● LCS Spring finals: Saturday and Sunday from <a href="#" onclick="yt.www.watch.player.seekTo(13*60+30);return false;">13:30</a> outside the venue! <br />● PAX East: Fri, Sat & Sun - more info in tomorrows video on the main channel!''',
|
|
||||||
283,
|
|
||||||
[]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
def test_youtube_chapters(self):
|
|
||||||
for description, duration, expected_chapters in self._TEST_CASES:
|
|
||||||
ie = YoutubeIE()
|
|
||||||
expect_value(
|
|
||||||
self, ie._extract_chapters_from_description(description, duration),
|
|
||||||
expected_chapters, None)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
@@ -12,6 +12,7 @@ from test.helper import FakeYDL
|
|||||||
|
|
||||||
from youtube_dlc.extractor import (
|
from youtube_dlc.extractor import (
|
||||||
YoutubePlaylistIE,
|
YoutubePlaylistIE,
|
||||||
|
YoutubeTabIE,
|
||||||
YoutubeIE,
|
YoutubeIE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,14 +58,22 @@ class TestYoutubeLists(unittest.TestCase):
|
|||||||
entries = result['entries']
|
entries = result['entries']
|
||||||
self.assertEqual(len(entries), 100)
|
self.assertEqual(len(entries), 100)
|
||||||
|
|
||||||
def test_youtube_flat_playlist_titles(self):
|
def test_youtube_flat_playlist_extraction(self):
|
||||||
dl = FakeYDL()
|
dl = FakeYDL()
|
||||||
dl.params['extract_flat'] = True
|
dl.params['extract_flat'] = True
|
||||||
ie = YoutubePlaylistIE(dl)
|
ie = YoutubeTabIE(dl)
|
||||||
result = ie.extract('https://www.youtube.com/playlist?list=PL-KKIb8rvtMSrAO9YFbeM6UQrAqoFTUWv')
|
result = ie.extract('https://www.youtube.com/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc')
|
||||||
self.assertIsPlaylist(result)
|
self.assertIsPlaylist(result)
|
||||||
for entry in result['entries']:
|
entries = list(result['entries'])
|
||||||
self.assertTrue(entry.get('title'))
|
self.assertTrue(len(entries) == 1)
|
||||||
|
video = entries[0]
|
||||||
|
self.assertEqual(video['_type'], 'url_transparent')
|
||||||
|
self.assertEqual(video['ie_key'], 'Youtube')
|
||||||
|
self.assertEqual(video['id'], 'BaW_jenozKc')
|
||||||
|
self.assertEqual(video['url'], 'BaW_jenozKc')
|
||||||
|
self.assertEqual(video['title'], 'youtube-dl test video "\'/\\ä↭𝕐')
|
||||||
|
self.assertEqual(video['duration'], 10)
|
||||||
|
self.assertEqual(video['uploader'], 'Philipp Hagemeister')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -19,55 +19,46 @@ from youtube_dlc.compat import compat_str, compat_urlretrieve
|
|||||||
_TESTS = [
|
_TESTS = [
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-vflHOr_nV.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-vflHOr_nV.js',
|
||||||
'js',
|
|
||||||
86,
|
86,
|
||||||
'>=<;:/.-[+*)(\'&%$#"!ZYX0VUTSRQPONMLKJIHGFEDCBA\\yxwvutsrqponmlkjihgfedcba987654321',
|
'>=<;:/.-[+*)(\'&%$#"!ZYX0VUTSRQPONMLKJIHGFEDCBA\\yxwvutsrqponmlkjihgfedcba987654321',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-vfldJ8xgI.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-vfldJ8xgI.js',
|
||||||
'js',
|
|
||||||
85,
|
85,
|
||||||
'3456789a0cdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRS[UVWXYZ!"#$%&\'()*+,-./:;<=>?@',
|
'3456789a0cdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRS[UVWXYZ!"#$%&\'()*+,-./:;<=>?@',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-vfle-mVwz.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-vfle-mVwz.js',
|
||||||
'js',
|
|
||||||
90,
|
90,
|
||||||
']\\[@?>=<;:/.-,+*)(\'&%$#"hZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjiagfedcb39876',
|
']\\[@?>=<;:/.-,+*)(\'&%$#"hZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjiagfedcb39876',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vfl0Cbn9e.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vfl0Cbn9e.js',
|
||||||
'js',
|
|
||||||
84,
|
84,
|
||||||
'O1I3456789abcde0ghijklmnopqrstuvwxyzABCDEFGHfJKLMN2PQRSTUVW@YZ!"#$%&\'()*+,-./:;<=',
|
'O1I3456789abcde0ghijklmnopqrstuvwxyzABCDEFGHfJKLMN2PQRSTUVW@YZ!"#$%&\'()*+,-./:;<=',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflXGBaUN.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflXGBaUN.js',
|
||||||
'js',
|
|
||||||
'2ACFC7A61CA478CD21425E5A57EBD73DDC78E22A.2094302436B2D377D14A3BBA23022D023B8BC25AA',
|
'2ACFC7A61CA478CD21425E5A57EBD73DDC78E22A.2094302436B2D377D14A3BBA23022D023B8BC25AA',
|
||||||
'A52CB8B320D22032ABB3A41D773D2B6342034902.A22E87CDD37DBE75A5E52412DC874AC16A7CFCA2',
|
'A52CB8B320D22032ABB3A41D773D2B6342034902.A22E87CDD37DBE75A5E52412DC874AC16A7CFCA2',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflBb0OQx.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflBb0OQx.js',
|
||||||
'js',
|
|
||||||
84,
|
84,
|
||||||
'123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQ0STUVWXYZ!"#$%&\'()*+,@./:;<=>'
|
'123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQ0STUVWXYZ!"#$%&\'()*+,@./:;<=>'
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vfl9FYC6l.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vfl9FYC6l.js',
|
||||||
'js',
|
|
||||||
83,
|
83,
|
||||||
'123456789abcdefghijklmnopqr0tuvwxyzABCDETGHIJKLMNOPQRS>UVWXYZ!"#$%&\'()*+,-./:;<=F'
|
'123456789abcdefghijklmnopqr0tuvwxyzABCDETGHIJKLMNOPQRS>UVWXYZ!"#$%&\'()*+,-./:;<=F'
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflCGk6yw/html5player.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflCGk6yw/html5player.js',
|
||||||
'js',
|
|
||||||
'4646B5181C6C3020DF1D9C7FCFEA.AD80ABF70C39BD369CCCAE780AFBB98FA6B6CB42766249D9488C288',
|
'4646B5181C6C3020DF1D9C7FCFEA.AD80ABF70C39BD369CCCAE780AFBB98FA6B6CB42766249D9488C288',
|
||||||
'82C8849D94266724DC6B6AF89BBFA087EACCD963.B93C07FBA084ACAEFCF7C9D1FD0203C6C1815B6B'
|
'82C8849D94266724DC6B6AF89BBFA087EACCD963.B93C07FBA084ACAEFCF7C9D1FD0203C6C1815B6B'
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflKjOTVq/html5player.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflKjOTVq/html5player.js',
|
||||||
'js',
|
|
||||||
'312AA52209E3623129A412D56A40F11CB0AF14AE.3EE09501CB14E3BCDC3B2AE808BF3F1D14E7FBF12',
|
'312AA52209E3623129A412D56A40F11CB0AF14AE.3EE09501CB14E3BCDC3B2AE808BF3F1D14E7FBF12',
|
||||||
'112AA5220913623229A412D56A40F11CB0AF14AE.3EE0950FCB14EEBCDC3B2AE808BF331D14E7FBF3',
|
'112AA5220913623229A412D56A40F11CB0AF14AE.3EE0950FCB14EEBCDC3B2AE808BF331D14E7FBF3',
|
||||||
)
|
)
|
||||||
@@ -78,6 +69,10 @@ class TestPlayerInfo(unittest.TestCase):
|
|||||||
def test_youtube_extract_player_info(self):
|
def test_youtube_extract_player_info(self):
|
||||||
PLAYER_URLS = (
|
PLAYER_URLS = (
|
||||||
('https://www.youtube.com/s/player/64dddad9/player_ias.vflset/en_US/base.js', '64dddad9'),
|
('https://www.youtube.com/s/player/64dddad9/player_ias.vflset/en_US/base.js', '64dddad9'),
|
||||||
|
('https://www.youtube.com/s/player/64dddad9/player_ias.vflset/fr_FR/base.js', '64dddad9'),
|
||||||
|
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-phone-en_US.vflset/base.js', '64dddad9'),
|
||||||
|
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-phone-de_DE.vflset/base.js', '64dddad9'),
|
||||||
|
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-tablet-en_US.vflset/base.js', '64dddad9'),
|
||||||
# obsolete
|
# obsolete
|
||||||
('https://www.youtube.com/yts/jsbin/player_ias-vfle4-e03/en_US/base.js', 'vfle4-e03'),
|
('https://www.youtube.com/yts/jsbin/player_ias-vfle4-e03/en_US/base.js', 'vfle4-e03'),
|
||||||
('https://www.youtube.com/yts/jsbin/player_ias-vfl49f_g4/en_US/base.js', 'vfl49f_g4'),
|
('https://www.youtube.com/yts/jsbin/player_ias-vfl49f_g4/en_US/base.js', 'vfl49f_g4'),
|
||||||
@@ -86,13 +81,9 @@ class TestPlayerInfo(unittest.TestCase):
|
|||||||
('https://www.youtube.com/yts/jsbin/player-en_US-vflaxXRn1/base.js', 'vflaxXRn1'),
|
('https://www.youtube.com/yts/jsbin/player-en_US-vflaxXRn1/base.js', 'vflaxXRn1'),
|
||||||
('https://s.ytimg.com/yts/jsbin/html5player-en_US-vflXGBaUN.js', 'vflXGBaUN'),
|
('https://s.ytimg.com/yts/jsbin/html5player-en_US-vflXGBaUN.js', 'vflXGBaUN'),
|
||||||
('https://s.ytimg.com/yts/jsbin/html5player-en_US-vflKjOTVq/html5player.js', 'vflKjOTVq'),
|
('https://s.ytimg.com/yts/jsbin/html5player-en_US-vflKjOTVq/html5player.js', 'vflKjOTVq'),
|
||||||
('http://s.ytimg.com/yt/swfbin/watch_as3-vflrEm9Nq.swf', 'vflrEm9Nq'),
|
|
||||||
('https://s.ytimg.com/yts/swfbin/player-vflenCdZL/watch_as3.swf', 'vflenCdZL'),
|
|
||||||
)
|
)
|
||||||
for player_url, expected_player_id in PLAYER_URLS:
|
for player_url, expected_player_id in PLAYER_URLS:
|
||||||
expected_player_type = player_url.split('.')[-1]
|
player_id = YoutubeIE._extract_player_info(player_url)
|
||||||
player_type, player_id = YoutubeIE._extract_player_info(player_url)
|
|
||||||
self.assertEqual(player_type, expected_player_type)
|
|
||||||
self.assertEqual(player_id, expected_player_id)
|
self.assertEqual(player_id, expected_player_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -104,13 +95,13 @@ class TestSignature(unittest.TestCase):
|
|||||||
os.mkdir(self.TESTDATA_DIR)
|
os.mkdir(self.TESTDATA_DIR)
|
||||||
|
|
||||||
|
|
||||||
def make_tfunc(url, stype, sig_input, expected_sig):
|
def make_tfunc(url, sig_input, expected_sig):
|
||||||
m = re.match(r'.*-([a-zA-Z0-9_-]+)(?:/watch_as3|/html5player)?\.[a-z]+$', url)
|
m = re.match(r'.*-([a-zA-Z0-9_-]+)(?:/watch_as3|/html5player)?\.[a-z]+$', url)
|
||||||
assert m, '%r should follow URL format' % url
|
assert m, '%r should follow URL format' % url
|
||||||
test_id = m.group(1)
|
test_id = m.group(1)
|
||||||
|
|
||||||
def test_func(self):
|
def test_func(self):
|
||||||
basename = 'player-%s.%s' % (test_id, stype)
|
basename = 'player-%s.js' % test_id
|
||||||
fn = os.path.join(self.TESTDATA_DIR, basename)
|
fn = os.path.join(self.TESTDATA_DIR, basename)
|
||||||
|
|
||||||
if not os.path.exists(fn):
|
if not os.path.exists(fn):
|
||||||
@@ -118,22 +109,16 @@ def make_tfunc(url, stype, sig_input, expected_sig):
|
|||||||
|
|
||||||
ydl = FakeYDL()
|
ydl = FakeYDL()
|
||||||
ie = YoutubeIE(ydl)
|
ie = YoutubeIE(ydl)
|
||||||
if stype == 'js':
|
|
||||||
with io.open(fn, encoding='utf-8') as testf:
|
with io.open(fn, encoding='utf-8') as testf:
|
||||||
jscode = testf.read()
|
jscode = testf.read()
|
||||||
func = ie._parse_sig_js(jscode)
|
func = ie._parse_sig_js(jscode)
|
||||||
else:
|
|
||||||
assert stype == 'swf'
|
|
||||||
with open(fn, 'rb') as testf:
|
|
||||||
swfcode = testf.read()
|
|
||||||
func = ie._parse_sig_swf(swfcode)
|
|
||||||
src_sig = (
|
src_sig = (
|
||||||
compat_str(string.printable[:sig_input])
|
compat_str(string.printable[:sig_input])
|
||||||
if isinstance(sig_input, int) else sig_input)
|
if isinstance(sig_input, int) else sig_input)
|
||||||
got_sig = func(src_sig)
|
got_sig = func(src_sig)
|
||||||
self.assertEqual(got_sig, expected_sig)
|
self.assertEqual(got_sig, expected_sig)
|
||||||
|
|
||||||
test_func.__name__ = str('test_signature_' + stype + '_' + test_id)
|
test_func.__name__ = str('test_signature_js_' + test_id)
|
||||||
setattr(TestSignature, test_func.__name__, test_func)
|
setattr(TestSignature, test_func.__name__, test_func)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
45
win/ver.txt
45
win/ver.txt
@@ -1,45 +0,0 @@
|
|||||||
# UTF-8
|
|
||||||
#
|
|
||||||
# For more details about fixed file info 'ffi' see:
|
|
||||||
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
|
|
||||||
VSVersionInfo(
|
|
||||||
ffi=FixedFileInfo(
|
|
||||||
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
|
|
||||||
# Set not needed items to zero 0.
|
|
||||||
filevers=(16, 9, 2020, 0),
|
|
||||||
prodvers=(16, 9, 2020, 0),
|
|
||||||
# Contains a bitmask that specifies the valid bits 'flags'r
|
|
||||||
mask=0x3f,
|
|
||||||
# Contains a bitmask that specifies the Boolean attributes of the file.
|
|
||||||
flags=0x0,
|
|
||||||
# The operating system for which this file was designed.
|
|
||||||
# 0x4 - NT and there is no need to change it.
|
|
||||||
# OS=0x40004,
|
|
||||||
OS=0x4,
|
|
||||||
# The general type of file.
|
|
||||||
# 0x1 - the file is an application.
|
|
||||||
fileType=0x1,
|
|
||||||
# The function of the file.
|
|
||||||
# 0x0 - the function is not defined for this fileType
|
|
||||||
subtype=0x0,
|
|
||||||
# Creation date and time stamp.
|
|
||||||
date=(0, 0)
|
|
||||||
),
|
|
||||||
kids=[
|
|
||||||
StringFileInfo(
|
|
||||||
[
|
|
||||||
StringTable(
|
|
||||||
u'040904B0',
|
|
||||||
[StringStruct(u'Comments', u'Youtube-dlc Command Line Interface.'),
|
|
||||||
StringStruct(u'CompanyName', u'theidel@uni-bremen.de'),
|
|
||||||
StringStruct(u'FileDescription', u'Media Downloader'),
|
|
||||||
StringStruct(u'FileVersion', u'16.9.2020.0'),
|
|
||||||
StringStruct(u'InternalName', u'youtube-dlc'),
|
|
||||||
StringStruct(u'LegalCopyright', u'theidel@uni-bremen.de | UNLICENSE'),
|
|
||||||
StringStruct(u'OriginalFilename', u'youtube-dlc.exe'),
|
|
||||||
StringStruct(u'ProductName', u'Youtube-dlc'),
|
|
||||||
StringStruct(u'ProductVersion', u'16.9.2020.0 | git.io/JUGsM')])
|
|
||||||
]),
|
|
||||||
VarFileInfo([VarStruct(u'Translation', [0, 1200])])
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@@ -27,6 +27,7 @@ import traceback
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from string import ascii_letters
|
from string import ascii_letters
|
||||||
|
from zipimport import zipimporter
|
||||||
|
|
||||||
from .compat import (
|
from .compat import (
|
||||||
compat_basestring,
|
compat_basestring,
|
||||||
@@ -49,6 +50,7 @@ from .utils import (
|
|||||||
date_from_str,
|
date_from_str,
|
||||||
DateRange,
|
DateRange,
|
||||||
DEFAULT_OUTTMPL,
|
DEFAULT_OUTTMPL,
|
||||||
|
OUTTMPL_TYPES,
|
||||||
determine_ext,
|
determine_ext,
|
||||||
determine_protocol,
|
determine_protocol,
|
||||||
DOT_DESKTOP_LINK_TEMPLATE,
|
DOT_DESKTOP_LINK_TEMPLATE,
|
||||||
@@ -61,6 +63,7 @@ from .utils import (
|
|||||||
ExistingVideoReached,
|
ExistingVideoReached,
|
||||||
expand_path,
|
expand_path,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
float_or_none,
|
||||||
format_bytes,
|
format_bytes,
|
||||||
format_field,
|
format_field,
|
||||||
formatSeconds,
|
formatSeconds,
|
||||||
@@ -91,6 +94,7 @@ from .utils import (
|
|||||||
sanitized_Request,
|
sanitized_Request,
|
||||||
std_headers,
|
std_headers,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
|
strftime_or_none,
|
||||||
subtitles_filename,
|
subtitles_filename,
|
||||||
to_high_limit_path,
|
to_high_limit_path,
|
||||||
UnavailableVideoError,
|
UnavailableVideoError,
|
||||||
@@ -172,18 +176,28 @@ class YoutubeDL(object):
|
|||||||
forcejson: Force printing info_dict as JSON.
|
forcejson: Force printing info_dict as JSON.
|
||||||
dump_single_json: Force printing the info_dict of the whole playlist
|
dump_single_json: Force printing the info_dict of the whole playlist
|
||||||
(or video) as a single JSON line.
|
(or video) as a single JSON line.
|
||||||
force_write_download_archive: Force writing download archive regardless of
|
force_write_download_archive: Force writing download archive regardless
|
||||||
'skip_download' or 'simulate'.
|
of 'skip_download' or 'simulate'.
|
||||||
simulate: Do not download the video files.
|
simulate: Do not download the video files.
|
||||||
format: Video format code. see "FORMAT SELECTION" for more details.
|
format: Video format code. see "FORMAT SELECTION" for more details.
|
||||||
format_sort: How to sort the video formats. see "Sorting Formats" for more details.
|
allow_unplayable_formats: Allow unplayable formats to be extracted and downloaded.
|
||||||
format_sort_force: Force the given format_sort. see "Sorting Formats" for more details.
|
format_sort: How to sort the video formats. see "Sorting Formats"
|
||||||
allow_multiple_video_streams: Allow multiple video streams to be merged into a single file
|
for more details.
|
||||||
allow_multiple_audio_streams: Allow multiple audio streams to be merged into a single file
|
format_sort_force: Force the given format_sort. see "Sorting Formats"
|
||||||
outtmpl: Template for output names.
|
for more details.
|
||||||
|
allow_multiple_video_streams: Allow multiple video streams to be merged
|
||||||
|
into a single file
|
||||||
|
allow_multiple_audio_streams: Allow multiple audio streams to be merged
|
||||||
|
into a single file
|
||||||
|
paths: Dictionary of output paths. The allowed keys are 'home'
|
||||||
|
'temp' and the keys of OUTTMPL_TYPES (in utils.py)
|
||||||
|
outtmpl: Dictionary of templates for output names. Allowed keys
|
||||||
|
are 'default' and the keys of OUTTMPL_TYPES (in utils.py).
|
||||||
|
A string a also accepted for backward compatibility
|
||||||
outtmpl_na_placeholder: Placeholder for unavailable meta fields.
|
outtmpl_na_placeholder: Placeholder for unavailable meta fields.
|
||||||
restrictfilenames: Do not allow "&" and spaces in file names
|
restrictfilenames: Do not allow "&" and spaces in file names
|
||||||
trim_file_name: Limit length of filename (extension excluded)
|
trim_file_name: Limit length of filename (extension excluded)
|
||||||
|
windowsfilenames: Force the filenames to be windows compatible
|
||||||
ignoreerrors: Do not stop on download errors
|
ignoreerrors: Do not stop on download errors
|
||||||
(Default True when running youtube-dlc,
|
(Default True when running youtube-dlc,
|
||||||
but False when directly accessing YoutubeDL class)
|
but False when directly accessing YoutubeDL class)
|
||||||
@@ -202,8 +216,12 @@ class YoutubeDL(object):
|
|||||||
logtostderr: Log messages to stderr instead of stdout.
|
logtostderr: Log messages to stderr instead of stdout.
|
||||||
writedescription: Write the video description to a .description file
|
writedescription: Write the video description to a .description file
|
||||||
writeinfojson: Write the video description to a .info.json file
|
writeinfojson: Write the video description to a .info.json file
|
||||||
|
writecomments: Extract video comments. This will not be written to disk
|
||||||
|
unless writeinfojson is also given
|
||||||
writeannotations: Write the video annotations to a .annotations.xml file
|
writeannotations: Write the video annotations to a .annotations.xml file
|
||||||
writethumbnail: Write the thumbnail image to a file
|
writethumbnail: Write the thumbnail image to a file
|
||||||
|
allow_playlist_files: Whether to write playlists' description, infojson etc
|
||||||
|
also to disk when using the 'write*' options
|
||||||
write_all_thumbnails: Write all thumbnail formats to files
|
write_all_thumbnails: Write all thumbnail formats to files
|
||||||
writelink: Write an internet shortcut file, depending on the
|
writelink: Write an internet shortcut file, depending on the
|
||||||
current platform (.url/.webloc/.desktop)
|
current platform (.url/.webloc/.desktop)
|
||||||
@@ -294,6 +312,9 @@ class YoutubeDL(object):
|
|||||||
Progress hooks are guaranteed to be called at least once
|
Progress hooks are guaranteed to be called at least once
|
||||||
(with status "finished") if the download is successful.
|
(with status "finished") if the download is successful.
|
||||||
merge_output_format: Extension to use when merging formats.
|
merge_output_format: Extension to use when merging formats.
|
||||||
|
final_ext: Expected final extension; used to detect when the file was
|
||||||
|
already downloaded and converted. "merge_output_format" is
|
||||||
|
replaced by this extension when given
|
||||||
fixup: Automatically correct known faults of the file.
|
fixup: Automatically correct known faults of the file.
|
||||||
One of:
|
One of:
|
||||||
- "never": do nothing
|
- "never": do nothing
|
||||||
@@ -347,7 +368,7 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
The following options are used by the post processors:
|
The following options are used by the post processors:
|
||||||
prefer_ffmpeg: If False, use avconv instead of ffmpeg if both are available,
|
prefer_ffmpeg: If False, use avconv instead of ffmpeg if both are available,
|
||||||
otherwise prefer ffmpeg.
|
otherwise prefer ffmpeg. (avconv support is deprecated)
|
||||||
ffmpeg_location: Location of the ffmpeg/avconv binary; either the path
|
ffmpeg_location: Location of the ffmpeg/avconv binary; either the path
|
||||||
to the binary or its containing directory.
|
to the binary or its containing directory.
|
||||||
postprocessor_args: A dictionary of postprocessor/executable keys (in lower case)
|
postprocessor_args: A dictionary of postprocessor/executable keys (in lower case)
|
||||||
@@ -375,8 +396,7 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
params = None
|
params = None
|
||||||
_ies = []
|
_ies = []
|
||||||
_pps = []
|
_pps = {'beforedl': [], 'aftermove': [], 'normal': []}
|
||||||
_pps_end = []
|
|
||||||
__prepare_filename_warned = False
|
__prepare_filename_warned = False
|
||||||
_download_retcode = None
|
_download_retcode = None
|
||||||
_num_downloads = None
|
_num_downloads = None
|
||||||
@@ -390,8 +410,7 @@ class YoutubeDL(object):
|
|||||||
params = {}
|
params = {}
|
||||||
self._ies = []
|
self._ies = []
|
||||||
self._ies_instances = {}
|
self._ies_instances = {}
|
||||||
self._pps = []
|
self._pps = {'beforedl': [], 'aftermove': [], 'normal': []}
|
||||||
self._pps_end = []
|
|
||||||
self.__prepare_filename_warned = False
|
self.__prepare_filename_warned = False
|
||||||
self._post_hooks = []
|
self._post_hooks = []
|
||||||
self._progress_hooks = []
|
self._progress_hooks = []
|
||||||
@@ -438,6 +457,14 @@ class YoutubeDL(object):
|
|||||||
if self.params.get('geo_verification_proxy') is None:
|
if self.params.get('geo_verification_proxy') is None:
|
||||||
self.params['geo_verification_proxy'] = self.params['cn_verification_proxy']
|
self.params['geo_verification_proxy'] = self.params['cn_verification_proxy']
|
||||||
|
|
||||||
|
if self.params.get('final_ext'):
|
||||||
|
if self.params.get('merge_output_format'):
|
||||||
|
self.report_warning('--merge-output-format will be ignored since --remux-video or --recode-video is given')
|
||||||
|
self.params['merge_output_format'] = self.params['final_ext']
|
||||||
|
|
||||||
|
if 'overwrites' in self.params and self.params['overwrites'] is None:
|
||||||
|
del self.params['overwrites']
|
||||||
|
|
||||||
check_deprecated('autonumber_size', '--autonumber-size', 'output template with %(autonumber)0Nd, where N in the number of digits')
|
check_deprecated('autonumber_size', '--autonumber-size', 'output template with %(autonumber)0Nd, where N in the number of digits')
|
||||||
check_deprecated('autonumber', '--auto-number', '-o "%(autonumber)s-%(title)s.%(ext)s"')
|
check_deprecated('autonumber', '--auto-number', '-o "%(autonumber)s-%(title)s.%(ext)s"')
|
||||||
check_deprecated('usetitle', '--title', '-o "%(title)s-%(id)s.%(ext)s"')
|
check_deprecated('usetitle', '--title', '-o "%(title)s-%(id)s.%(ext)s"')
|
||||||
@@ -479,10 +506,7 @@ class YoutubeDL(object):
|
|||||||
'Set the LC_ALL environment variable to fix this.')
|
'Set the LC_ALL environment variable to fix this.')
|
||||||
self.params['restrictfilenames'] = True
|
self.params['restrictfilenames'] = True
|
||||||
|
|
||||||
if isinstance(params.get('outtmpl'), bytes):
|
self.outtmpl_dict = self.parse_outtmpl()
|
||||||
self.report_warning(
|
|
||||||
'Parameter outtmpl is bytes, but should be a unicode string. '
|
|
||||||
'Put from __future__ import unicode_literals at the top of your code file or consider switching to Python 3.x.')
|
|
||||||
|
|
||||||
self._setup_opener()
|
self._setup_opener()
|
||||||
|
|
||||||
@@ -494,11 +518,13 @@ class YoutubeDL(object):
|
|||||||
pp_class = get_postprocessor(pp_def_raw['key'])
|
pp_class = get_postprocessor(pp_def_raw['key'])
|
||||||
pp_def = dict(pp_def_raw)
|
pp_def = dict(pp_def_raw)
|
||||||
del pp_def['key']
|
del pp_def['key']
|
||||||
after_move = pp_def.get('_after_move', False)
|
if 'when' in pp_def:
|
||||||
if '_after_move' in pp_def:
|
when = pp_def['when']
|
||||||
del pp_def['_after_move']
|
del pp_def['when']
|
||||||
|
else:
|
||||||
|
when = 'normal'
|
||||||
pp = pp_class(self, **compat_kwargs(pp_def))
|
pp = pp_class(self, **compat_kwargs(pp_def))
|
||||||
self.add_post_processor(pp, after_move=after_move)
|
self.add_post_processor(pp, when=when)
|
||||||
|
|
||||||
for ph in self.params.get('post_hooks', []):
|
for ph in self.params.get('post_hooks', []):
|
||||||
self.add_post_hook(ph)
|
self.add_post_hook(ph)
|
||||||
@@ -550,12 +576,9 @@ class YoutubeDL(object):
|
|||||||
for ie in gen_extractor_classes():
|
for ie in gen_extractor_classes():
|
||||||
self.add_info_extractor(ie)
|
self.add_info_extractor(ie)
|
||||||
|
|
||||||
def add_post_processor(self, pp, after_move=False):
|
def add_post_processor(self, pp, when='normal'):
|
||||||
"""Add a PostProcessor object to the end of the chain."""
|
"""Add a PostProcessor object to the end of the chain."""
|
||||||
if after_move:
|
self._pps[when].append(pp)
|
||||||
self._pps_end.append(pp)
|
|
||||||
else:
|
|
||||||
self._pps.append(pp)
|
|
||||||
pp.set_downloader(self)
|
pp.set_downloader(self)
|
||||||
|
|
||||||
def add_post_hook(self, ph):
|
def add_post_hook(self, ph):
|
||||||
@@ -715,15 +738,33 @@ class YoutubeDL(object):
|
|||||||
def report_file_delete(self, file_name):
|
def report_file_delete(self, file_name):
|
||||||
"""Report that existing file will be deleted."""
|
"""Report that existing file will be deleted."""
|
||||||
try:
|
try:
|
||||||
self.to_screen('Deleting already existent file %s' % file_name)
|
self.to_screen('Deleting existing file %s' % file_name)
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
self.to_screen('Deleting already existent file')
|
self.to_screen('Deleting existing file')
|
||||||
|
|
||||||
def prepare_filename(self, info_dict, warn=False):
|
def parse_outtmpl(self):
|
||||||
"""Generate the output filename."""
|
outtmpl_dict = self.params.get('outtmpl', {})
|
||||||
|
if not isinstance(outtmpl_dict, dict):
|
||||||
|
outtmpl_dict = {'default': outtmpl_dict}
|
||||||
|
outtmpl_dict.update({
|
||||||
|
k: v for k, v in DEFAULT_OUTTMPL.items()
|
||||||
|
if not outtmpl_dict.get(k)})
|
||||||
|
for key, val in outtmpl_dict.items():
|
||||||
|
if isinstance(val, bytes):
|
||||||
|
self.report_warning(
|
||||||
|
'Parameter outtmpl is bytes, but should be a unicode string. '
|
||||||
|
'Put from __future__ import unicode_literals at the top of your code file or consider switching to Python 3.x.')
|
||||||
|
return outtmpl_dict
|
||||||
|
|
||||||
|
def _prepare_filename(self, info_dict, tmpl_type='default'):
|
||||||
try:
|
try:
|
||||||
template_dict = dict(info_dict)
|
template_dict = dict(info_dict)
|
||||||
|
|
||||||
|
template_dict['duration_string'] = ( # %(duration>%H-%M-%S)s is wrong if duration > 24hrs
|
||||||
|
formatSeconds(info_dict['duration'], '-')
|
||||||
|
if info_dict.get('duration', None) is not None
|
||||||
|
else None)
|
||||||
|
|
||||||
template_dict['epoch'] = int(time.time())
|
template_dict['epoch'] = int(time.time())
|
||||||
autonumber_size = self.params.get('autonumber_size')
|
autonumber_size = self.params.get('autonumber_size')
|
||||||
if autonumber_size is None:
|
if autonumber_size is None:
|
||||||
@@ -744,9 +785,11 @@ class YoutubeDL(object):
|
|||||||
template_dict = dict((k, v if isinstance(v, compat_numeric_types) else sanitize(k, v))
|
template_dict = dict((k, v if isinstance(v, compat_numeric_types) else sanitize(k, v))
|
||||||
for k, v in template_dict.items()
|
for k, v in template_dict.items()
|
||||||
if v is not None and not isinstance(v, (list, tuple, dict)))
|
if v is not None and not isinstance(v, (list, tuple, dict)))
|
||||||
template_dict = collections.defaultdict(lambda: self.params.get('outtmpl_na_placeholder', 'NA'), template_dict)
|
na = self.params.get('outtmpl_na_placeholder', 'NA')
|
||||||
|
template_dict = collections.defaultdict(lambda: na, template_dict)
|
||||||
|
|
||||||
outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
|
outtmpl = self.outtmpl_dict.get(tmpl_type, self.outtmpl_dict['default'])
|
||||||
|
force_ext = OUTTMPL_TYPES.get(tmpl_type)
|
||||||
|
|
||||||
# For fields playlist_index and autonumber convert all occurrences
|
# For fields playlist_index and autonumber convert all occurrences
|
||||||
# of %(field)s to %(field)0Nd for backward compatibility
|
# of %(field)s to %(field)0Nd for backward compatibility
|
||||||
@@ -762,12 +805,6 @@ class YoutubeDL(object):
|
|||||||
r'%%(\1)0%dd' % field_size_compat_map[mobj.group('field')],
|
r'%%(\1)0%dd' % field_size_compat_map[mobj.group('field')],
|
||||||
outtmpl)
|
outtmpl)
|
||||||
|
|
||||||
# Missing numeric fields used together with integer presentation types
|
|
||||||
# in format specification will break the argument substitution since
|
|
||||||
# string NA placeholder is returned for missing fields. We will patch
|
|
||||||
# output template for missing fields to meet string presentation type.
|
|
||||||
for numeric_field in self._NUMERIC_FIELDS:
|
|
||||||
if numeric_field not in template_dict:
|
|
||||||
# As of [1] format syntax is:
|
# As of [1] format syntax is:
|
||||||
# %[mapping_key][conversion_flags][minimum_width][.precision][length_modifier]type
|
# %[mapping_key][conversion_flags][minimum_width][.precision][length_modifier]type
|
||||||
# 1. https://docs.python.org/2/library/stdtypes.html#string-formatting
|
# 1. https://docs.python.org/2/library/stdtypes.html#string-formatting
|
||||||
@@ -779,10 +816,34 @@ class YoutubeDL(object):
|
|||||||
(?:\d+)? # minimum field width (optional)
|
(?:\d+)? # minimum field width (optional)
|
||||||
(?:\.\d+)? # precision (optional)
|
(?:\.\d+)? # precision (optional)
|
||||||
[hlL]? # length modifier (optional)
|
[hlL]? # length modifier (optional)
|
||||||
[diouxXeEfFgGcrs%] # conversion type
|
(?P<type>[diouxXeEfFgGcrs%]) # conversion type
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
numeric_fields = list(self._NUMERIC_FIELDS)
|
||||||
|
|
||||||
|
# Format date
|
||||||
|
FORMAT_DATE_RE = FORMAT_RE.format(r'(?P<key>(?P<field>\w+)>(?P<format>.+?))')
|
||||||
|
for mobj in re.finditer(FORMAT_DATE_RE, outtmpl):
|
||||||
|
conv_type, field, frmt, key = mobj.group('type', 'field', 'format', 'key')
|
||||||
|
if key in template_dict:
|
||||||
|
continue
|
||||||
|
value = strftime_or_none(template_dict.get(field), frmt, na)
|
||||||
|
if conv_type in 'crs': # string
|
||||||
|
value = sanitize(field, value)
|
||||||
|
else: # number
|
||||||
|
numeric_fields.append(key)
|
||||||
|
value = float_or_none(value, default=None)
|
||||||
|
if value is not None:
|
||||||
|
template_dict[key] = value
|
||||||
|
|
||||||
|
# Missing numeric fields used together with integer presentation types
|
||||||
|
# in format specification will break the argument substitution since
|
||||||
|
# string NA placeholder is returned for missing fields. We will patch
|
||||||
|
# output template for missing fields to meet string presentation type.
|
||||||
|
for numeric_field in numeric_fields:
|
||||||
|
if numeric_field not in template_dict:
|
||||||
outtmpl = re.sub(
|
outtmpl = re.sub(
|
||||||
FORMAT_RE.format(numeric_field),
|
FORMAT_RE.format(re.escape(numeric_field)),
|
||||||
r'%({0})s'.format(numeric_field), outtmpl)
|
r'%({0})s'.format(numeric_field), outtmpl)
|
||||||
|
|
||||||
# expand_path translates '%%' into '%' and '$$' into '$'
|
# expand_path translates '%%' into '%' and '$$' into '$'
|
||||||
@@ -798,6 +859,9 @@ class YoutubeDL(object):
|
|||||||
# title "Hello $PATH", we don't want `$PATH` to be expanded.
|
# title "Hello $PATH", we don't want `$PATH` to be expanded.
|
||||||
filename = expand_path(outtmpl).replace(sep, '') % template_dict
|
filename = expand_path(outtmpl).replace(sep, '') % template_dict
|
||||||
|
|
||||||
|
if force_ext is not None:
|
||||||
|
filename = replace_extension(filename, force_ext, template_dict.get('ext'))
|
||||||
|
|
||||||
# https://github.com/blackjack4494/youtube-dlc/issues/85
|
# https://github.com/blackjack4494/youtube-dlc/issues/85
|
||||||
trim_file_name = self.params.get('trim_file_name', False)
|
trim_file_name = self.params.get('trim_file_name', False)
|
||||||
if trim_file_name:
|
if trim_file_name:
|
||||||
@@ -808,37 +872,40 @@ class YoutubeDL(object):
|
|||||||
sub_ext = fn_groups[-2]
|
sub_ext = fn_groups[-2]
|
||||||
filename = '.'.join(filter(None, [fn_groups[0][:trim_file_name], sub_ext, ext]))
|
filename = '.'.join(filter(None, [fn_groups[0][:trim_file_name], sub_ext, ext]))
|
||||||
|
|
||||||
# Temporary fix for #4787
|
return filename
|
||||||
# 'Treat' all problem characters by passing filename through preferredencoding
|
except ValueError as err:
|
||||||
# to workaround encoding issues with subprocess on python2 @ Windows
|
self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
|
||||||
if sys.version_info < (3, 0) and sys.platform == 'win32':
|
return None
|
||||||
filename = encodeFilename(filename, True).decode(preferredencoding())
|
|
||||||
filename = sanitize_path(filename)
|
def prepare_filename(self, info_dict, dir_type='', warn=False):
|
||||||
|
"""Generate the output filename."""
|
||||||
|
paths = self.params.get('paths', {})
|
||||||
|
assert isinstance(paths, dict)
|
||||||
|
filename = self._prepare_filename(info_dict, dir_type or 'default')
|
||||||
|
|
||||||
if warn and not self.__prepare_filename_warned:
|
if warn and not self.__prepare_filename_warned:
|
||||||
if not self.params.get('paths'):
|
if not paths:
|
||||||
pass
|
pass
|
||||||
elif filename == '-':
|
elif filename == '-':
|
||||||
self.report_warning('--paths is ignored when an outputting to stdout')
|
self.report_warning('--paths is ignored when an outputting to stdout')
|
||||||
elif os.path.isabs(filename):
|
elif os.path.isabs(filename):
|
||||||
self.report_warning('--paths is ignored since an absolute path is given in output template')
|
self.report_warning('--paths is ignored since an absolute path is given in output template')
|
||||||
self.__prepare_filename_warned = True
|
self.__prepare_filename_warned = True
|
||||||
|
if filename == '-' or not filename:
|
||||||
return filename
|
return filename
|
||||||
except ValueError as err:
|
|
||||||
self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
|
|
||||||
return None
|
|
||||||
|
|
||||||
def prepare_filepath(self, filename, dir_type=''):
|
|
||||||
if filename == '-':
|
|
||||||
return filename
|
|
||||||
paths = self.params.get('paths', {})
|
|
||||||
assert isinstance(paths, dict)
|
|
||||||
homepath = expand_path(paths.get('home', '').strip())
|
homepath = expand_path(paths.get('home', '').strip())
|
||||||
assert isinstance(homepath, compat_str)
|
assert isinstance(homepath, compat_str)
|
||||||
subdir = expand_path(paths.get(dir_type, '').strip()) if dir_type else ''
|
subdir = expand_path(paths.get(dir_type, '').strip()) if dir_type else ''
|
||||||
assert isinstance(subdir, compat_str)
|
assert isinstance(subdir, compat_str)
|
||||||
return sanitize_path(os.path.join(homepath, subdir, filename))
|
path = os.path.join(homepath, subdir, filename)
|
||||||
|
|
||||||
|
# Temporary fix for #4787
|
||||||
|
# 'Treat' all problem characters by passing filename through preferredencoding
|
||||||
|
# to workaround encoding issues with subprocess on python2 @ Windows
|
||||||
|
if sys.version_info < (3, 0) and sys.platform == 'win32':
|
||||||
|
path = encodeFilename(path, True).decode(preferredencoding())
|
||||||
|
return sanitize_path(path, force=self.params.get('windowsfilenames'))
|
||||||
|
|
||||||
def _match_entry(self, info_dict, incomplete):
|
def _match_entry(self, info_dict, incomplete):
|
||||||
""" Returns None if the file should be downloaded """
|
""" Returns None if the file should be downloaded """
|
||||||
@@ -933,9 +1000,7 @@ class YoutubeDL(object):
|
|||||||
self.to_screen("[%s] %s: has already been recorded in archive" % (
|
self.to_screen("[%s] %s: has already been recorded in archive" % (
|
||||||
ie_key, temp_id))
|
ie_key, temp_id))
|
||||||
break
|
break
|
||||||
|
|
||||||
return self.__extract_info(url, ie, download, extra_info, process, info_dict)
|
return self.__extract_info(url, ie, download, extra_info, process, info_dict)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.report_error('no suitable InfoExtractor for URL %s' % url)
|
self.report_error('no suitable InfoExtractor for URL %s' % url)
|
||||||
|
|
||||||
@@ -987,10 +1052,6 @@ class YoutubeDL(object):
|
|||||||
self.add_extra_info(ie_result, {
|
self.add_extra_info(ie_result, {
|
||||||
'extractor': ie.IE_NAME,
|
'extractor': ie.IE_NAME,
|
||||||
'webpage_url': url,
|
'webpage_url': url,
|
||||||
'duration_string': (
|
|
||||||
formatSeconds(ie_result['duration'], '-')
|
|
||||||
if ie_result.get('duration', None) is not None
|
|
||||||
else None),
|
|
||||||
'webpage_url_basename': url_basename(url),
|
'webpage_url_basename': url_basename(url),
|
||||||
'extractor_key': ie.ie_key(),
|
'extractor_key': ie.ie_key(),
|
||||||
})
|
})
|
||||||
@@ -1010,10 +1071,7 @@ class YoutubeDL(object):
|
|||||||
extract_flat = self.params.get('extract_flat', False)
|
extract_flat = self.params.get('extract_flat', False)
|
||||||
if ((extract_flat == 'in_playlist' and 'playlist' in extra_info)
|
if ((extract_flat == 'in_playlist' and 'playlist' in extra_info)
|
||||||
or extract_flat is True):
|
or extract_flat is True):
|
||||||
self.__forced_printings(
|
self.__forced_printings(ie_result, self.prepare_filename(ie_result), incomplete=True)
|
||||||
ie_result,
|
|
||||||
self.prepare_filepath(self.prepare_filename(ie_result)),
|
|
||||||
incomplete=True)
|
|
||||||
return ie_result
|
return ie_result
|
||||||
|
|
||||||
if result_type == 'video':
|
if result_type == 'video':
|
||||||
@@ -1104,6 +1162,53 @@ class YoutubeDL(object):
|
|||||||
playlist = ie_result.get('title') or ie_result.get('id')
|
playlist = ie_result.get('title') or ie_result.get('id')
|
||||||
self.to_screen('[download] Downloading playlist: %s' % playlist)
|
self.to_screen('[download] Downloading playlist: %s' % playlist)
|
||||||
|
|
||||||
|
if self.params.get('allow_playlist_files', True):
|
||||||
|
ie_copy = {
|
||||||
|
'playlist': playlist,
|
||||||
|
'playlist_id': ie_result.get('id'),
|
||||||
|
'playlist_title': ie_result.get('title'),
|
||||||
|
'playlist_uploader': ie_result.get('uploader'),
|
||||||
|
'playlist_uploader_id': ie_result.get('uploader_id'),
|
||||||
|
'playlist_index': 0
|
||||||
|
}
|
||||||
|
ie_copy.update(dict(ie_result))
|
||||||
|
|
||||||
|
def ensure_dir_exists(path):
|
||||||
|
return make_dir(path, self.report_error)
|
||||||
|
|
||||||
|
if self.params.get('writeinfojson', False):
|
||||||
|
infofn = self.prepare_filename(ie_copy, 'pl_infojson')
|
||||||
|
if not ensure_dir_exists(encodeFilename(infofn)):
|
||||||
|
return
|
||||||
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
|
||||||
|
self.to_screen('[info] Playlist metadata is already present')
|
||||||
|
else:
|
||||||
|
playlist_info = dict(ie_result)
|
||||||
|
# playlist_info['entries'] = list(playlist_info['entries']) # Entries is a generator which shouldnot be resolved here
|
||||||
|
del playlist_info['entries']
|
||||||
|
self.to_screen('[info] Writing playlist metadata as JSON to: ' + infofn)
|
||||||
|
try:
|
||||||
|
write_json_file(self.filter_requested_info(playlist_info), infofn)
|
||||||
|
except (OSError, IOError):
|
||||||
|
self.report_error('Cannot write playlist metadata to JSON file ' + infofn)
|
||||||
|
|
||||||
|
if self.params.get('writedescription', False):
|
||||||
|
descfn = self.prepare_filename(ie_copy, 'pl_description')
|
||||||
|
if not ensure_dir_exists(encodeFilename(descfn)):
|
||||||
|
return
|
||||||
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
|
||||||
|
self.to_screen('[info] Playlist description is already present')
|
||||||
|
elif ie_result.get('description') is None:
|
||||||
|
self.report_warning('There\'s no playlist description to write.')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.to_screen('[info] Writing playlist description to: ' + descfn)
|
||||||
|
with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
|
||||||
|
descfile.write(ie_result['description'])
|
||||||
|
except (OSError, IOError):
|
||||||
|
self.report_error('Cannot write playlist description file ' + descfn)
|
||||||
|
return
|
||||||
|
|
||||||
playlist_results = []
|
playlist_results = []
|
||||||
|
|
||||||
playliststart = self.params.get('playliststart', 1) - 1
|
playliststart = self.params.get('playliststart', 1) - 1
|
||||||
@@ -1288,7 +1393,7 @@ class YoutubeDL(object):
|
|||||||
and (
|
and (
|
||||||
not can_merge()
|
not can_merge()
|
||||||
or info_dict.get('is_live', False)
|
or info_dict.get('is_live', False)
|
||||||
or self.params.get('outtmpl', DEFAULT_OUTTMPL) == '-'))
|
or self.outtmpl_dict['default'] == '-'))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
'best/bestvideo+bestaudio'
|
'best/bestvideo+bestaudio'
|
||||||
@@ -1799,7 +1904,7 @@ class YoutubeDL(object):
|
|||||||
if req_format is None:
|
if req_format is None:
|
||||||
req_format = self._default_format_spec(info_dict, download=download)
|
req_format = self._default_format_spec(info_dict, download=download)
|
||||||
if self.params.get('verbose'):
|
if self.params.get('verbose'):
|
||||||
self._write_string('[debug] Default format spec: %s\n' % req_format)
|
self.to_screen('[debug] Default format spec: %s' % req_format)
|
||||||
|
|
||||||
format_selector = self.build_format_selector(req_format)
|
format_selector = self.build_format_selector(req_format)
|
||||||
|
|
||||||
@@ -1948,10 +2053,12 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
self._num_downloads += 1
|
self._num_downloads += 1
|
||||||
|
|
||||||
filename = self.prepare_filename(info_dict, warn=True)
|
info_dict = self.pre_process(info_dict)
|
||||||
info_dict['_filename'] = full_filename = self.prepare_filepath(filename)
|
|
||||||
temp_filename = self.prepare_filepath(filename, 'temp')
|
info_dict['_filename'] = full_filename = self.prepare_filename(info_dict, warn=True)
|
||||||
|
temp_filename = self.prepare_filename(info_dict, 'temp')
|
||||||
files_to_move = {}
|
files_to_move = {}
|
||||||
|
skip_dl = self.params.get('skip_download', False)
|
||||||
|
|
||||||
# Forced printings
|
# Forced printings
|
||||||
self.__forced_printings(info_dict, full_filename, incomplete=False)
|
self.__forced_printings(info_dict, full_filename, incomplete=False)
|
||||||
@@ -1963,7 +2070,7 @@ class YoutubeDL(object):
|
|||||||
# Do nothing else if in simulate mode
|
# Do nothing else if in simulate mode
|
||||||
return
|
return
|
||||||
|
|
||||||
if filename is None:
|
if full_filename is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
def ensure_dir_exists(path):
|
def ensure_dir_exists(path):
|
||||||
@@ -1975,9 +2082,7 @@ class YoutubeDL(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.params.get('writedescription', False):
|
if self.params.get('writedescription', False):
|
||||||
descfn = replace_extension(
|
descfn = self.prepare_filename(info_dict, 'description')
|
||||||
self.prepare_filepath(filename, 'description'),
|
|
||||||
'description', info_dict.get('ext'))
|
|
||||||
if not ensure_dir_exists(encodeFilename(descfn)):
|
if not ensure_dir_exists(encodeFilename(descfn)):
|
||||||
return
|
return
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
|
||||||
@@ -1994,9 +2099,7 @@ class YoutubeDL(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.params.get('writeannotations', False):
|
if self.params.get('writeannotations', False):
|
||||||
annofn = replace_extension(
|
annofn = self.prepare_filename(info_dict, 'annotation')
|
||||||
self.prepare_filepath(filename, 'annotation'),
|
|
||||||
'annotations.xml', info_dict.get('ext'))
|
|
||||||
if not ensure_dir_exists(encodeFilename(annofn)):
|
if not ensure_dir_exists(encodeFilename(annofn)):
|
||||||
return
|
return
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(annofn)):
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(annofn)):
|
||||||
@@ -2032,10 +2135,11 @@ class YoutubeDL(object):
|
|||||||
# ie = self.get_info_extractor(info_dict['extractor_key'])
|
# ie = self.get_info_extractor(info_dict['extractor_key'])
|
||||||
for sub_lang, sub_info in subtitles.items():
|
for sub_lang, sub_info in subtitles.items():
|
||||||
sub_format = sub_info['ext']
|
sub_format = sub_info['ext']
|
||||||
sub_filename = subtitles_filename(temp_filename, sub_lang, sub_format, info_dict.get('ext'))
|
sub_fn = self.prepare_filename(info_dict, 'subtitle')
|
||||||
sub_filename_final = subtitles_filename(
|
sub_filename = subtitles_filename(
|
||||||
self.prepare_filepath(filename, 'subtitle'),
|
temp_filename if not skip_dl else sub_fn,
|
||||||
sub_lang, sub_format, info_dict.get('ext'))
|
sub_lang, sub_format, info_dict.get('ext'))
|
||||||
|
sub_filename_final = subtitles_filename(sub_fn, sub_lang, sub_format, info_dict.get('ext'))
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(sub_filename)):
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(sub_filename)):
|
||||||
self.to_screen('[info] Video subtitle %s.%s is already present' % (sub_lang, sub_format))
|
self.to_screen('[info] Video subtitle %s.%s is already present' % (sub_lang, sub_format))
|
||||||
files_to_move[sub_filename] = sub_filename_final
|
files_to_move[sub_filename] = sub_filename_final
|
||||||
@@ -2069,10 +2173,10 @@ class YoutubeDL(object):
|
|||||||
(sub_lang, error_to_compat_str(err)))
|
(sub_lang, error_to_compat_str(err)))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self.params.get('skip_download', False):
|
if skip_dl:
|
||||||
if self.params.get('convertsubtitles', False):
|
if self.params.get('convertsubtitles', False):
|
||||||
# subconv = FFmpegSubtitlesConvertorPP(self, format=self.params.get('convertsubtitles'))
|
# subconv = FFmpegSubtitlesConvertorPP(self, format=self.params.get('convertsubtitles'))
|
||||||
filename_real_ext = os.path.splitext(filename)[1][1:]
|
filename_real_ext = os.path.splitext(full_filename)[1][1:]
|
||||||
filename_wo_ext = (
|
filename_wo_ext = (
|
||||||
os.path.splitext(full_filename)[0]
|
os.path.splitext(full_filename)[0]
|
||||||
if filename_real_ext == info_dict['ext']
|
if filename_real_ext == info_dict['ext']
|
||||||
@@ -2087,29 +2191,31 @@ class YoutubeDL(object):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.post_process(full_filename, info_dict, files_to_move)
|
self.post_process(full_filename, info_dict, files_to_move)
|
||||||
except (PostProcessingError) as err:
|
except PostProcessingError as err:
|
||||||
self.report_error('postprocessing: %s' % str(err))
|
self.report_error('Postprocessing: %s' % str(err))
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.params.get('writeinfojson', False):
|
if self.params.get('writeinfojson', False):
|
||||||
infofn = replace_extension(
|
infofn = self.prepare_filename(info_dict, 'infojson')
|
||||||
self.prepare_filepath(filename, 'infojson'),
|
|
||||||
'info.json', info_dict.get('ext'))
|
|
||||||
if not ensure_dir_exists(encodeFilename(infofn)):
|
if not ensure_dir_exists(encodeFilename(infofn)):
|
||||||
return
|
return
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
|
||||||
self.to_screen('[info] Video description metadata is already present')
|
self.to_screen('[info] Video metadata is already present')
|
||||||
else:
|
else:
|
||||||
self.to_screen('[info] Writing video description metadata as JSON to: ' + infofn)
|
self.to_screen('[info] Writing video metadata as JSON to: ' + infofn)
|
||||||
try:
|
try:
|
||||||
write_json_file(self.filter_requested_info(info_dict), infofn)
|
write_json_file(self.filter_requested_info(info_dict), infofn)
|
||||||
except (OSError, IOError):
|
except (OSError, IOError):
|
||||||
self.report_error('Cannot write metadata to JSON file ' + infofn)
|
self.report_error('Cannot write video metadata to JSON file ' + infofn)
|
||||||
return
|
return
|
||||||
|
info_dict['__infojson_filename'] = infofn
|
||||||
|
|
||||||
thumbdir = os.path.dirname(self.prepare_filepath(filename, 'thumbnail'))
|
thumbfn = self.prepare_filename(info_dict, 'thumbnail')
|
||||||
for thumbfn in self._write_thumbnails(info_dict, temp_filename):
|
thumb_fn_temp = temp_filename if not skip_dl else thumbfn
|
||||||
files_to_move[thumbfn] = os.path.join(thumbdir, os.path.basename(thumbfn))
|
for thumb_ext in self._write_thumbnails(info_dict, thumb_fn_temp):
|
||||||
|
thumb_filename_temp = replace_extension(thumb_fn_temp, thumb_ext, info_dict.get('ext'))
|
||||||
|
thumb_filename = replace_extension(thumbfn, thumb_ext, info_dict.get('ext'))
|
||||||
|
files_to_move[thumb_filename_temp] = info_dict['__thumbnail_filename'] = thumb_filename
|
||||||
|
|
||||||
# Write internet shortcut files
|
# Write internet shortcut files
|
||||||
url_link = webloc_link = desktop_link = False
|
url_link = webloc_link = desktop_link = False
|
||||||
@@ -2162,37 +2268,44 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
# Download
|
# Download
|
||||||
must_record_download_archive = False
|
must_record_download_archive = False
|
||||||
if not self.params.get('skip_download', False):
|
if not skip_dl:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
def existing_file(filename, temp_filename):
|
def existing_file(*filepaths):
|
||||||
file_exists = os.path.exists(encodeFilename(filename))
|
ext = info_dict.get('ext')
|
||||||
tempfile_exists = (
|
final_ext = self.params.get('final_ext', ext)
|
||||||
False if temp_filename == filename
|
existing_files = []
|
||||||
else os.path.exists(encodeFilename(temp_filename)))
|
for file in orderedSet(filepaths):
|
||||||
if not self.params.get('overwrites', False) and (file_exists or tempfile_exists):
|
if final_ext != ext:
|
||||||
existing_filename = temp_filename if tempfile_exists else filename
|
converted = replace_extension(file, final_ext, ext)
|
||||||
self.to_screen('[download] %s has already been downloaded and merged' % existing_filename)
|
if os.path.exists(encodeFilename(converted)):
|
||||||
return existing_filename
|
existing_files.append(converted)
|
||||||
if tempfile_exists:
|
if os.path.exists(encodeFilename(file)):
|
||||||
self.report_file_delete(temp_filename)
|
existing_files.append(file)
|
||||||
os.remove(encodeFilename(temp_filename))
|
|
||||||
if file_exists:
|
if not existing_files or self.params.get('overwrites', False):
|
||||||
self.report_file_delete(filename)
|
for file in orderedSet(existing_files):
|
||||||
os.remove(encodeFilename(filename))
|
self.report_file_delete(file)
|
||||||
|
os.remove(encodeFilename(file))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
self.report_file_already_downloaded(existing_files[0])
|
||||||
|
info_dict['ext'] = os.path.splitext(existing_files[0])[1][1:]
|
||||||
|
return existing_files[0]
|
||||||
|
|
||||||
success = True
|
success = True
|
||||||
if info_dict.get('requested_formats') is not None:
|
if info_dict.get('requested_formats') is not None:
|
||||||
downloaded = []
|
downloaded = []
|
||||||
merger = FFmpegMergerPP(self)
|
merger = FFmpegMergerPP(self)
|
||||||
if not merger.available:
|
if self.params.get('allow_unplayable_formats'):
|
||||||
postprocessors = []
|
self.report_warning(
|
||||||
self.report_warning('You have requested multiple '
|
'You have requested merging of multiple formats '
|
||||||
'formats but ffmpeg or avconv are not installed.'
|
'while also allowing unplayable formats to be downloaded. '
|
||||||
' The formats won\'t be merged.')
|
'The formats won\'t be merged to prevent data corruption.')
|
||||||
else:
|
elif not merger.available:
|
||||||
postprocessors = [merger]
|
self.report_warning(
|
||||||
|
'You have requested merging of multiple formats but ffmpeg is not installed. '
|
||||||
|
'The formats won\'t be merged.')
|
||||||
|
|
||||||
def compatible_formats(formats):
|
def compatible_formats(formats):
|
||||||
# TODO: some formats actually allow this (mkv, webm, ogg, mp4), but not all of them.
|
# TODO: some formats actually allow this (mkv, webm, ogg, mp4), but not all of them.
|
||||||
@@ -2232,22 +2345,28 @@ class YoutubeDL(object):
|
|||||||
full_filename = correct_ext(full_filename)
|
full_filename = correct_ext(full_filename)
|
||||||
temp_filename = correct_ext(temp_filename)
|
temp_filename = correct_ext(temp_filename)
|
||||||
dl_filename = existing_file(full_filename, temp_filename)
|
dl_filename = existing_file(full_filename, temp_filename)
|
||||||
|
info_dict['__real_download'] = False
|
||||||
if dl_filename is None:
|
if dl_filename is None:
|
||||||
for f in requested_formats:
|
for f in requested_formats:
|
||||||
new_info = dict(info_dict)
|
new_info = dict(info_dict)
|
||||||
new_info.update(f)
|
new_info.update(f)
|
||||||
fname = prepend_extension(
|
fname = prepend_extension(
|
||||||
self.prepare_filepath(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 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)
|
||||||
|
info_dict['__real_download'] = info_dict['__real_download'] or real_download
|
||||||
success = success and partial_success
|
success = success and partial_success
|
||||||
info_dict['__postprocessors'] = postprocessors
|
if merger.available and not self.params.get('allow_unplayable_formats'):
|
||||||
|
info_dict['__postprocessors'].append(merger)
|
||||||
info_dict['__files_to_merge'] = downloaded
|
info_dict['__files_to_merge'] = downloaded
|
||||||
# Even if there were no downloads, it is being merged only now
|
# Even if there were no downloads, it is being merged only now
|
||||||
info_dict['__real_download'] = True
|
info_dict['__real_download'] = True
|
||||||
|
else:
|
||||||
|
for file in downloaded:
|
||||||
|
files_to_move[file] = None
|
||||||
else:
|
else:
|
||||||
# Just a single file
|
# Just a single file
|
||||||
dl_filename = existing_file(full_filename, temp_filename)
|
dl_filename = existing_file(full_filename, temp_filename)
|
||||||
@@ -2267,13 +2386,13 @@ class YoutubeDL(object):
|
|||||||
self.report_error('content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
|
self.report_error('content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
|
||||||
return
|
return
|
||||||
|
|
||||||
if success and filename != '-':
|
if success and full_filename != '-':
|
||||||
# Fixup content
|
# Fixup content
|
||||||
fixup_policy = self.params.get('fixup')
|
fixup_policy = self.params.get('fixup')
|
||||||
if fixup_policy is None:
|
if fixup_policy is None:
|
||||||
fixup_policy = 'detect_or_warn'
|
fixup_policy = 'detect_or_warn'
|
||||||
|
|
||||||
INSTALL_FFMPEG_MESSAGE = 'Install ffmpeg or avconv to fix this automatically.'
|
INSTALL_FFMPEG_MESSAGE = 'Install ffmpeg to fix this automatically.'
|
||||||
|
|
||||||
stretched_ratio = info_dict.get('stretched_ratio')
|
stretched_ratio = info_dict.get('stretched_ratio')
|
||||||
if stretched_ratio is not None and stretched_ratio != 1:
|
if stretched_ratio is not None and stretched_ratio != 1:
|
||||||
@@ -2292,7 +2411,8 @@ class YoutubeDL(object):
|
|||||||
assert fixup_policy in ('ignore', 'never')
|
assert fixup_policy in ('ignore', 'never')
|
||||||
|
|
||||||
if (info_dict.get('requested_formats') is None
|
if (info_dict.get('requested_formats') is None
|
||||||
and info_dict.get('container') == 'm4a_dash'):
|
and info_dict.get('container') == 'm4a_dash'
|
||||||
|
and info_dict.get('ext') == 'm4a'):
|
||||||
if fixup_policy == 'warn':
|
if fixup_policy == 'warn':
|
||||||
self.report_warning(
|
self.report_warning(
|
||||||
'%s: writing DASH m4a. '
|
'%s: writing DASH m4a. '
|
||||||
@@ -2329,8 +2449,8 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
self.post_process(dl_filename, info_dict, files_to_move)
|
self.post_process(dl_filename, info_dict, files_to_move)
|
||||||
except (PostProcessingError) as err:
|
except PostProcessingError as err:
|
||||||
self.report_error('postprocessing: %s' % str(err))
|
self.report_error('Postprocessing: %s' % str(err))
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
for ph in self._post_hooks:
|
for ph in self._post_hooks:
|
||||||
@@ -2348,7 +2468,7 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
def download(self, url_list):
|
def download(self, url_list):
|
||||||
"""Download a given list of URLs."""
|
"""Download a given list of URLs."""
|
||||||
outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
|
outtmpl = self.outtmpl_dict['default']
|
||||||
if (len(url_list) > 1
|
if (len(url_list) > 1
|
||||||
and outtmpl != '-'
|
and outtmpl != '-'
|
||||||
and '%' not in outtmpl
|
and '%' not in outtmpl
|
||||||
@@ -2396,24 +2516,16 @@ class YoutubeDL(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def filter_requested_info(info_dict):
|
def filter_requested_info(info_dict):
|
||||||
|
fields_to_remove = ('requested_formats', 'requested_subtitles')
|
||||||
return dict(
|
return dict(
|
||||||
(k, v) for k, v in info_dict.items()
|
(k, v) for k, v in info_dict.items()
|
||||||
if k not in ['requested_formats', 'requested_subtitles'])
|
if (k[0] != '_' or k == '_type') and k not in fields_to_remove)
|
||||||
|
|
||||||
def post_process(self, filename, ie_info, files_to_move={}):
|
def run_pp(self, pp, infodict, files_to_move={}):
|
||||||
"""Run all the postprocessors on the given file."""
|
|
||||||
info = dict(ie_info)
|
|
||||||
info['filepath'] = filename
|
|
||||||
|
|
||||||
def run_pp(pp):
|
|
||||||
files_to_delete = []
|
files_to_delete = []
|
||||||
infodict = info
|
|
||||||
try:
|
|
||||||
files_to_delete, infodict = pp.run(infodict)
|
files_to_delete, infodict = pp.run(infodict)
|
||||||
except PostProcessingError as e:
|
|
||||||
self.report_error(e.msg)
|
|
||||||
if not files_to_delete:
|
if not files_to_delete:
|
||||||
return infodict
|
return files_to_move, infodict
|
||||||
|
|
||||||
if self.params.get('keepvideo', False):
|
if self.params.get('keepvideo', False):
|
||||||
for f in files_to_delete:
|
for f in files_to_delete:
|
||||||
@@ -2427,14 +2539,25 @@ class YoutubeDL(object):
|
|||||||
self.report_warning('Unable to remove downloaded original file')
|
self.report_warning('Unable to remove downloaded original file')
|
||||||
if old_filename in files_to_move:
|
if old_filename in files_to_move:
|
||||||
del files_to_move[old_filename]
|
del files_to_move[old_filename]
|
||||||
return infodict
|
return files_to_move, infodict
|
||||||
|
|
||||||
for pp in ie_info.get('__postprocessors', []) + self._pps:
|
def pre_process(self, ie_info):
|
||||||
info = run_pp(pp)
|
info = dict(ie_info)
|
||||||
info = run_pp(MoveFilesAfterDownloadPP(self, files_to_move))
|
for pp in self._pps['beforedl']:
|
||||||
files_to_move = {}
|
info = self.run_pp(pp, info)[1]
|
||||||
for pp in self._pps_end:
|
return info
|
||||||
info = run_pp(pp)
|
|
||||||
|
def post_process(self, filename, ie_info, files_to_move={}):
|
||||||
|
"""Run all the postprocessors on the given file."""
|
||||||
|
info = dict(ie_info)
|
||||||
|
info['filepath'] = filename
|
||||||
|
info['__files_to_move'] = {}
|
||||||
|
|
||||||
|
for pp in ie_info.get('__postprocessors', []) + self._pps['normal']:
|
||||||
|
files_to_move, info = self.run_pp(pp, info, files_to_move)
|
||||||
|
info = self.run_pp(MoveFilesAfterDownloadPP(self, files_to_move), info)[1]
|
||||||
|
for pp in self._pps['aftermove']:
|
||||||
|
info = self.run_pp(pp, info, {})[1]
|
||||||
|
|
||||||
def _make_archive_id(self, info_dict):
|
def _make_archive_id(self, info_dict):
|
||||||
video_id = info_dict.get('id')
|
video_id = info_dict.get('id')
|
||||||
@@ -2574,7 +2697,7 @@ class YoutubeDL(object):
|
|||||||
'|',
|
'|',
|
||||||
format_field(f, 'filesize', ' %s', func=format_bytes) + format_field(f, 'filesize_approx', '~%s', func=format_bytes),
|
format_field(f, 'filesize', ' %s', func=format_bytes) + format_field(f, 'filesize_approx', '~%s', func=format_bytes),
|
||||||
format_field(f, 'tbr', '%4dk'),
|
format_field(f, 'tbr', '%4dk'),
|
||||||
f.get('protocol').replace('http_dash_segments', 'dash').replace("native", "n"),
|
f.get('protocol').replace('http_dash_segments', 'dash').replace("native", "n").replace('niconico_', ''),
|
||||||
'|',
|
'|',
|
||||||
format_field(f, 'vcodec', default='unknown').replace('none', ''),
|
format_field(f, 'vcodec', default='unknown').replace('none', ''),
|
||||||
format_field(f, 'vbr', '%4dk'),
|
format_field(f, 'vbr', '%4dk'),
|
||||||
@@ -2597,8 +2720,6 @@ class YoutubeDL(object):
|
|||||||
if f.get('preference') is None or f['preference'] >= -1000]
|
if f.get('preference') is None or f['preference'] >= -1000]
|
||||||
header_line = ['format code', 'extension', 'resolution', 'note']
|
header_line = ['format code', 'extension', 'resolution', 'note']
|
||||||
|
|
||||||
# if len(formats) > 1:
|
|
||||||
# table[-1][-1] += (' ' if table[-1][-1] else '') + '(best)'
|
|
||||||
self.to_screen(
|
self.to_screen(
|
||||||
'[info] Available formats for %s:\n%s' % (info_dict['id'], render_table(
|
'[info] Available formats for %s:\n%s' % (info_dict['id'], render_table(
|
||||||
header_line,
|
header_line,
|
||||||
@@ -2655,7 +2776,12 @@ class YoutubeDL(object):
|
|||||||
self.get_encoding()))
|
self.get_encoding()))
|
||||||
write_string(encoding_str, encoding=None)
|
write_string(encoding_str, encoding=None)
|
||||||
|
|
||||||
self._write_string('[debug] yt-dlp version %s\n' % __version__)
|
source = (
|
||||||
|
'(exe)' if hasattr(sys, 'frozen')
|
||||||
|
else '(zip)' if isinstance(globals().get('__loader__'), zipimporter)
|
||||||
|
else '(source)' if os.path.basename(sys.argv[0]) == '__main__.py'
|
||||||
|
else '')
|
||||||
|
self._write_string('[debug] yt-dlp version %s %s\n' % (__version__, source))
|
||||||
if _LAZY_LOADER:
|
if _LAZY_LOADER:
|
||||||
self._write_string('[debug] Lazy loading extractors enabled\n')
|
self._write_string('[debug] Lazy loading extractors enabled\n')
|
||||||
if _PLUGIN_CLASSES:
|
if _PLUGIN_CLASSES:
|
||||||
@@ -2682,8 +2808,10 @@ class YoutubeDL(object):
|
|||||||
return impl_name + ' version %d.%d.%d' % sys.pypy_version_info[:3]
|
return impl_name + ' version %d.%d.%d' % sys.pypy_version_info[:3]
|
||||||
return impl_name
|
return impl_name
|
||||||
|
|
||||||
self._write_string('[debug] Python version %s (%s) - %s\n' % (
|
self._write_string('[debug] Python version %s (%s %s) - %s\n' % (
|
||||||
platform.python_version(), python_implementation(),
|
platform.python_version(),
|
||||||
|
python_implementation(),
|
||||||
|
platform.architecture()[0],
|
||||||
platform_name()))
|
platform_name()))
|
||||||
|
|
||||||
exe_versions = FFmpegPostProcessor.get_versions(self)
|
exe_versions = FFmpegPostProcessor.get_versions(self)
|
||||||
@@ -2785,25 +2913,22 @@ class YoutubeDL(object):
|
|||||||
encoding = preferredencoding()
|
encoding = preferredencoding()
|
||||||
return encoding
|
return encoding
|
||||||
|
|
||||||
def _write_thumbnails(self, info_dict, filename):
|
def _write_thumbnails(self, info_dict, filename): # return the extensions
|
||||||
if self.params.get('writethumbnail', False):
|
write_all = self.params.get('write_all_thumbnails', False)
|
||||||
thumbnails = info_dict.get('thumbnails')
|
|
||||||
if thumbnails:
|
|
||||||
thumbnails = [thumbnails[-1]]
|
|
||||||
elif self.params.get('write_all_thumbnails', False):
|
|
||||||
thumbnails = info_dict.get('thumbnails') or []
|
|
||||||
else:
|
|
||||||
thumbnails = []
|
thumbnails = []
|
||||||
|
if write_all or self.params.get('writethumbnail', False):
|
||||||
|
thumbnails = info_dict.get('thumbnails') or []
|
||||||
|
multiple = write_all and len(thumbnails) > 1
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
for t in thumbnails:
|
for t in thumbnails[::1 if write_all else -1]:
|
||||||
thumb_ext = determine_ext(t['url'], 'jpg')
|
thumb_ext = determine_ext(t['url'], 'jpg')
|
||||||
suffix = '_%s' % t['id'] if len(thumbnails) > 1 else ''
|
suffix = '%s.' % t['id'] if multiple else ''
|
||||||
thumb_display_id = '%s ' % t['id'] if len(thumbnails) > 1 else ''
|
thumb_display_id = '%s ' % t['id'] if multiple else ''
|
||||||
t['filename'] = thumb_filename = replace_extension(filename + suffix, thumb_ext, info_dict.get('ext'))
|
t['filename'] = thumb_filename = replace_extension(filename, suffix + thumb_ext, info_dict.get('ext'))
|
||||||
|
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(thumb_filename)):
|
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(thumb_filename)):
|
||||||
ret.append(thumb_filename)
|
ret.append(suffix + thumb_ext)
|
||||||
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:
|
||||||
@@ -2813,10 +2938,12 @@ class YoutubeDL(object):
|
|||||||
uf = self.urlopen(t['url'])
|
uf = self.urlopen(t['url'])
|
||||||
with open(encodeFilename(thumb_filename), 'wb') as thumbf:
|
with open(encodeFilename(thumb_filename), 'wb') as thumbf:
|
||||||
shutil.copyfileobj(uf, thumbf)
|
shutil.copyfileobj(uf, thumbf)
|
||||||
ret.append(thumb_filename)
|
ret.append(suffix + thumb_ext)
|
||||||
self.to_screen('[%s] %s: Writing thumbnail %sto: %s' %
|
self.to_screen('[%s] %s: Writing thumbnail %sto: %s' %
|
||||||
(info_dict['extractor'], info_dict['id'], thumb_display_id, thumb_filename))
|
(info_dict['extractor'], info_dict['id'], thumb_display_id, thumb_filename))
|
||||||
except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
|
except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
|
||||||
self.report_warning('Unable to download thumbnail "%s": %s' %
|
self.report_warning('Unable to download thumbnail "%s": %s' %
|
||||||
(t['url'], error_to_compat_str(err)))
|
(t['url'], error_to_compat_str(err)))
|
||||||
|
if ret and not write_all:
|
||||||
|
break
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ from .compat import (
|
|||||||
from .utils import (
|
from .utils import (
|
||||||
DateRange,
|
DateRange,
|
||||||
decodeOption,
|
decodeOption,
|
||||||
DEFAULT_OUTTMPL,
|
|
||||||
DownloadError,
|
DownloadError,
|
||||||
ExistingVideoReached,
|
ExistingVideoReached,
|
||||||
expand_path,
|
expand_path,
|
||||||
@@ -32,11 +31,12 @@ from .utils import (
|
|||||||
preferredencoding,
|
preferredencoding,
|
||||||
read_batch_urls,
|
read_batch_urls,
|
||||||
RejectedVideoReached,
|
RejectedVideoReached,
|
||||||
|
REMUX_EXTENSIONS,
|
||||||
|
render_table,
|
||||||
SameFileError,
|
SameFileError,
|
||||||
setproctitle,
|
setproctitle,
|
||||||
std_headers,
|
std_headers,
|
||||||
write_string,
|
write_string,
|
||||||
render_table,
|
|
||||||
)
|
)
|
||||||
from .update import update_self
|
from .update import update_self
|
||||||
from .downloader import (
|
from .downloader import (
|
||||||
@@ -45,6 +45,7 @@ from .downloader import (
|
|||||||
from .extractor import gen_extractors, list_extractors
|
from .extractor import gen_extractors, list_extractors
|
||||||
from .extractor.common import InfoExtractor
|
from .extractor.common import InfoExtractor
|
||||||
from .extractor.adobepass import MSO_INFO
|
from .extractor.adobepass import MSO_INFO
|
||||||
|
from .postprocessor.metadatafromfield import MetadataFromFieldPP
|
||||||
from .YoutubeDL import YoutubeDL
|
from .YoutubeDL import YoutubeDL
|
||||||
|
|
||||||
|
|
||||||
@@ -208,12 +209,14 @@ def _real_main(argv=None):
|
|||||||
opts.audioquality = opts.audioquality.strip('k').strip('K')
|
opts.audioquality = opts.audioquality.strip('k').strip('K')
|
||||||
if not opts.audioquality.isdigit():
|
if not opts.audioquality.isdigit():
|
||||||
parser.error('invalid audio quality specified')
|
parser.error('invalid audio quality specified')
|
||||||
if opts.remuxvideo is not None:
|
|
||||||
if opts.remuxvideo not in ['mp4', 'mkv']:
|
|
||||||
parser.error('invalid video container format specified')
|
|
||||||
if opts.recodevideo is not None:
|
if opts.recodevideo is not None:
|
||||||
if opts.recodevideo not in ['mp4', 'flv', 'webm', 'ogg', 'mkv', 'avi']:
|
if opts.recodevideo not in REMUX_EXTENSIONS:
|
||||||
parser.error('invalid video recode format specified')
|
parser.error('invalid video recode format specified')
|
||||||
|
if opts.remuxvideo is not None:
|
||||||
|
opts.remuxvideo = opts.remuxvideo.replace(' ', '')
|
||||||
|
remux_regex = r'{0}(?:/{0})*$'.format(r'(?:\w+>)?(?:%s)' % '|'.join(REMUX_EXTENSIONS))
|
||||||
|
if not re.match(remux_regex, opts.remuxvideo):
|
||||||
|
parser.error('invalid video remux format specified')
|
||||||
if opts.convertsubtitles is not None:
|
if opts.convertsubtitles is not None:
|
||||||
if opts.convertsubtitles not in ['srt', 'vtt', 'ass', 'lrc']:
|
if opts.convertsubtitles not in ['srt', 'vtt', 'ass', 'lrc']:
|
||||||
parser.error('invalid subtitle format specified')
|
parser.error('invalid subtitle format specified')
|
||||||
@@ -227,38 +230,79 @@ def _real_main(argv=None):
|
|||||||
if opts.extractaudio and not opts.keepvideo and opts.format is None:
|
if opts.extractaudio and not opts.keepvideo and opts.format is None:
|
||||||
opts.format = 'bestaudio/best'
|
opts.format = 'bestaudio/best'
|
||||||
|
|
||||||
# --all-sub automatically sets --write-sub if --write-auto-sub is not given
|
outtmpl = opts.outtmpl
|
||||||
# this was the old behaviour if only --all-sub was given.
|
if not outtmpl:
|
||||||
if opts.allsubtitles and not opts.writeautomaticsub:
|
outtmpl = {'default': (
|
||||||
opts.writesubtitles = True
|
'%(title)s-%(id)s-%(format)s.%(ext)s' if opts.format == '-1' and opts.usetitle
|
||||||
|
else '%(id)s-%(format)s.%(ext)s' if opts.format == '-1'
|
||||||
outtmpl = ((opts.outtmpl is not None and opts.outtmpl)
|
else '%(autonumber)s-%(title)s-%(id)s.%(ext)s' if opts.usetitle and opts.autonumber
|
||||||
or (opts.format == '-1' and opts.usetitle and '%(title)s-%(id)s-%(format)s.%(ext)s')
|
else '%(title)s-%(id)s.%(ext)s' if opts.usetitle
|
||||||
or (opts.format == '-1' and '%(id)s-%(format)s.%(ext)s')
|
else '%(id)s.%(ext)s' if opts.useid
|
||||||
or (opts.usetitle and opts.autonumber and '%(autonumber)s-%(title)s-%(id)s.%(ext)s')
|
else '%(autonumber)s-%(id)s.%(ext)s' if opts.autonumber
|
||||||
or (opts.usetitle and '%(title)s-%(id)s.%(ext)s')
|
else None)}
|
||||||
or (opts.useid and '%(id)s.%(ext)s')
|
outtmpl_default = outtmpl.get('default')
|
||||||
or (opts.autonumber and '%(autonumber)s-%(id)s.%(ext)s')
|
if outtmpl_default is not None and not os.path.splitext(outtmpl_default)[1] and opts.extractaudio:
|
||||||
or DEFAULT_OUTTMPL)
|
|
||||||
if not os.path.splitext(outtmpl)[1] and opts.extractaudio:
|
|
||||||
parser.error('Cannot download a video and extract audio into the same'
|
parser.error('Cannot download a video and extract audio into the same'
|
||||||
' file! Use "{0}.%(ext)s" instead of "{0}" as the output'
|
' file! Use "{0}.%(ext)s" instead of "{0}" as the output'
|
||||||
' template'.format(outtmpl))
|
' template'.format(outtmpl_default))
|
||||||
|
|
||||||
for f in opts.format_sort:
|
for f in opts.format_sort:
|
||||||
if re.match(InfoExtractor.FormatSort.regex, f) is None:
|
if re.match(InfoExtractor.FormatSort.regex, f) is None:
|
||||||
parser.error('invalid format sort string "%s" specified' % f)
|
parser.error('invalid format sort string "%s" specified' % f)
|
||||||
|
|
||||||
|
if opts.metafromfield is None:
|
||||||
|
opts.metafromfield = []
|
||||||
|
if opts.metafromtitle is not None:
|
||||||
|
opts.metafromfield.append('title:%s' % opts.metafromtitle)
|
||||||
|
for f in opts.metafromfield:
|
||||||
|
if re.match(MetadataFromFieldPP.regex, f) is None:
|
||||||
|
parser.error('invalid format string "%s" specified for --parse-metadata' % f)
|
||||||
|
|
||||||
any_getting = opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat or opts.getduration or opts.dumpjson or opts.dump_single_json
|
any_getting = opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat or opts.getduration or opts.dumpjson or opts.dump_single_json
|
||||||
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
|
||||||
|
|
||||||
|
def report_conflict(arg1, arg2):
|
||||||
|
write_string('WARNING: %s is ignored since %s was given\n' % (arg2, arg1), out=sys.stderr)
|
||||||
|
if opts.remuxvideo and opts.recodevideo:
|
||||||
|
report_conflict('--recode-video', '--remux-video')
|
||||||
|
opts.remuxvideo = False
|
||||||
|
if opts.allow_unplayable_formats:
|
||||||
|
if opts.extractaudio:
|
||||||
|
report_conflict('--allow-unplayable-formats', '--extract-audio')
|
||||||
|
opts.extractaudio = False
|
||||||
|
if opts.remuxvideo:
|
||||||
|
report_conflict('--allow-unplayable-formats', '--remux-video')
|
||||||
|
opts.remuxvideo = False
|
||||||
|
if opts.recodevideo:
|
||||||
|
report_conflict('--allow-unplayable-formats', '--recode-video')
|
||||||
|
opts.recodevideo = False
|
||||||
|
if opts.addmetadata:
|
||||||
|
report_conflict('--allow-unplayable-formats', '--add-metadata')
|
||||||
|
opts.addmetadata = False
|
||||||
|
if opts.embedsubtitles:
|
||||||
|
report_conflict('--allow-unplayable-formats', '--embed-subs')
|
||||||
|
opts.embedsubtitles = False
|
||||||
|
if opts.embedthumbnail:
|
||||||
|
report_conflict('--allow-unplayable-formats', '--embed-thumbnail')
|
||||||
|
opts.embedthumbnail = False
|
||||||
|
if opts.xattrs:
|
||||||
|
report_conflict('--allow-unplayable-formats', '--xattrs')
|
||||||
|
opts.xattrs = False
|
||||||
|
if opts.fixup and opts.fixup.lower() not in ('never', 'ignore'):
|
||||||
|
report_conflict('--allow-unplayable-formats', '--fixup')
|
||||||
|
opts.fixup = 'never'
|
||||||
|
if opts.sponskrub:
|
||||||
|
report_conflict('--allow-unplayable-formats', '--sponskrub')
|
||||||
|
opts.sponskrub = False
|
||||||
|
|
||||||
# PostProcessors
|
# PostProcessors
|
||||||
postprocessors = []
|
postprocessors = []
|
||||||
if opts.metafromtitle:
|
if opts.metafromfield:
|
||||||
postprocessors.append({
|
postprocessors.append({
|
||||||
'key': 'MetadataFromTitle',
|
'key': 'MetadataFromField',
|
||||||
'titleformat': opts.metafromtitle
|
'formats': opts.metafromfield,
|
||||||
|
'when': 'beforedl'
|
||||||
})
|
})
|
||||||
if opts.extractaudio:
|
if opts.extractaudio:
|
||||||
postprocessors.append({
|
postprocessors.append({
|
||||||
@@ -293,9 +337,17 @@ def _real_main(argv=None):
|
|||||||
'format': opts.convertsubtitles,
|
'format': opts.convertsubtitles,
|
||||||
})
|
})
|
||||||
if opts.embedsubtitles:
|
if opts.embedsubtitles:
|
||||||
|
already_have_subtitle = opts.writesubtitles
|
||||||
postprocessors.append({
|
postprocessors.append({
|
||||||
'key': 'FFmpegEmbedSubtitle',
|
'key': 'FFmpegEmbedSubtitle',
|
||||||
|
'already_have_subtitle': already_have_subtitle
|
||||||
})
|
})
|
||||||
|
if not already_have_subtitle:
|
||||||
|
opts.writesubtitles = True
|
||||||
|
# --all-sub automatically sets --write-sub if --write-auto-sub is not given
|
||||||
|
# this was the old behaviour if only --all-sub was given.
|
||||||
|
if opts.allsubtitles and not opts.writeautomaticsub:
|
||||||
|
opts.writesubtitles = True
|
||||||
if opts.embedthumbnail:
|
if opts.embedthumbnail:
|
||||||
already_have_thumbnail = opts.writethumbnail or opts.write_all_thumbnails
|
already_have_thumbnail = opts.writethumbnail or opts.write_all_thumbnails
|
||||||
postprocessors.append({
|
postprocessors.append({
|
||||||
@@ -324,18 +376,27 @@ def _real_main(argv=None):
|
|||||||
postprocessors.append({
|
postprocessors.append({
|
||||||
'key': 'ExecAfterDownload',
|
'key': 'ExecAfterDownload',
|
||||||
'exec_cmd': opts.exec_cmd,
|
'exec_cmd': opts.exec_cmd,
|
||||||
'_after_move': True
|
'when': 'aftermove'
|
||||||
})
|
})
|
||||||
|
|
||||||
_args_compat_warning = 'WARNING: %s given without specifying name. The arguments will be given to all %s\n'
|
def report_args_compat(arg, name):
|
||||||
|
write_string(
|
||||||
|
'WARNING: %s given without specifying name. The arguments will be given to all %s\n' % (arg, name),
|
||||||
|
out=sys.stderr)
|
||||||
if 'default' in opts.external_downloader_args:
|
if 'default' in opts.external_downloader_args:
|
||||||
write_string(_args_compat_warning % ('--external-downloader-args', 'external downloaders'), out=sys.stderr),
|
report_args_compat('--external-downloader-args', 'external downloaders')
|
||||||
|
|
||||||
if 'default-compat' in opts.postprocessor_args and 'default' not in opts.postprocessor_args:
|
if 'default-compat' in opts.postprocessor_args and 'default' not in opts.postprocessor_args:
|
||||||
write_string(_args_compat_warning % ('--post-processor-args', 'post-processors'), out=sys.stderr),
|
report_args_compat('--post-processor-args', 'post-processors')
|
||||||
opts.postprocessor_args.setdefault('sponskrub', [])
|
opts.postprocessor_args.setdefault('sponskrub', [])
|
||||||
opts.postprocessor_args['default'] = opts.postprocessor_args['default-compat']
|
opts.postprocessor_args['default'] = opts.postprocessor_args['default-compat']
|
||||||
|
|
||||||
|
final_ext = (
|
||||||
|
opts.recodevideo
|
||||||
|
or (opts.remuxvideo in REMUX_EXTENSIONS) and opts.remuxvideo
|
||||||
|
or (opts.extractaudio and opts.audioformat != 'best') and opts.audioformat
|
||||||
|
or None)
|
||||||
|
|
||||||
match_filter = (
|
match_filter = (
|
||||||
None if opts.match_filter is None
|
None if opts.match_filter is None
|
||||||
else match_filter_func(opts.match_filter))
|
else match_filter_func(opts.match_filter))
|
||||||
@@ -366,6 +427,7 @@ def _real_main(argv=None):
|
|||||||
'simulate': opts.simulate or any_getting,
|
'simulate': opts.simulate or any_getting,
|
||||||
'skip_download': opts.skip_download,
|
'skip_download': opts.skip_download,
|
||||||
'format': opts.format,
|
'format': opts.format,
|
||||||
|
'allow_unplayable_formats': opts.allow_unplayable_formats,
|
||||||
'format_sort': opts.format_sort,
|
'format_sort': opts.format_sort,
|
||||||
'format_sort_force': opts.format_sort_force,
|
'format_sort_force': opts.format_sort_force,
|
||||||
'allow_multiple_video_streams': opts.allow_multiple_video_streams,
|
'allow_multiple_video_streams': opts.allow_multiple_video_streams,
|
||||||
@@ -378,6 +440,7 @@ def _real_main(argv=None):
|
|||||||
'autonumber_size': opts.autonumber_size,
|
'autonumber_size': opts.autonumber_size,
|
||||||
'autonumber_start': opts.autonumber_start,
|
'autonumber_start': opts.autonumber_start,
|
||||||
'restrictfilenames': opts.restrictfilenames,
|
'restrictfilenames': opts.restrictfilenames,
|
||||||
|
'windowsfilenames': opts.windowsfilenames,
|
||||||
'ignoreerrors': opts.ignoreerrors,
|
'ignoreerrors': opts.ignoreerrors,
|
||||||
'force_generic_extractor': opts.force_generic_extractor,
|
'force_generic_extractor': opts.force_generic_extractor,
|
||||||
'ratelimit': opts.ratelimit,
|
'ratelimit': opts.ratelimit,
|
||||||
@@ -397,13 +460,15 @@ def _real_main(argv=None):
|
|||||||
'playlistreverse': opts.playlist_reverse,
|
'playlistreverse': opts.playlist_reverse,
|
||||||
'playlistrandom': opts.playlist_random,
|
'playlistrandom': opts.playlist_random,
|
||||||
'noplaylist': opts.noplaylist,
|
'noplaylist': opts.noplaylist,
|
||||||
'logtostderr': opts.outtmpl == '-',
|
'logtostderr': outtmpl_default == '-',
|
||||||
'consoletitle': opts.consoletitle,
|
'consoletitle': opts.consoletitle,
|
||||||
'nopart': opts.nopart,
|
'nopart': opts.nopart,
|
||||||
'updatetime': opts.updatetime,
|
'updatetime': opts.updatetime,
|
||||||
'writedescription': opts.writedescription,
|
'writedescription': opts.writedescription,
|
||||||
'writeannotations': opts.writeannotations,
|
'writeannotations': opts.writeannotations,
|
||||||
'writeinfojson': opts.writeinfojson,
|
'writeinfojson': opts.writeinfojson or opts.getcomments,
|
||||||
|
'allow_playlist_files': opts.allow_playlist_files,
|
||||||
|
'getcomments': opts.getcomments,
|
||||||
'writethumbnail': opts.writethumbnail,
|
'writethumbnail': opts.writethumbnail,
|
||||||
'write_all_thumbnails': opts.write_all_thumbnails,
|
'write_all_thumbnails': opts.write_all_thumbnails,
|
||||||
'writelink': opts.writelink,
|
'writelink': opts.writelink,
|
||||||
@@ -454,6 +519,7 @@ def _real_main(argv=None):
|
|||||||
'extract_flat': opts.extract_flat,
|
'extract_flat': opts.extract_flat,
|
||||||
'mark_watched': opts.mark_watched,
|
'mark_watched': opts.mark_watched,
|
||||||
'merge_output_format': opts.merge_output_format,
|
'merge_output_format': opts.merge_output_format,
|
||||||
|
'final_ext': final_ext,
|
||||||
'postprocessors': postprocessors,
|
'postprocessors': postprocessors,
|
||||||
'fixup': opts.fixup,
|
'fixup': opts.fixup,
|
||||||
'source_address': opts.source_address,
|
'source_address': opts.source_address,
|
||||||
@@ -484,16 +550,22 @@ def _real_main(argv=None):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with YoutubeDL(ydl_opts) as ydl:
|
with YoutubeDL(ydl_opts) as ydl:
|
||||||
# Update version
|
actual_use = len(all_urls) or opts.load_info_filename
|
||||||
if opts.update_self:
|
|
||||||
update_self(ydl.to_screen, opts.verbose, ydl._opener)
|
|
||||||
|
|
||||||
# Remove cache dir
|
# Remove cache dir
|
||||||
if opts.rm_cachedir:
|
if opts.rm_cachedir:
|
||||||
ydl.cache.remove()
|
ydl.cache.remove()
|
||||||
|
|
||||||
|
# Update version
|
||||||
|
if opts.update_self:
|
||||||
|
# If updater returns True, exit. Required for windows
|
||||||
|
if update_self(ydl.to_screen, opts.verbose, ydl._opener):
|
||||||
|
if actual_use:
|
||||||
|
sys.exit('ERROR: The program must exit for the update to complete')
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
# Maybe do nothing
|
# Maybe do nothing
|
||||||
if (len(all_urls) < 1) and (opts.load_info_filename is None):
|
if not actual_use:
|
||||||
if opts.update_self or opts.rm_cachedir:
|
if opts.update_self or opts.rm_cachedir:
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,33 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from ..utils import (
|
||||||
|
determine_protocol,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_real_downloader(info_dict, protocol=None, *args, **kwargs):
|
||||||
|
info_copy = info_dict.copy()
|
||||||
|
if protocol:
|
||||||
|
info_copy['protocol'] = protocol
|
||||||
|
return get_suitable_downloader(info_copy, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# Some of these require _get_real_downloader
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
|
from .dash import DashSegmentsFD
|
||||||
from .f4m import F4mFD
|
from .f4m import F4mFD
|
||||||
from .hls import HlsFD
|
from .hls import HlsFD
|
||||||
from .http import HttpFD
|
from .http import HttpFD
|
||||||
from .rtmp import RtmpFD
|
from .rtmp import RtmpFD
|
||||||
from .dash import DashSegmentsFD
|
|
||||||
from .rtsp import RtspFD
|
from .rtsp import RtspFD
|
||||||
from .ism import IsmFD
|
from .ism import IsmFD
|
||||||
|
from .niconico import NiconicoDmcFD
|
||||||
from .youtube_live_chat import YoutubeLiveChatReplayFD
|
from .youtube_live_chat import YoutubeLiveChatReplayFD
|
||||||
from .external import (
|
from .external import (
|
||||||
get_external_downloader,
|
get_external_downloader,
|
||||||
FFmpegFD,
|
FFmpegFD,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..utils import (
|
|
||||||
determine_protocol,
|
|
||||||
)
|
|
||||||
|
|
||||||
PROTOCOL_MAP = {
|
PROTOCOL_MAP = {
|
||||||
'rtmp': RtmpFD,
|
'rtmp': RtmpFD,
|
||||||
'm3u8_native': HlsFD,
|
'm3u8_native': HlsFD,
|
||||||
@@ -27,11 +37,12 @@ PROTOCOL_MAP = {
|
|||||||
'f4m': F4mFD,
|
'f4m': F4mFD,
|
||||||
'http_dash_segments': DashSegmentsFD,
|
'http_dash_segments': DashSegmentsFD,
|
||||||
'ism': IsmFD,
|
'ism': IsmFD,
|
||||||
|
'niconico_dmc': NiconicoDmcFD,
|
||||||
'youtube_live_chat_replay': YoutubeLiveChatReplayFD,
|
'youtube_live_chat_replay': YoutubeLiveChatReplayFD,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_suitable_downloader(info_dict, params={}):
|
def get_suitable_downloader(info_dict, params={}, default=HttpFD):
|
||||||
"""Get the downloader class that can handle the info dict."""
|
"""Get the downloader class that can handle the info dict."""
|
||||||
protocol = determine_protocol(info_dict)
|
protocol = determine_protocol(info_dict)
|
||||||
info_dict['protocol'] = protocol
|
info_dict['protocol'] = protocol
|
||||||
@@ -45,16 +56,17 @@ def get_suitable_downloader(info_dict, params={}):
|
|||||||
if ed.can_download(info_dict):
|
if ed.can_download(info_dict):
|
||||||
return ed
|
return ed
|
||||||
|
|
||||||
if protocol.startswith('m3u8') and info_dict.get('is_live'):
|
if protocol.startswith('m3u8'):
|
||||||
|
if info_dict.get('is_live'):
|
||||||
return FFmpegFD
|
return FFmpegFD
|
||||||
|
elif _get_real_downloader(info_dict, 'frag_urls', params, None):
|
||||||
if protocol == 'm3u8' and params.get('hls_prefer_native') is True:
|
|
||||||
return HlsFD
|
return HlsFD
|
||||||
|
elif params.get('hls_prefer_native') is True:
|
||||||
if protocol == 'm3u8_native' and params.get('hls_prefer_native') is False:
|
return HlsFD
|
||||||
|
elif params.get('hls_prefer_native') is False:
|
||||||
return FFmpegFD
|
return FFmpegFD
|
||||||
|
|
||||||
return PROTOCOL_MAP.get(protocol, HttpFD)
|
return PROTOCOL_MAP.get(protocol, default)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ class FileDownloader(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
nooverwrites_and_exists = (
|
nooverwrites_and_exists = (
|
||||||
not self.params.get('overwrites', True)
|
not self.params.get('overwrites', subtitle)
|
||||||
and os.path.exists(encodeFilename(filename))
|
and os.path.exists(encodeFilename(filename))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
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,
|
||||||
@@ -20,31 +22,42 @@ class DashSegmentsFD(FragmentFD):
|
|||||||
fragments = info_dict['fragments'][:1] if self.params.get(
|
fragments = info_dict['fragments'][:1] if self.params.get(
|
||||||
'test', False) else info_dict['fragments']
|
'test', False) else info_dict['fragments']
|
||||||
|
|
||||||
|
real_downloader = _get_real_downloader(info_dict, 'frag_urls', self.params, None)
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
'filename': filename,
|
'filename': filename,
|
||||||
'total_frags': len(fragments),
|
'total_frags': len(fragments),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if real_downloader:
|
||||||
|
self._prepare_external_frag_download(ctx)
|
||||||
|
else:
|
||||||
self._prepare_and_start_frag_download(ctx)
|
self._prepare_and_start_frag_download(ctx)
|
||||||
|
|
||||||
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 = []
|
||||||
frag_index = 0
|
frag_index = 0
|
||||||
for i, fragment in enumerate(fragments):
|
for i, fragment in enumerate(fragments):
|
||||||
frag_index += 1
|
frag_index += 1
|
||||||
if frag_index <= ctx['fragment_index']:
|
if frag_index <= ctx['fragment_index']:
|
||||||
continue
|
continue
|
||||||
|
fragment_url = fragment.get('url')
|
||||||
|
if not fragment_url:
|
||||||
|
assert fragment_base_url
|
||||||
|
fragment_url = urljoin(fragment_base_url, fragment['path'])
|
||||||
|
|
||||||
|
if real_downloader:
|
||||||
|
fragment_urls.append(fragment_url)
|
||||||
|
continue
|
||||||
|
|
||||||
# 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
|
||||||
fatal = i == 0 or not skip_unavailable_fragments
|
fatal = i == 0 or not skip_unavailable_fragments
|
||||||
count = 0
|
count = 0
|
||||||
while count <= fragment_retries:
|
while count <= fragment_retries:
|
||||||
try:
|
try:
|
||||||
fragment_url = fragment.get('url')
|
|
||||||
if not fragment_url:
|
|
||||||
assert fragment_base_url
|
|
||||||
fragment_url = urljoin(fragment_base_url, fragment['path'])
|
|
||||||
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
|
||||||
@@ -75,6 +88,16 @@ class DashSegmentsFD(FragmentFD):
|
|||||||
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
|
||||||
|
|
||||||
|
if real_downloader:
|
||||||
|
info_copy = info_dict.copy()
|
||||||
|
info_copy['url_list'] = fragment_urls
|
||||||
|
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:
|
||||||
self._finish_frag_download(ctx)
|
self._finish_frag_download(ctx)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
try:
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
can_decrypt_frag = True
|
||||||
|
except ImportError:
|
||||||
|
can_decrypt_frag = False
|
||||||
|
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_setenv,
|
compat_setenv,
|
||||||
@@ -18,15 +24,20 @@ from ..utils import (
|
|||||||
cli_bool_option,
|
cli_bool_option,
|
||||||
cli_configuration_args,
|
cli_configuration_args,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
|
error_to_compat_str,
|
||||||
encodeArgument,
|
encodeArgument,
|
||||||
handle_youtubedl_headers,
|
handle_youtubedl_headers,
|
||||||
check_executable,
|
check_executable,
|
||||||
is_outdated_version,
|
is_outdated_version,
|
||||||
process_communicate_or_kill,
|
process_communicate_or_kill,
|
||||||
|
sanitized_Request,
|
||||||
|
sanitize_open,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExternalFD(FileDownloader):
|
class ExternalFD(FileDownloader):
|
||||||
|
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps')
|
||||||
|
|
||||||
def real_download(self, filename, info_dict):
|
def real_download(self, filename, info_dict):
|
||||||
self.report_destination(filename)
|
self.report_destination(filename)
|
||||||
tmpfilename = self.temp_name(filename)
|
tmpfilename = self.temp_name(filename)
|
||||||
@@ -79,7 +90,7 @@ class ExternalFD(FileDownloader):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def supports(cls, info_dict):
|
def supports(cls, info_dict):
|
||||||
return info_dict['protocol'] in ('http', 'https', 'ftp', 'ftps')
|
return info_dict['protocol'] in cls.SUPPORTED_PROTOCOLS
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def can_download(cls, info_dict):
|
def can_download(cls, info_dict):
|
||||||
@@ -109,8 +120,53 @@ class ExternalFD(FileDownloader):
|
|||||||
_, stderr = process_communicate_or_kill(p)
|
_, stderr = process_communicate_or_kill(p)
|
||||||
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:
|
||||||
|
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')
|
||||||
|
for i, file in enumerate(file_list):
|
||||||
|
src, _ = sanitize_open(file, 'rb')
|
||||||
|
if key_list:
|
||||||
|
decrypt_info = next((x for x in key_list if x['INDEX'] == i), decrypt_info)
|
||||||
|
if decrypt_info['METHOD'] == 'AES-128':
|
||||||
|
iv = decrypt_info.get('IV')
|
||||||
|
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
|
||||||
|
self._prepare_url(info_dict, info_dict.get('_decryption_key_url') or decrypt_info['URI'])).read()
|
||||||
|
encrypted_data = src.read()
|
||||||
|
decrypted_data = AES.new(
|
||||||
|
decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(encrypted_data)
|
||||||
|
dest.write(decrypted_data)
|
||||||
|
else:
|
||||||
|
fragment_data = src.read()
|
||||||
|
dest.write(fragment_data)
|
||||||
|
else:
|
||||||
|
fragment_data = src.read()
|
||||||
|
dest.write(fragment_data)
|
||||||
|
src.close()
|
||||||
|
dest.close()
|
||||||
|
if not self.params.get('keep_fragments', False):
|
||||||
|
for file_path in file_list:
|
||||||
|
try:
|
||||||
|
os.remove(file_path)
|
||||||
|
except OSError as ose:
|
||||||
|
self.report_error("Unable to delete file %s; %s" % (file_path, error_to_compat_str(ose)))
|
||||||
|
try:
|
||||||
|
file_path = '%s.frag.urls' % tmpfilename
|
||||||
|
os.remove(file_path)
|
||||||
|
except OSError as ose:
|
||||||
|
self.report_error("Unable to delete file %s; %s" % (file_path, error_to_compat_str(ose)))
|
||||||
|
|
||||||
return p.returncode
|
return p.returncode
|
||||||
|
|
||||||
|
def _prepare_url(self, info_dict, url):
|
||||||
|
headers = info_dict.get('http_headers')
|
||||||
|
return sanitized_Request(url, None, headers) if headers else url
|
||||||
|
|
||||||
|
|
||||||
class CurlFD(ExternalFD):
|
class CurlFD(ExternalFD):
|
||||||
AVAILABLE_OPT = '-V'
|
AVAILABLE_OPT = '-V'
|
||||||
@@ -186,15 +242,17 @@ class WgetFD(ExternalFD):
|
|||||||
|
|
||||||
class Aria2cFD(ExternalFD):
|
class Aria2cFD(ExternalFD):
|
||||||
AVAILABLE_OPT = '-v'
|
AVAILABLE_OPT = '-v'
|
||||||
|
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'frag_urls')
|
||||||
|
|
||||||
def _make_cmd(self, tmpfilename, info_dict):
|
def _make_cmd(self, tmpfilename, info_dict):
|
||||||
cmd = [self.exe, '-c']
|
cmd = [self.exe, '-c']
|
||||||
cmd += self._configuration_args([
|
|
||||||
'--min-split-size', '1M', '--max-connection-per-server', '4'])
|
|
||||||
dn = os.path.dirname(tmpfilename)
|
dn = os.path.dirname(tmpfilename)
|
||||||
|
if 'url_list' not in info_dict:
|
||||||
|
cmd += ['--out', os.path.basename(tmpfilename)]
|
||||||
|
verbose_level_args = ['--console-log-level=warn', '--summary-interval=0']
|
||||||
|
cmd += self._configuration_args(['--file-allocation=none', '-x16', '-j16', '-s16'] + verbose_level_args)
|
||||||
if dn:
|
if dn:
|
||||||
cmd += ['--dir', dn]
|
cmd += ['--dir', dn]
|
||||||
cmd += ['--out', os.path.basename(tmpfilename)]
|
|
||||||
if info_dict.get('http_headers') is not None:
|
if info_dict.get('http_headers') is not None:
|
||||||
for key, val in info_dict['http_headers'].items():
|
for key, val in info_dict['http_headers'].items():
|
||||||
cmd += ['--header', '%s: %s' % (key, val)]
|
cmd += ['--header', '%s: %s' % (key, val)]
|
||||||
@@ -202,6 +260,21 @@ class Aria2cFD(ExternalFD):
|
|||||||
cmd += self._option('--all-proxy', 'proxy')
|
cmd += self._option('--all-proxy', 'proxy')
|
||||||
cmd += self._bool_option('--check-certificate', 'nocheckcertificate', 'false', 'true', '=')
|
cmd += self._bool_option('--check-certificate', 'nocheckcertificate', 'false', 'true', '=')
|
||||||
cmd += self._bool_option('--remote-time', 'updatetime', 'true', 'false', '=')
|
cmd += self._bool_option('--remote-time', 'updatetime', 'true', 'false', '=')
|
||||||
|
cmd += ['--auto-file-renaming=false']
|
||||||
|
if 'url_list' in info_dict:
|
||||||
|
cmd += verbose_level_args
|
||||||
|
cmd += ['--uri-selector', 'inorder', '--download-result=hide']
|
||||||
|
url_list_file = '%s.frag.urls' % tmpfilename
|
||||||
|
url_list = []
|
||||||
|
for [i, url] in enumerate(info_dict['url_list']):
|
||||||
|
tmpsegmentname = '%s_%s.frag' % (os.path.basename(tmpfilename), i)
|
||||||
|
url_list.append('%s\n\tout=%s' % (url, tmpsegmentname))
|
||||||
|
stream, _ = sanitize_open(url_list_file, 'wb')
|
||||||
|
stream.write('\n'.join(url_list).encode('utf-8'))
|
||||||
|
stream.close()
|
||||||
|
|
||||||
|
cmd += ['-i', url_list_file]
|
||||||
|
else:
|
||||||
cmd += ['--', info_dict['url']]
|
cmd += ['--', info_dict['url']]
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
@@ -221,9 +294,7 @@ class HttpieFD(ExternalFD):
|
|||||||
|
|
||||||
|
|
||||||
class FFmpegFD(ExternalFD):
|
class FFmpegFD(ExternalFD):
|
||||||
@classmethod
|
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'm3u8', 'rtsp', 'rtmp', 'mms')
|
||||||
def supports(cls, info_dict):
|
|
||||||
return info_dict['protocol'] in ('http', 'https', 'ftp', 'ftps', 'm3u8', 'rtsp', 'rtmp', 'mms')
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def available(cls):
|
def available(cls):
|
||||||
@@ -233,7 +304,7 @@ class FFmpegFD(ExternalFD):
|
|||||||
url = info_dict['url']
|
url = info_dict['url']
|
||||||
ffpp = FFmpegPostProcessor(downloader=self)
|
ffpp = FFmpegPostProcessor(downloader=self)
|
||||||
if not ffpp.available:
|
if not ffpp.available:
|
||||||
self.report_error('m3u8 download detected but ffmpeg or avconv could not be found. Please install one.')
|
self.report_error('m3u8 download detected but ffmpeg could not be found. Please install')
|
||||||
return False
|
return False
|
||||||
ffpp.check_version()
|
ffpp.check_version()
|
||||||
|
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ class F4mFD(FragmentFD):
|
|||||||
media = doc.findall(_add_ns('media'))
|
media = doc.findall(_add_ns('media'))
|
||||||
if not media:
|
if not media:
|
||||||
self.report_error('No media found')
|
self.report_error('No media found')
|
||||||
|
if not self.params.get('allow_unplayable_formats'):
|
||||||
for e in (doc.findall(_add_ns('drmAdditionalHeader'))
|
for e in (doc.findall(_add_ns('drmAdditionalHeader'))
|
||||||
+ doc.findall(_add_ns('drmAdditionalHeaderSet'))):
|
+ doc.findall(_add_ns('drmAdditionalHeaderSet'))):
|
||||||
# If id attribute is missing it's valid for all media nodes
|
# If id attribute is missing it's valid for all media nodes
|
||||||
|
|||||||
@@ -95,11 +95,12 @@ class FragmentFD(FileDownloader):
|
|||||||
frag_index_stream.write(json.dumps({'downloader': downloader}))
|
frag_index_stream.write(json.dumps({'downloader': downloader}))
|
||||||
frag_index_stream.close()
|
frag_index_stream.close()
|
||||||
|
|
||||||
def _download_fragment(self, ctx, frag_url, info_dict, headers=None):
|
def _download_fragment(self, ctx, frag_url, info_dict, headers=None, request_data=None):
|
||||||
fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], ctx['fragment_index'])
|
fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], ctx['fragment_index'])
|
||||||
fragment_info_dict = {
|
fragment_info_dict = {
|
||||||
'url': frag_url,
|
'url': frag_url,
|
||||||
'http_headers': headers or info_dict.get('http_headers'),
|
'http_headers': headers or info_dict.get('http_headers'),
|
||||||
|
'request_data': request_data,
|
||||||
}
|
}
|
||||||
success = ctx['dl'].download(fragment_filename, fragment_info_dict)
|
success = ctx['dl'].download(fragment_filename, fragment_info_dict)
|
||||||
if not success:
|
if not success:
|
||||||
@@ -277,3 +278,24 @@ class FragmentFD(FileDownloader):
|
|||||||
'status': 'finished',
|
'status': 'finished',
|
||||||
'elapsed': elapsed,
|
'elapsed': elapsed,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def _prepare_external_frag_download(self, ctx):
|
||||||
|
if 'live' not in ctx:
|
||||||
|
ctx['live'] = False
|
||||||
|
if not ctx['live']:
|
||||||
|
total_frags_str = '%d' % ctx['total_frags']
|
||||||
|
ad_frags = ctx.get('ad_frags', 0)
|
||||||
|
if ad_frags:
|
||||||
|
total_frags_str += ' (not including %d ad)' % ad_frags
|
||||||
|
else:
|
||||||
|
total_frags_str = 'unknown (live)'
|
||||||
|
self.to_screen(
|
||||||
|
'[%s] Total fragments: %s' % (self.FD_NAME, total_frags_str))
|
||||||
|
|
||||||
|
tmpfilename = self.temp_name(ctx['filename'])
|
||||||
|
|
||||||
|
# Should be initialized before ytdl file check
|
||||||
|
ctx.update({
|
||||||
|
'tmpfilename': tmpfilename,
|
||||||
|
'fragment_index': 0,
|
||||||
|
})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
can_decrypt_frag = False
|
can_decrypt_frag = False
|
||||||
|
|
||||||
|
from ..downloader import _get_real_downloader
|
||||||
from .fragment import FragmentFD
|
from .fragment import FragmentFD
|
||||||
from .external import FFmpegFD
|
from .external import FFmpegFD
|
||||||
|
|
||||||
@@ -28,9 +29,8 @@ class HlsFD(FragmentFD):
|
|||||||
FD_NAME = 'hlsnative'
|
FD_NAME = 'hlsnative'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def can_download(manifest, info_dict):
|
def can_download(manifest, info_dict, allow_unplayable_formats=False):
|
||||||
UNSUPPORTED_FEATURES = (
|
UNSUPPORTED_FEATURES = [
|
||||||
r'#EXT-X-KEY:METHOD=(?!NONE|AES-128)', # encrypted streams [1]
|
|
||||||
# r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [2]
|
# r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [2]
|
||||||
|
|
||||||
# Live streams heuristic does not always work (e.g. geo restricted to Germany
|
# Live streams heuristic does not always work (e.g. geo restricted to Germany
|
||||||
@@ -49,7 +49,11 @@ class HlsFD(FragmentFD):
|
|||||||
# 3. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3.2
|
# 3. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3.2
|
||||||
# 4. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3.5
|
# 4. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3.5
|
||||||
# 5. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.5
|
# 5. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.5
|
||||||
)
|
]
|
||||||
|
if not allow_unplayable_formats:
|
||||||
|
UNSUPPORTED_FEATURES += [
|
||||||
|
r'#EXT-X-KEY:METHOD=(?!NONE|AES-128)', # encrypted streams [1]
|
||||||
|
]
|
||||||
check_results = [not re.search(feature, manifest) for feature in UNSUPPORTED_FEATURES]
|
check_results = [not re.search(feature, manifest) for feature in UNSUPPORTED_FEATURES]
|
||||||
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(can_decrypt_frag or not is_aes128_enc)
|
check_results.append(can_decrypt_frag or not is_aes128_enc)
|
||||||
@@ -65,7 +69,7 @@ class HlsFD(FragmentFD):
|
|||||||
man_url = urlh.geturl()
|
man_url = urlh.geturl()
|
||||||
s = urlh.read().decode('utf-8', 'ignore')
|
s = urlh.read().decode('utf-8', 'ignore')
|
||||||
|
|
||||||
if not self.can_download(s, info_dict):
|
if not self.can_download(s, info_dict, self.params.get('allow_unplayable_formats')):
|
||||||
if info_dict.get('extra_param_to_segment_url') or info_dict.get('_decryption_key_url'):
|
if info_dict.get('extra_param_to_segment_url') or info_dict.get('_decryption_key_url'):
|
||||||
self.report_error('pycrypto not found. Please install it.')
|
self.report_error('pycrypto not found. Please install it.')
|
||||||
return False
|
return False
|
||||||
@@ -73,10 +77,13 @@ class HlsFD(FragmentFD):
|
|||||||
'hlsnative has detected features it does not support, '
|
'hlsnative has detected features it does not support, '
|
||||||
'extraction will be delegated to ffmpeg')
|
'extraction will be delegated to ffmpeg')
|
||||||
fd = FFmpegFD(self.ydl, self.params)
|
fd = FFmpegFD(self.ydl, self.params)
|
||||||
for ph in self._progress_hooks:
|
# TODO: Make progress updates work without hooking twice
|
||||||
fd.add_progress_hook(ph)
|
# for ph in self._progress_hooks:
|
||||||
|
# fd.add_progress_hook(ph)
|
||||||
return fd.real_download(filename, info_dict)
|
return fd.real_download(filename, info_dict)
|
||||||
|
|
||||||
|
real_downloader = _get_real_downloader(info_dict, 'frag_urls', self.params, None)
|
||||||
|
|
||||||
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
|
||||||
or s.startswith('#UPLYNK-SEGMENT') and s.endswith(',ad'))
|
or s.startswith('#UPLYNK-SEGMENT') and s.endswith(',ad'))
|
||||||
@@ -85,6 +92,8 @@ 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 = []
|
||||||
|
|
||||||
media_frags = 0
|
media_frags = 0
|
||||||
ad_frags = 0
|
ad_frags = 0
|
||||||
ad_frag_next = False
|
ad_frag_next = False
|
||||||
@@ -109,6 +118,9 @@ class HlsFD(FragmentFD):
|
|||||||
'ad_frags': ad_frags,
|
'ad_frags': ad_frags,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if real_downloader:
|
||||||
|
self._prepare_external_frag_download(ctx)
|
||||||
|
else:
|
||||||
self._prepare_and_start_frag_download(ctx)
|
self._prepare_and_start_frag_download(ctx)
|
||||||
|
|
||||||
fragment_retries = self.params.get('fragment_retries', 0)
|
fragment_retries = self.params.get('fragment_retries', 0)
|
||||||
@@ -122,6 +134,7 @@ 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 = {}
|
||||||
frag_index = 0
|
frag_index = 0
|
||||||
ad_frag_next = False
|
ad_frag_next = False
|
||||||
@@ -140,6 +153,11 @@ class HlsFD(FragmentFD):
|
|||||||
else compat_urlparse.urljoin(man_url, line))
|
else compat_urlparse.urljoin(man_url, line))
|
||||||
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)
|
||||||
|
continue
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
headers = info_dict.get('http_headers', {})
|
headers = info_dict.get('http_headers', {})
|
||||||
if byte_range:
|
if byte_range:
|
||||||
@@ -168,6 +186,7 @@ class HlsFD(FragmentFD):
|
|||||||
self.report_error(
|
self.report_error(
|
||||||
'giving up after %s fragment retries' % fragment_retries)
|
'giving up after %s fragment retries' % fragment_retries)
|
||||||
return False
|
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)
|
||||||
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
|
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
|
||||||
@@ -197,6 +216,10 @@ 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:])
|
||||||
elif line.startswith('#EXT-X-BYTERANGE'):
|
elif line.startswith('#EXT-X-BYTERANGE'):
|
||||||
@@ -211,6 +234,17 @@ class HlsFD(FragmentFD):
|
|||||||
elif is_ad_fragment_end(line):
|
elif is_ad_fragment_end(line):
|
||||||
ad_frag_next = False
|
ad_frag_next = False
|
||||||
|
|
||||||
|
if real_downloader:
|
||||||
|
info_copy = info_dict.copy()
|
||||||
|
info_copy['url_list'] = fragment_urls
|
||||||
|
info_copy['key_list'] = key_list
|
||||||
|
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:
|
||||||
self._finish_frag_download(ctx)
|
self._finish_frag_download(ctx)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from ..utils import (
|
|||||||
class HttpFD(FileDownloader):
|
class HttpFD(FileDownloader):
|
||||||
def real_download(self, filename, info_dict):
|
def real_download(self, filename, info_dict):
|
||||||
url = info_dict['url']
|
url = info_dict['url']
|
||||||
|
request_data = info_dict.get('request_data', None)
|
||||||
|
|
||||||
class DownloadContext(dict):
|
class DownloadContext(dict):
|
||||||
__getattr__ = dict.get
|
__getattr__ = dict.get
|
||||||
@@ -101,7 +102,7 @@ class HttpFD(FileDownloader):
|
|||||||
range_end = ctx.data_len - 1
|
range_end = ctx.data_len - 1
|
||||||
has_range = range_start is not None
|
has_range = range_start is not None
|
||||||
ctx.has_range = has_range
|
ctx.has_range = has_range
|
||||||
request = sanitized_Request(url, None, headers)
|
request = sanitized_Request(url, request_data, headers)
|
||||||
if has_range:
|
if has_range:
|
||||||
set_range(request, range_start, range_end)
|
set_range(request, range_start, range_end)
|
||||||
# Establish connection
|
# Establish connection
|
||||||
@@ -152,7 +153,7 @@ class HttpFD(FileDownloader):
|
|||||||
try:
|
try:
|
||||||
# Open the connection again without the range header
|
# Open the connection again without the range header
|
||||||
ctx.data = self.ydl.urlopen(
|
ctx.data = self.ydl.urlopen(
|
||||||
sanitized_Request(url, None, headers))
|
sanitized_Request(url, request_data, headers))
|
||||||
content_length = ctx.data.info()['Content-Length']
|
content_length = ctx.data.info()['Content-Length']
|
||||||
except (compat_urllib_error.HTTPError, ) as err:
|
except (compat_urllib_error.HTTPError, ) as err:
|
||||||
if err.code < 500 or err.code >= 600:
|
if err.code < 500 or err.code >= 600:
|
||||||
|
|||||||
54
youtube_dlc/downloader/niconico.py
Normal file
54
youtube_dlc/downloader/niconico.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from .common import FileDownloader
|
||||||
|
from ..downloader import _get_real_downloader
|
||||||
|
from ..extractor.niconico import NiconicoIE
|
||||||
|
from ..compat import compat_urllib_request
|
||||||
|
|
||||||
|
|
||||||
|
class NiconicoDmcFD(FileDownloader):
|
||||||
|
""" Downloading niconico douga from DMC with heartbeat """
|
||||||
|
|
||||||
|
FD_NAME = 'niconico_dmc'
|
||||||
|
|
||||||
|
def real_download(self, filename, info_dict):
|
||||||
|
self.to_screen('[%s] Downloading from DMC' % self.FD_NAME)
|
||||||
|
|
||||||
|
ie = NiconicoIE(self.ydl)
|
||||||
|
info_dict, heartbeat_info_dict = ie._get_heartbeat_info(info_dict)
|
||||||
|
|
||||||
|
fd = _get_real_downloader(info_dict, params=self.params)(self.ydl, self.params)
|
||||||
|
|
||||||
|
success = download_complete = False
|
||||||
|
timer = [None]
|
||||||
|
|
||||||
|
heartbeat_lock = threading.Lock()
|
||||||
|
heartbeat_url = heartbeat_info_dict['url']
|
||||||
|
heartbeat_data = heartbeat_info_dict['data']
|
||||||
|
heartbeat_interval = heartbeat_info_dict.get('interval', 30)
|
||||||
|
self.to_screen('[%s] Heartbeat with %s second interval...' % (self.FD_NAME, heartbeat_interval))
|
||||||
|
|
||||||
|
def heartbeat():
|
||||||
|
try:
|
||||||
|
compat_urllib_request.urlopen(url=heartbeat_url, data=heartbeat_data.encode())
|
||||||
|
except Exception:
|
||||||
|
self.to_screen('[%s] Heartbeat failed' % self.FD_NAME)
|
||||||
|
|
||||||
|
with heartbeat_lock:
|
||||||
|
if not download_complete:
|
||||||
|
timer[0] = threading.Timer(heartbeat_interval, heartbeat)
|
||||||
|
timer[0].start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
heartbeat()
|
||||||
|
success = fd.real_download(filename, info_dict)
|
||||||
|
finally:
|
||||||
|
if heartbeat_lock:
|
||||||
|
with heartbeat_lock:
|
||||||
|
timer[0].cancel()
|
||||||
|
download_complete = True
|
||||||
|
|
||||||
|
return success
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
from __future__ import division, unicode_literals
|
from __future__ import division, unicode_literals
|
||||||
|
|
||||||
import re
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from .fragment import FragmentFD
|
from .fragment import FragmentFD
|
||||||
|
from ..compat import compat_urllib_error
|
||||||
|
from ..utils import (
|
||||||
|
try_get,
|
||||||
|
RegexNotFoundError,
|
||||||
|
)
|
||||||
|
from ..extractor.youtube import YoutubeBaseInfoExtractor as YT_BaseIE
|
||||||
|
|
||||||
|
|
||||||
class YoutubeLiveChatReplayFD(FragmentFD):
|
class YoutubeLiveChatReplayFD(FragmentFD):
|
||||||
@@ -15,6 +20,7 @@ class YoutubeLiveChatReplayFD(FragmentFD):
|
|||||||
video_id = info_dict['video_id']
|
video_id = info_dict['video_id']
|
||||||
self.to_screen('[%s] Downloading live chat' % self.FD_NAME)
|
self.to_screen('[%s] Downloading live chat' % self.FD_NAME)
|
||||||
|
|
||||||
|
fragment_retries = self.params.get('fragment_retries', 0)
|
||||||
test = self.params.get('test', False)
|
test = self.params.get('test', False)
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
@@ -23,19 +29,53 @@ class YoutubeLiveChatReplayFD(FragmentFD):
|
|||||||
'total_frags': None,
|
'total_frags': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def dl_fragment(url):
|
ie = YT_BaseIE(self.ydl)
|
||||||
headers = info_dict.get('http_headers', {})
|
|
||||||
return self._download_fragment(ctx, url, info_dict, headers)
|
|
||||||
|
|
||||||
def parse_yt_initial_data(data):
|
def dl_fragment(url, data=None, headers=None):
|
||||||
window_patt = b'window\\["ytInitialData"\\]\\s*=\\s*(.*?)(?<=});'
|
http_headers = info_dict.get('http_headers', {})
|
||||||
var_patt = b'var\\s+ytInitialData\\s*=\\s*(.*?)(?<=});'
|
if headers:
|
||||||
for patt in window_patt, var_patt:
|
http_headers = http_headers.copy()
|
||||||
|
http_headers.update(headers)
|
||||||
|
return self._download_fragment(ctx, url, info_dict, http_headers, data)
|
||||||
|
|
||||||
|
def download_and_parse_fragment(url, frag_index, request_data):
|
||||||
|
count = 0
|
||||||
|
while count <= fragment_retries:
|
||||||
try:
|
try:
|
||||||
raw_json = re.search(patt, data).group(1)
|
success, raw_fragment = dl_fragment(url, request_data, {'content-type': 'application/json'})
|
||||||
return json.loads(raw_json)
|
if not success:
|
||||||
except AttributeError:
|
return False, None, None
|
||||||
continue
|
try:
|
||||||
|
data = ie._extract_yt_initial_data(video_id, raw_fragment.decode('utf-8', 'replace'))
|
||||||
|
except RegexNotFoundError:
|
||||||
|
data = None
|
||||||
|
if not data:
|
||||||
|
data = json.loads(raw_fragment)
|
||||||
|
live_chat_continuation = try_get(
|
||||||
|
data,
|
||||||
|
lambda x: x['continuationContents']['liveChatContinuation'], dict) or {}
|
||||||
|
offset = continuation_id = None
|
||||||
|
processed_fragment = bytearray()
|
||||||
|
for action in live_chat_continuation.get('actions', []):
|
||||||
|
if 'replayChatItemAction' in action:
|
||||||
|
replay_chat_item_action = action['replayChatItemAction']
|
||||||
|
offset = int(replay_chat_item_action['videoOffsetTimeMsec'])
|
||||||
|
processed_fragment.extend(
|
||||||
|
json.dumps(action, ensure_ascii=False).encode('utf-8') + b'\n')
|
||||||
|
if offset is not None:
|
||||||
|
continuation_id = try_get(
|
||||||
|
live_chat_continuation,
|
||||||
|
lambda x: x['continuations'][0]['liveChatReplayContinuationData']['continuation'])
|
||||||
|
self._append_fragment(ctx, processed_fragment)
|
||||||
|
|
||||||
|
return True, continuation_id, offset
|
||||||
|
except compat_urllib_error.HTTPError as err:
|
||||||
|
count += 1
|
||||||
|
if count <= fragment_retries:
|
||||||
|
self.report_retry_fragment(err, frag_index, count, fragment_retries)
|
||||||
|
if count > fragment_retries:
|
||||||
|
self.report_error('giving up after %s fragment retries' % fragment_retries)
|
||||||
|
return False, None, None
|
||||||
|
|
||||||
self._prepare_and_start_frag_download(ctx)
|
self._prepare_and_start_frag_download(ctx)
|
||||||
|
|
||||||
@@ -43,55 +83,41 @@ class YoutubeLiveChatReplayFD(FragmentFD):
|
|||||||
'https://www.youtube.com/watch?v={}'.format(video_id))
|
'https://www.youtube.com/watch?v={}'.format(video_id))
|
||||||
if not success:
|
if not success:
|
||||||
return False
|
return False
|
||||||
data = parse_yt_initial_data(raw_fragment)
|
try:
|
||||||
continuation_id = data['contents']['twoColumnWatchNextResults']['conversationBar']['liveChatRenderer']['continuations'][0]['reloadContinuationData']['continuation']
|
data = ie._extract_yt_initial_data(video_id, raw_fragment.decode('utf-8', 'replace'))
|
||||||
|
except RegexNotFoundError:
|
||||||
|
return False
|
||||||
|
continuation_id = try_get(
|
||||||
|
data,
|
||||||
|
lambda x: x['contents']['twoColumnWatchNextResults']['conversationBar']['liveChatRenderer']['continuations'][0]['reloadContinuationData']['continuation'])
|
||||||
# no data yet but required to call _append_fragment
|
# no data yet but required to call _append_fragment
|
||||||
self._append_fragment(ctx, b'')
|
self._append_fragment(ctx, b'')
|
||||||
|
|
||||||
first = True
|
ytcfg = ie._extract_ytcfg(video_id, raw_fragment.decode('utf-8', 'replace'))
|
||||||
offset = None
|
|
||||||
|
if not ytcfg:
|
||||||
|
return False
|
||||||
|
api_key = try_get(ytcfg, lambda x: x['INNERTUBE_API_KEY'])
|
||||||
|
innertube_context = try_get(ytcfg, lambda x: x['INNERTUBE_CONTEXT'])
|
||||||
|
if not api_key or not innertube_context:
|
||||||
|
return False
|
||||||
|
url = 'https://www.youtube.com/youtubei/v1/live_chat/get_live_chat_replay?key=' + api_key
|
||||||
|
|
||||||
|
frag_index = offset = 0
|
||||||
while continuation_id is not None:
|
while continuation_id is not None:
|
||||||
data = None
|
frag_index += 1
|
||||||
if first:
|
request_data = {
|
||||||
url = 'https://www.youtube.com/live_chat_replay?continuation={}'.format(continuation_id)
|
'context': innertube_context,
|
||||||
success, raw_fragment = dl_fragment(url)
|
'continuation': continuation_id,
|
||||||
|
}
|
||||||
|
if frag_index > 1:
|
||||||
|
request_data['currentPlayerState'] = {'playerOffsetMs': str(max(offset - 5000, 0))}
|
||||||
|
success, continuation_id, offset = download_and_parse_fragment(
|
||||||
|
url, frag_index, json.dumps(request_data, ensure_ascii=False).encode('utf-8') + b'\n')
|
||||||
if not success:
|
if not success:
|
||||||
return False
|
return False
|
||||||
data = parse_yt_initial_data(raw_fragment)
|
if test:
|
||||||
else:
|
|
||||||
url = ('https://www.youtube.com/live_chat_replay/get_live_chat_replay'
|
|
||||||
+ '?continuation={}'.format(continuation_id)
|
|
||||||
+ '&playerOffsetMs={}'.format(max(offset - 5000, 0))
|
|
||||||
+ '&hidden=false'
|
|
||||||
+ '&pbj=1')
|
|
||||||
success, raw_fragment = dl_fragment(url)
|
|
||||||
if not success:
|
|
||||||
return False
|
|
||||||
data = json.loads(raw_fragment)['response']
|
|
||||||
|
|
||||||
first = False
|
|
||||||
continuation_id = None
|
|
||||||
|
|
||||||
live_chat_continuation = data['continuationContents']['liveChatContinuation']
|
|
||||||
offset = None
|
|
||||||
processed_fragment = bytearray()
|
|
||||||
if 'actions' in live_chat_continuation:
|
|
||||||
for action in live_chat_continuation['actions']:
|
|
||||||
if 'replayChatItemAction' in action:
|
|
||||||
replay_chat_item_action = action['replayChatItemAction']
|
|
||||||
offset = int(replay_chat_item_action['videoOffsetTimeMsec'])
|
|
||||||
processed_fragment.extend(
|
|
||||||
json.dumps(action, ensure_ascii=False).encode('utf-8') + b'\n')
|
|
||||||
try:
|
|
||||||
continuation_id = live_chat_continuation['continuations'][0]['liveChatReplayContinuationData']['continuation']
|
|
||||||
except KeyError:
|
|
||||||
continuation_id = None
|
|
||||||
|
|
||||||
self._append_fragment(ctx, processed_fragment)
|
|
||||||
|
|
||||||
if test or offset is None:
|
|
||||||
break
|
break
|
||||||
|
|
||||||
self._finish_frag_download(ctx)
|
self._finish_frag_download(ctx)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ try:
|
|||||||
from .lazy_extractors import _ALL_CLASSES
|
from .lazy_extractors import _ALL_CLASSES
|
||||||
_LAZY_LOADER = True
|
_LAZY_LOADER = True
|
||||||
_PLUGIN_CLASSES = []
|
_PLUGIN_CLASSES = []
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_LAZY_LOADER = False
|
_LAZY_LOADER = False
|
||||||
|
|
||||||
|
if not _LAZY_LOADER:
|
||||||
from .extractors import *
|
from .extractors import *
|
||||||
|
|
||||||
_PLUGIN_CLASSES = load_plugins('extractor', 'IE', globals())
|
_PLUGIN_CLASSES = load_plugins('extractor', 'IE', globals())
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import calendar
|
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
|
|
||||||
from .amp import AMPIE
|
from .amp import AMPIE
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .youtube import YoutubeIE
|
from ..utils import (
|
||||||
from ..compat import compat_urlparse
|
parse_duration,
|
||||||
|
parse_iso8601,
|
||||||
|
try_get,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AbcNewsVideoIE(AMPIE):
|
class AbcNewsVideoIE(AMPIE):
|
||||||
@@ -18,8 +19,8 @@ class AbcNewsVideoIE(AMPIE):
|
|||||||
(?:
|
(?:
|
||||||
abcnews\.go\.com/
|
abcnews\.go\.com/
|
||||||
(?:
|
(?:
|
||||||
[^/]+/video/(?P<display_id>[0-9a-z-]+)-|
|
(?:[^/]+/)*video/(?P<display_id>[0-9a-z-]+)-|
|
||||||
video/embed\?.*?\bid=
|
video/(?:embed|itemfeed)\?.*?\bid=
|
||||||
)|
|
)|
|
||||||
fivethirtyeight\.abcnews\.go\.com/video/embed/\d+/
|
fivethirtyeight\.abcnews\.go\.com/video/embed/\d+/
|
||||||
)
|
)
|
||||||
@@ -36,6 +37,8 @@ class AbcNewsVideoIE(AMPIE):
|
|||||||
'description': 'George Stephanopoulos goes one-on-one with Iranian Foreign Minister Dr. Javad Zarif.',
|
'description': 'George Stephanopoulos goes one-on-one with Iranian Foreign Minister Dr. Javad Zarif.',
|
||||||
'duration': 180,
|
'duration': 180,
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
'timestamp': 1380454200,
|
||||||
|
'upload_date': '20130929',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
@@ -47,6 +50,12 @@ class AbcNewsVideoIE(AMPIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://abcnews.go.com/2020/video/2020-husband-stands-teacher-jail-student-affairs-26119478',
|
'url': 'http://abcnews.go.com/2020/video/2020-husband-stands-teacher-jail-student-affairs-26119478',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://abcnews.go.com/video/itemfeed?id=46979033',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://abcnews.go.com/GMA/News/video/history-christmas-story-67894761',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -67,28 +76,23 @@ class AbcNewsIE(InfoExtractor):
|
|||||||
_VALID_URL = r'https?://abcnews\.go\.com/(?:[^/]+/)+(?P<display_id>[0-9a-z-]+)/story\?id=(?P<id>\d+)'
|
_VALID_URL = r'https?://abcnews\.go\.com/(?:[^/]+/)+(?P<display_id>[0-9a-z-]+)/story\?id=(?P<id>\d+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://abcnews.go.com/Blotter/News/dramatic-video-rare-death-job-america/story?id=10498713#.UIhwosWHLjY',
|
# Youtube Embeds
|
||||||
|
'url': 'https://abcnews.go.com/Entertainment/peter-billingsley-child-actor-christmas-story-hollywood-power/story?id=51286501',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '10505354',
|
'id': '51286501',
|
||||||
'ext': 'flv',
|
'title': "Peter Billingsley: From child actor in 'A Christmas Story' to Hollywood power player",
|
||||||
'display_id': 'dramatic-video-rare-death-job-america',
|
'description': 'Billingsley went from a child actor to Hollywood power player.',
|
||||||
'title': 'Occupational Hazards',
|
|
||||||
'description': 'Nightline investigates the dangers that lurk at various jobs.',
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
|
||||||
'upload_date': '20100428',
|
|
||||||
'timestamp': 1272412800,
|
|
||||||
},
|
},
|
||||||
'add_ie': ['AbcNewsVideo'],
|
'playlist_count': 5,
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://abcnews.go.com/Entertainment/justin-timberlake-performs-stop-feeling-eurovision-2016/story?id=39125818',
|
'url': 'http://abcnews.go.com/Entertainment/justin-timberlake-performs-stop-feeling-eurovision-2016/story?id=39125818',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '38897857',
|
'id': '38897857',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'display_id': 'justin-timberlake-performs-stop-feeling-eurovision-2016',
|
|
||||||
'title': 'Justin Timberlake Drops Hints For Secret Single',
|
'title': 'Justin Timberlake Drops Hints For Secret Single',
|
||||||
'description': 'Lara Spencer reports the buzziest stories of the day in "GMA" Pop News.',
|
'description': 'Lara Spencer reports the buzziest stories of the day in "GMA" Pop News.',
|
||||||
'upload_date': '20160515',
|
'upload_date': '20160505',
|
||||||
'timestamp': 1463329500,
|
'timestamp': 1462442280,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
@@ -100,49 +104,55 @@ class AbcNewsIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://abcnews.go.com/Technology/exclusive-apple-ceo-tim-cook-iphone-cracking-software/story?id=37173343',
|
'url': 'http://abcnews.go.com/Technology/exclusive-apple-ceo-tim-cook-iphone-cracking-software/story?id=37173343',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# inline.type == 'video'
|
||||||
|
'url': 'http://abcnews.go.com/Technology/exclusive-apple-ceo-tim-cook-iphone-cracking-software/story?id=37173343',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
story_id = self._match_id(url)
|
||||||
display_id = mobj.group('display_id')
|
webpage = self._download_webpage(url, story_id)
|
||||||
video_id = mobj.group('id')
|
story = self._parse_json(self._search_regex(
|
||||||
|
r"window\['__abcnews__'\]\s*=\s*({.+?});",
|
||||||
|
webpage, 'data'), story_id)['page']['content']['story']['everscroll'][0]
|
||||||
|
article_contents = story.get('articleContents') or {}
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
def entries():
|
||||||
video_url = self._search_regex(
|
featured_video = story.get('featuredVideo') or {}
|
||||||
r'window\.abcnvideo\.url\s*=\s*"([^"]+)"', webpage, 'video URL')
|
feed = try_get(featured_video, lambda x: x['video']['feed'])
|
||||||
full_video_url = compat_urlparse.urljoin(url, video_url)
|
if feed:
|
||||||
|
yield {
|
||||||
youtube_url = YoutubeIE._extract_url(webpage)
|
'_type': 'url',
|
||||||
|
'id': featured_video.get('id'),
|
||||||
timestamp = None
|
'title': featured_video.get('name'),
|
||||||
date_str = self._html_search_regex(
|
'url': feed,
|
||||||
r'<span[^>]+class="timestamp">([^<]+)</span>',
|
'thumbnail': featured_video.get('images'),
|
||||||
webpage, 'timestamp', fatal=False)
|
'description': featured_video.get('description'),
|
||||||
if date_str:
|
'timestamp': parse_iso8601(featured_video.get('uploadDate')),
|
||||||
tz_offset = 0
|
'duration': parse_duration(featured_video.get('duration')),
|
||||||
if date_str.endswith(' ET'): # Eastern Time
|
|
||||||
tz_offset = -5
|
|
||||||
date_str = date_str[:-3]
|
|
||||||
date_formats = ['%b. %d, %Y', '%b %d, %Y, %I:%M %p']
|
|
||||||
for date_format in date_formats:
|
|
||||||
try:
|
|
||||||
timestamp = calendar.timegm(time.strptime(date_str.strip(), date_format))
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
if timestamp is not None:
|
|
||||||
timestamp -= tz_offset * 3600
|
|
||||||
|
|
||||||
entry = {
|
|
||||||
'_type': 'url_transparent',
|
|
||||||
'ie_key': AbcNewsVideoIE.ie_key(),
|
'ie_key': AbcNewsVideoIE.ie_key(),
|
||||||
'url': full_video_url,
|
|
||||||
'id': video_id,
|
|
||||||
'display_id': display_id,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if youtube_url:
|
for inline in (article_contents.get('inlines') or []):
|
||||||
entries = [entry, self.url_result(youtube_url, ie=YoutubeIE.ie_key())]
|
inline_type = inline.get('type')
|
||||||
return self.playlist_result(entries)
|
if inline_type == 'iframe':
|
||||||
|
iframe_url = try_get(inline, lambda x: x['attrs']['src'])
|
||||||
|
if iframe_url:
|
||||||
|
yield self.url_result(iframe_url)
|
||||||
|
elif inline_type == 'video':
|
||||||
|
video_id = inline.get('id')
|
||||||
|
if video_id:
|
||||||
|
yield {
|
||||||
|
'_type': 'url',
|
||||||
|
'id': video_id,
|
||||||
|
'url': 'http://abcnews.go.com/video/embed?id=' + video_id,
|
||||||
|
'thumbnail': inline.get('imgSrc') or inline.get('imgDefault'),
|
||||||
|
'description': inline.get('description'),
|
||||||
|
'duration': parse_duration(inline.get('duration')),
|
||||||
|
'ie_key': AbcNewsVideoIE.ie_key(),
|
||||||
|
}
|
||||||
|
|
||||||
return entry
|
return self.playlist_result(
|
||||||
|
entries(), story_id, article_contents.get('headline'),
|
||||||
|
article_contents.get('subHead'))
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from ..utils import (
|
|||||||
strip_or_none,
|
strip_or_none,
|
||||||
try_get,
|
try_get,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
|
urlencode_postdata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -51,9 +52,12 @@ class ADNIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_NETRC_MACHINE = 'animedigitalnetwork'
|
||||||
_BASE_URL = 'http://animedigitalnetwork.fr'
|
_BASE_URL = 'http://animedigitalnetwork.fr'
|
||||||
_API_BASE_URL = 'https://gw.api.animedigitalnetwork.fr/'
|
_API_BASE_URL = 'https://gw.api.animedigitalnetwork.fr/'
|
||||||
_PLAYER_BASE_URL = _API_BASE_URL + 'player/'
|
_PLAYER_BASE_URL = _API_BASE_URL + 'player/'
|
||||||
|
_HEADERS = {}
|
||||||
|
_LOGIN_ERR_MESSAGE = 'Unable to log in'
|
||||||
_RSA_KEY = (0x9B42B08905199A5CCE2026274399CA560ECB209EE9878A708B1C0812E1BB8CB5D1FB7441861147C1A1F2F3A0476DD63A9CAC20D3E983613346850AA6CB38F16DC7D720FD7D86FC6E5B3D5BBC72E14CD0BF9E869F2CEA2CCAD648F1DCE38F1FF916CEFB2D339B64AA0264372344BC775E265E8A852F88144AB0BD9AA06C1A4ABB, 65537)
|
_RSA_KEY = (0x9B42B08905199A5CCE2026274399CA560ECB209EE9878A708B1C0812E1BB8CB5D1FB7441861147C1A1F2F3A0476DD63A9CAC20D3E983613346850AA6CB38F16DC7D720FD7D86FC6E5B3D5BBC72E14CD0BF9E869F2CEA2CCAD648F1DCE38F1FF916CEFB2D339B64AA0264372344BC775E265E8A852F88144AB0BD9AA06C1A4ABB, 65537)
|
||||||
_POS_ALIGN_MAP = {
|
_POS_ALIGN_MAP = {
|
||||||
'start': 1,
|
'start': 1,
|
||||||
@@ -129,19 +133,42 @@ Format: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'''
|
|||||||
}])
|
}])
|
||||||
return subtitles
|
return subtitles
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
username, password = self._get_login_info()
|
||||||
|
if not username:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
access_token = (self._download_json(
|
||||||
|
self._API_BASE_URL + 'authentication/login', None,
|
||||||
|
'Logging in', self._LOGIN_ERR_MESSAGE, fatal=False,
|
||||||
|
data=urlencode_postdata({
|
||||||
|
'password': password,
|
||||||
|
'rememberMe': False,
|
||||||
|
'source': 'Web',
|
||||||
|
'username': username,
|
||||||
|
})) or {}).get('accessToken')
|
||||||
|
if access_token:
|
||||||
|
self._HEADERS = {'authorization': 'Bearer ' + access_token}
|
||||||
|
except ExtractorError as e:
|
||||||
|
message = None
|
||||||
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
|
||||||
|
resp = self._parse_json(
|
||||||
|
e.cause.read().decode(), None, fatal=False) or {}
|
||||||
|
message = resp.get('message') or resp.get('code')
|
||||||
|
self.report_warning(message or self._LOGIN_ERR_MESSAGE)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
video_base_url = self._PLAYER_BASE_URL + 'video/%s/' % video_id
|
video_base_url = self._PLAYER_BASE_URL + 'video/%s/' % video_id
|
||||||
player = self._download_json(
|
player = self._download_json(
|
||||||
video_base_url + 'configuration', video_id,
|
video_base_url + 'configuration', video_id,
|
||||||
'Downloading player config JSON metadata')['player']
|
'Downloading player config JSON metadata',
|
||||||
|
headers=self._HEADERS)['player']
|
||||||
options = player['options']
|
options = player['options']
|
||||||
|
|
||||||
user = options['user']
|
user = options['user']
|
||||||
if not user.get('hasAccess'):
|
if not user.get('hasAccess'):
|
||||||
raise ExtractorError(
|
self.raise_login_required()
|
||||||
'This video is only available for paying users', expected=True)
|
|
||||||
# self.raise_login_required() # FIXME: Login is not implemented
|
|
||||||
|
|
||||||
token = self._download_json(
|
token = self._download_json(
|
||||||
user.get('refreshTokenUrl') or (self._PLAYER_BASE_URL + 'refresh/token'),
|
user.get('refreshTokenUrl') or (self._PLAYER_BASE_URL + 'refresh/token'),
|
||||||
@@ -188,7 +215,6 @@ Format: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'''
|
|||||||
message = error.get('message')
|
message = error.get('message')
|
||||||
if e.cause.code == 403 and error.get('code') == 'player-bad-geolocation-country':
|
if e.cause.code == 403 and error.get('code') == 'player-bad-geolocation-country':
|
||||||
self.raise_geo_restricted(msg=message)
|
self.raise_geo_restricted(msg=message)
|
||||||
else:
|
|
||||||
raise ExtractorError(message)
|
raise ExtractorError(message)
|
||||||
else:
|
else:
|
||||||
raise ExtractorError('Giving up retrying')
|
raise ExtractorError('Giving up retrying')
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class AdobeTVBaseIE(InfoExtractor):
|
|||||||
if original_filename.startswith('s3://') and not s3_extracted:
|
if original_filename.startswith('s3://') and not s3_extracted:
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': 'original',
|
'format_id': 'original',
|
||||||
'preference': 1,
|
'quality': 1,
|
||||||
'url': original_filename.replace('s3://', 'https://s3.amazonaws.com/'),
|
'url': original_filename.replace('s3://', 'https://s3.amazonaws.com/'),
|
||||||
})
|
})
|
||||||
s3_extracted = True
|
s3_extracted = True
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ class AENetworksShowIE(AENetworksListBaseIE):
|
|||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.history.com/shows/ancient-aliens',
|
'url': 'http://www.history.com/shows/ancient-aliens',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'SH012427480000',
|
'id': 'SERIES1574',
|
||||||
'title': 'Ancient Aliens',
|
'title': 'Ancient Aliens',
|
||||||
'description': 'md5:3f6d74daf2672ff3ae29ed732e37ea7f',
|
'description': 'md5:3f6d74daf2672ff3ae29ed732e37ea7f',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class AluraIE(InfoExtractor):
|
|||||||
f['height'] = int('720' if m.group('res') == 'hd' else '480')
|
f['height'] = int('720' if m.group('res') == 'hd' else '480')
|
||||||
formats.extend(video_format)
|
formats.extend(video_format)
|
||||||
|
|
||||||
self._sort_formats(formats, field_preference=('height', 'width', 'tbr', 'format_id'))
|
self._sort_formats(formats)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
|
unified_timestamp,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ class AMPIE(InfoExtractor):
|
|||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
timestamp = parse_iso8601(item.get('pubDate'), ' ') or parse_iso8601(item.get('dc-date'))
|
timestamp = unified_timestamp(item.get('pubDate'), ' ') or parse_iso8601(item.get('dc-date'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ from ..utils import (
|
|||||||
unsmuggle_url,
|
unsmuggle_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# This import causes a ModuleNotFoundError on some systems for unknown reason.
|
||||||
|
# See issues:
|
||||||
|
# https://github.com/pukkandan/yt-dlp/issues/35
|
||||||
|
# https://github.com/ytdl-org/youtube-dl/issues/27449
|
||||||
|
# https://github.com/animelover1984/youtube-dl/issues/17
|
||||||
|
try:
|
||||||
|
from .anvato_token_generator import NFLTokenGenerator
|
||||||
|
except ImportError:
|
||||||
|
NFLTokenGenerator = None
|
||||||
|
|
||||||
|
|
||||||
def md5_text(s):
|
def md5_text(s):
|
||||||
if not isinstance(s, compat_str):
|
if not isinstance(s, compat_str):
|
||||||
@@ -203,6 +213,10 @@ class AnvatoIE(InfoExtractor):
|
|||||||
'telemundo': 'anvato_mcp_telemundo_web_prod_c5278d51ad46fda4b6ca3d0ea44a7846a054f582'
|
'telemundo': 'anvato_mcp_telemundo_web_prod_c5278d51ad46fda4b6ca3d0ea44a7846a054f582'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_TOKEN_GENERATORS = {
|
||||||
|
'GXvEgwyJeWem8KCYXfeoHWknwP48Mboj': NFLTokenGenerator,
|
||||||
|
}
|
||||||
|
|
||||||
_API_KEY = '3hwbSuqqT690uxjNYBktSQpa5ZrpYYR0Iofx7NcJHyA'
|
_API_KEY = '3hwbSuqqT690uxjNYBktSQpa5ZrpYYR0Iofx7NcJHyA'
|
||||||
|
|
||||||
_ANVP_RE = r'<script[^>]+\bdata-anvp\s*=\s*(["\'])(?P<anvp>(?:(?!\1).)+)\1'
|
_ANVP_RE = r'<script[^>]+\bdata-anvp\s*=\s*(["\'])(?P<anvp>(?:(?!\1).)+)\1'
|
||||||
@@ -262,6 +276,9 @@ class AnvatoIE(InfoExtractor):
|
|||||||
'anvrid': anvrid,
|
'anvrid': anvrid,
|
||||||
'anvts': server_time,
|
'anvts': server_time,
|
||||||
}
|
}
|
||||||
|
if self._TOKEN_GENERATORS.get(access_key) is not None:
|
||||||
|
api['anvstk2'] = self._TOKEN_GENERATORS[access_key].generate(self, access_key, video_id)
|
||||||
|
else:
|
||||||
api['anvstk'] = md5_text('%s|%s|%d|%s' % (
|
api['anvstk'] = md5_text('%s|%s|%d|%s' % (
|
||||||
access_key, anvrid, server_time,
|
access_key, anvrid, server_time,
|
||||||
self._ANVACK_TABLE.get(access_key, self._API_KEY)))
|
self._ANVACK_TABLE.get(access_key, self._API_KEY)))
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class AolIE(YahooIE):
|
|||||||
'height': int_or_none(qs.get('h', [None])[0]),
|
'height': int_or_none(qs.get('h', [None])[0]),
|
||||||
})
|
})
|
||||||
formats.append(f)
|
formats.append(f)
|
||||||
self._sort_formats(formats, ('width', 'height', 'tbr', 'format_id'))
|
self._sort_formats(formats)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
|||||||
@@ -72,8 +72,7 @@ class AparatIE(InfoExtractor):
|
|||||||
r'(\d+)[pP]', label or '', 'height',
|
r'(\d+)[pP]', label or '', 'height',
|
||||||
default=None)),
|
default=None)),
|
||||||
})
|
})
|
||||||
self._sort_formats(
|
self._sort_formats(formats)
|
||||||
formats, field_preference=('height', 'width', 'tbr', 'format_id'))
|
|
||||||
|
|
||||||
info = self._search_json_ld(webpage, video_id, default={})
|
info = self._search_json_ld(webpage, video_id, default={})
|
||||||
|
|
||||||
|
|||||||
@@ -129,10 +129,6 @@ class ArcPublishingIE(InfoExtractor):
|
|||||||
if all([f.get('acodec') == 'none' for f in m3u8_formats]):
|
if all([f.get('acodec') == 'none' for f in m3u8_formats]):
|
||||||
continue
|
continue
|
||||||
for f in m3u8_formats:
|
for f in m3u8_formats:
|
||||||
if f.get('acodec') == 'none':
|
|
||||||
f['preference'] = -40
|
|
||||||
elif f.get('vcodec') == 'none':
|
|
||||||
f['preference'] = -50
|
|
||||||
height = f.get('height')
|
height = f.get('height')
|
||||||
if not height:
|
if not height:
|
||||||
continue
|
continue
|
||||||
@@ -150,10 +146,9 @@ class ArcPublishingIE(InfoExtractor):
|
|||||||
'height': int_or_none(s.get('height')),
|
'height': int_or_none(s.get('height')),
|
||||||
'filesize': int_or_none(s.get('filesize')),
|
'filesize': int_or_none(s.get('filesize')),
|
||||||
'url': s_url,
|
'url': s_url,
|
||||||
'preference': -1,
|
'quality': -10,
|
||||||
})
|
})
|
||||||
self._sort_formats(
|
self._sort_formats(formats)
|
||||||
formats, ('preference', 'width', 'height', 'vbr', 'filesize', 'tbr', 'ext', 'format_id'))
|
|
||||||
|
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
for subtitle in (try_get(video, lambda x: x['subtitles']['urls'], list) or []):
|
for subtitle in (try_get(video, lambda x: x['subtitles']['urls'], list) or []):
|
||||||
|
|||||||
@@ -324,20 +324,42 @@ class ARDIE(InfoExtractor):
|
|||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for a in video_node.findall('.//asset'):
|
for a in video_node.findall('.//asset'):
|
||||||
|
file_name = xpath_text(a, './fileName', default=None)
|
||||||
|
if not file_name:
|
||||||
|
continue
|
||||||
|
format_type = a.attrib.get('type')
|
||||||
|
format_url = url_or_none(file_name)
|
||||||
|
if format_url:
|
||||||
|
ext = determine_ext(file_name)
|
||||||
|
if ext == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
format_url, display_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
|
m3u8_id=format_type or 'hls', fatal=False))
|
||||||
|
continue
|
||||||
|
elif ext == 'f4m':
|
||||||
|
formats.extend(self._extract_f4m_formats(
|
||||||
|
update_url_query(format_url, {'hdcore': '3.7.0'}),
|
||||||
|
display_id, f4m_id=format_type or 'hds', fatal=False))
|
||||||
|
continue
|
||||||
f = {
|
f = {
|
||||||
'format_id': a.attrib['type'],
|
'format_id': format_type,
|
||||||
'width': int_or_none(a.find('./frameWidth').text),
|
'width': int_or_none(xpath_text(a, './frameWidth')),
|
||||||
'height': int_or_none(a.find('./frameHeight').text),
|
'height': int_or_none(xpath_text(a, './frameHeight')),
|
||||||
'vbr': int_or_none(a.find('./bitrateVideo').text),
|
'vbr': int_or_none(xpath_text(a, './bitrateVideo')),
|
||||||
'abr': int_or_none(a.find('./bitrateAudio').text),
|
'abr': int_or_none(xpath_text(a, './bitrateAudio')),
|
||||||
'vcodec': a.find('./codecVideo').text,
|
'vcodec': xpath_text(a, './codecVideo'),
|
||||||
'tbr': int_or_none(a.find('./totalBitrate').text),
|
'tbr': int_or_none(xpath_text(a, './totalBitrate')),
|
||||||
}
|
}
|
||||||
if a.find('./serverPrefix').text:
|
server_prefix = xpath_text(a, './serverPrefix', default=None)
|
||||||
f['url'] = a.find('./serverPrefix').text
|
if server_prefix:
|
||||||
f['playpath'] = a.find('./fileName').text
|
f.update({
|
||||||
|
'url': server_prefix,
|
||||||
|
'playpath': file_name,
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
f['url'] = a.find('./fileName').text
|
if not format_url:
|
||||||
|
continue
|
||||||
|
f['url'] = format_url
|
||||||
formats.append(f)
|
formats.append(f)
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,6 @@ class ArteTVIE(ArteTVBaseIE):
|
|||||||
|
|
||||||
format = {
|
format = {
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
'preference': -10 if f.get('videoFormat') == 'M3U8' else None,
|
|
||||||
'language_preference': lang_pref,
|
'language_preference': lang_pref,
|
||||||
'format_note': '%s, %s' % (f.get('versionCode'), f.get('versionLibelle')),
|
'format_note': '%s, %s' % (f.get('versionCode'), f.get('versionLibelle')),
|
||||||
'width': int_or_none(f.get('width')),
|
'width': int_or_none(f.get('width')),
|
||||||
@@ -168,7 +167,9 @@ class ArteTVIE(ArteTVBaseIE):
|
|||||||
|
|
||||||
formats.append(format)
|
formats.append(format)
|
||||||
|
|
||||||
self._sort_formats(formats)
|
# For this extractor, quality only represents the relative quality
|
||||||
|
# with respect to other formats with the same resolution
|
||||||
|
self._sort_formats(formats, ('res', 'quality'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': player_info.get('VID') or video_id,
|
'id': player_info.get('VID') or video_id,
|
||||||
|
|||||||
247
youtube_dlc/extractor/audius.py
Normal file
247
youtube_dlc/extractor/audius.py
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import ExtractorError, try_get, compat_str, str_or_none
|
||||||
|
from ..compat import compat_urllib_parse_unquote
|
||||||
|
|
||||||
|
|
||||||
|
class AudiusBaseIE(InfoExtractor):
|
||||||
|
_API_BASE = None
|
||||||
|
_API_V = '/v1'
|
||||||
|
|
||||||
|
def _get_response_data(self, response):
|
||||||
|
if isinstance(response, dict):
|
||||||
|
response_data = response.get('data')
|
||||||
|
if response_data is not None:
|
||||||
|
return response_data
|
||||||
|
if len(response) == 1 and 'message' in response:
|
||||||
|
raise ExtractorError('API error: %s' % response['message'],
|
||||||
|
expected=True)
|
||||||
|
raise ExtractorError('Unexpected API response')
|
||||||
|
|
||||||
|
def _select_api_base(self):
|
||||||
|
"""Selecting one of the currently available API hosts"""
|
||||||
|
response = super(AudiusBaseIE, self)._download_json(
|
||||||
|
'https://api.audius.co/', None,
|
||||||
|
note='Requesting available API hosts',
|
||||||
|
errnote='Unable to request available API hosts')
|
||||||
|
hosts = self._get_response_data(response)
|
||||||
|
if isinstance(hosts, list):
|
||||||
|
self._API_BASE = random.choice(hosts)
|
||||||
|
return
|
||||||
|
raise ExtractorError('Unable to get available API hosts')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _prepare_url(url, title):
|
||||||
|
"""
|
||||||
|
Audius removes forward slashes from the uri, but leaves backslashes.
|
||||||
|
The problem is that the current version of Chrome replaces backslashes
|
||||||
|
in the address bar with a forward slashes, so if you copy the link from
|
||||||
|
there and paste it into youtube-dl, you won't be able to download
|
||||||
|
anything from this link, since the Audius API won't be able to resolve
|
||||||
|
this url
|
||||||
|
"""
|
||||||
|
url = compat_urllib_parse_unquote(url)
|
||||||
|
title = compat_urllib_parse_unquote(title)
|
||||||
|
if '/' in title or '%2F' in title:
|
||||||
|
fixed_title = title.replace('/', '%5C').replace('%2F', '%5C')
|
||||||
|
return url.replace(title, fixed_title)
|
||||||
|
return url
|
||||||
|
|
||||||
|
def _api_request(self, path, item_id=None, note='Downloading JSON metadata',
|
||||||
|
errnote='Unable to download JSON metadata',
|
||||||
|
expected_status=None):
|
||||||
|
if self._API_BASE is None:
|
||||||
|
self._select_api_base()
|
||||||
|
try:
|
||||||
|
response = super(AudiusBaseIE, self)._download_json(
|
||||||
|
'%s%s%s' % (self._API_BASE, self._API_V, path), item_id, note=note,
|
||||||
|
errnote=errnote, expected_status=expected_status)
|
||||||
|
except ExtractorError as exc:
|
||||||
|
# some of Audius API hosts may not work as expected and return HTML
|
||||||
|
if 'Failed to parse JSON' in compat_str(exc):
|
||||||
|
raise ExtractorError('An error occurred while receiving data. Try again',
|
||||||
|
expected=True)
|
||||||
|
raise exc
|
||||||
|
return self._get_response_data(response)
|
||||||
|
|
||||||
|
def _resolve_url(self, url, item_id):
|
||||||
|
return self._api_request('/resolve?url=%s' % url, item_id,
|
||||||
|
expected_status=404)
|
||||||
|
|
||||||
|
|
||||||
|
class AudiusIE(AudiusBaseIE):
|
||||||
|
_VALID_URL = r'''(?x)https?://(?:www\.)?(?:audius\.co/(?P<uploader>[\w\d-]+)(?!/album|/playlist)/(?P<title>\S+))'''
|
||||||
|
IE_DESC = 'Audius.co'
|
||||||
|
_TESTS = [
|
||||||
|
{
|
||||||
|
# URL from Chrome address bar which replace backslash to forward slash
|
||||||
|
'url': 'https://audius.co/test_acc/t%D0%B5%D0%B5%D0%B5est-1.%5E_%7B%7D/%22%3C%3E.%E2%84%96~%60-198631',
|
||||||
|
'md5': '92c35d3e754d5a0f17eef396b0d33582',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'xd8gY',
|
||||||
|
'title': '''Tеееest/ 1.!@#$%^&*()_+=[]{};'\\\":<>,.?/№~`''',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'description': 'Description',
|
||||||
|
'duration': 30,
|
||||||
|
'track': '''Tеееest/ 1.!@#$%^&*()_+=[]{};'\\\":<>,.?/№~`''',
|
||||||
|
'artist': 'test',
|
||||||
|
'genre': 'Electronic',
|
||||||
|
'thumbnail': r're:https?://.*\.jpg',
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'repost_count': int,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# Regular track
|
||||||
|
'url': 'https://audius.co/voltra/radar-103692',
|
||||||
|
'md5': '491898a0a8de39f20c5d6a8a80ab5132',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'KKdy2',
|
||||||
|
'title': 'RADAR',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'duration': 318,
|
||||||
|
'track': 'RADAR',
|
||||||
|
'artist': 'voltra',
|
||||||
|
'genre': 'Trance',
|
||||||
|
'thumbnail': r're:https?://.*\.jpg',
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'repost_count': int,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
_ARTWORK_MAP = {
|
||||||
|
"150x150": 150,
|
||||||
|
"480x480": 480,
|
||||||
|
"1000x1000": 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
track_id = try_get(mobj, lambda x: x.group('track_id'))
|
||||||
|
if track_id is None:
|
||||||
|
title = mobj.group('title')
|
||||||
|
# uploader = mobj.group('uploader')
|
||||||
|
url = self._prepare_url(url, title)
|
||||||
|
track_data = self._resolve_url(url, title)
|
||||||
|
else: # API link
|
||||||
|
title = None
|
||||||
|
# uploader = None
|
||||||
|
track_data = self._api_request('/tracks/%s' % track_id, track_id)
|
||||||
|
|
||||||
|
if not isinstance(track_data, dict):
|
||||||
|
raise ExtractorError('Unexpected API response')
|
||||||
|
|
||||||
|
track_id = track_data.get('id')
|
||||||
|
if track_id is None:
|
||||||
|
raise ExtractorError('Unable to get ID of the track')
|
||||||
|
|
||||||
|
artworks_data = track_data.get('artwork')
|
||||||
|
thumbnails = []
|
||||||
|
if isinstance(artworks_data, dict):
|
||||||
|
for quality_key, thumbnail_url in artworks_data.items():
|
||||||
|
thumbnail = {
|
||||||
|
"url": thumbnail_url
|
||||||
|
}
|
||||||
|
quality_code = self._ARTWORK_MAP.get(quality_key)
|
||||||
|
if quality_code is not None:
|
||||||
|
thumbnail['preference'] = quality_code
|
||||||
|
thumbnails.append(thumbnail)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': track_id,
|
||||||
|
'title': track_data.get('title', title),
|
||||||
|
'url': '%s/v1/tracks/%s/stream' % (self._API_BASE, track_id),
|
||||||
|
'ext': 'mp3',
|
||||||
|
'description': track_data.get('description'),
|
||||||
|
'duration': track_data.get('duration'),
|
||||||
|
'track': track_data.get('title'),
|
||||||
|
'artist': try_get(track_data, lambda x: x['user']['name'], compat_str),
|
||||||
|
'genre': track_data.get('genre'),
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
'view_count': track_data.get('play_count'),
|
||||||
|
'like_count': track_data.get('favorite_count'),
|
||||||
|
'repost_count': track_data.get('repost_count'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AudiusTrackIE(AudiusIE):
|
||||||
|
_VALID_URL = r'''(?x)(?:audius:)(?:https?://(?:www\.)?.+/v1/tracks/)?(?P<track_id>\w+)'''
|
||||||
|
IE_NAME = 'audius:track'
|
||||||
|
IE_DESC = 'Audius track ID or API link. Prepend with "audius:"'
|
||||||
|
_TESTS = [
|
||||||
|
{
|
||||||
|
'url': 'audius:9RWlo',
|
||||||
|
'only_matching': True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'audius:http://discoveryprovider.audius.prod-us-west-2.staked.cloud/v1/tracks/9RWlo',
|
||||||
|
'only_matching': True
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AudiusPlaylistIE(AudiusBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?audius\.co/(?P<uploader>[\w\d-]+)/(?:album|playlist)/(?P<title>\S+)'
|
||||||
|
IE_NAME = 'audius:playlist'
|
||||||
|
IE_DESC = 'Audius.co playlists'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://audius.co/test_acc/playlist/test-playlist-22910',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'DNvjN',
|
||||||
|
'title': 'test playlist',
|
||||||
|
'description': 'Test description\n\nlol',
|
||||||
|
},
|
||||||
|
'playlist_count': 175,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _build_playlist(self, tracks):
|
||||||
|
entries = []
|
||||||
|
for track in tracks:
|
||||||
|
if not isinstance(track, dict):
|
||||||
|
raise ExtractorError('Unexpected API response')
|
||||||
|
track_id = str_or_none(track.get('id'))
|
||||||
|
if not track_id:
|
||||||
|
raise ExtractorError('Unable to get track ID from playlist')
|
||||||
|
entries.append(self.url_result(
|
||||||
|
'audius:%s' % track_id,
|
||||||
|
ie=AudiusTrackIE.ie_key(), video_id=track_id))
|
||||||
|
return entries
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
self._select_api_base()
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
title = mobj.group('title')
|
||||||
|
# uploader = mobj.group('uploader')
|
||||||
|
url = self._prepare_url(url, title)
|
||||||
|
playlist_response = self._resolve_url(url, title)
|
||||||
|
|
||||||
|
if not isinstance(playlist_response, list) or len(playlist_response) != 1:
|
||||||
|
raise ExtractorError('Unexpected API response')
|
||||||
|
|
||||||
|
playlist_data = playlist_response[0]
|
||||||
|
if not isinstance(playlist_data, dict):
|
||||||
|
raise ExtractorError('Unexpected API response')
|
||||||
|
|
||||||
|
playlist_id = playlist_data.get('id')
|
||||||
|
if playlist_id is None:
|
||||||
|
raise ExtractorError('Unable to get playlist ID')
|
||||||
|
|
||||||
|
playlist_tracks = self._api_request(
|
||||||
|
'/playlists/%s/tracks' % playlist_id,
|
||||||
|
title, note='Downloading playlist tracks metadata',
|
||||||
|
errnote='Unable to download playlist tracks metadata')
|
||||||
|
if not isinstance(playlist_tracks, list):
|
||||||
|
raise ExtractorError('Unexpected API response')
|
||||||
|
|
||||||
|
entries = self._build_playlist(playlist_tracks)
|
||||||
|
return self.playlist_result(entries, playlist_id,
|
||||||
|
playlist_data.get('playlist_name', title),
|
||||||
|
playlist_data.get('description'))
|
||||||
@@ -48,6 +48,7 @@ class AWAANBaseIE(InfoExtractor):
|
|||||||
'duration': int_or_none(video_data.get('duration')),
|
'duration': int_or_none(video_data.get('duration')),
|
||||||
'timestamp': parse_iso8601(video_data.get('create_time'), ' '),
|
'timestamp': parse_iso8601(video_data.get('create_time'), ' '),
|
||||||
'is_live': is_live,
|
'is_live': is_live,
|
||||||
|
'uploader_id': video_data.get('user_id'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -107,6 +108,7 @@ class AWAANLiveIE(AWAANBaseIE):
|
|||||||
'title': 're:Dubai Al Oula [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
'title': 're:Dubai Al Oula [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
||||||
'upload_date': '20150107',
|
'upload_date': '20150107',
|
||||||
'timestamp': 1420588800,
|
'timestamp': 1420588800,
|
||||||
|
'uploader_id': '71',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class AZMedienIE(InfoExtractor):
|
|||||||
'url': 'https://www.telebaern.tv/telebaern-news/montag-1-oktober-2018-ganze-sendung-133531189#video=0_7xjo9lf1',
|
'url': 'https://www.telebaern.tv/telebaern-news/montag-1-oktober-2018-ganze-sendung-133531189#video=0_7xjo9lf1',
|
||||||
'only_matching': True
|
'only_matching': True
|
||||||
}]
|
}]
|
||||||
_API_TEMPL = 'https://www.%s/api/pub/gql/%s/NewsArticleTeaser/cb9f2f81ed22e9b47f4ca64ea3cc5a5d13e88d1d'
|
_API_TEMPL = 'https://www.%s/api/pub/gql/%s/NewsArticleTeaser/a4016f65fe62b81dc6664dd9f4910e4ab40383be'
|
||||||
_PARTNER_ID = '1719221'
|
_PARTNER_ID = '1719221'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|||||||
@@ -69,12 +69,10 @@ class BeatportIE(InfoExtractor):
|
|||||||
'vcodec': 'none',
|
'vcodec': 'none',
|
||||||
}
|
}
|
||||||
if ext == 'mp3':
|
if ext == 'mp3':
|
||||||
fmt['preference'] = 0
|
|
||||||
fmt['acodec'] = 'mp3'
|
fmt['acodec'] = 'mp3'
|
||||||
fmt['abr'] = 96
|
fmt['abr'] = 96
|
||||||
fmt['asr'] = 44100
|
fmt['asr'] = 44100
|
||||||
elif ext == 'mp4':
|
elif ext == 'mp4':
|
||||||
fmt['preference'] = 1
|
|
||||||
fmt['acodec'] = 'aac'
|
fmt['acodec'] = 'aac'
|
||||||
fmt['abr'] = 96
|
fmt['abr'] = 96
|
||||||
fmt['asr'] = 44100
|
fmt['asr'] = 44100
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor, SearchInfoExtractor
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_parse_qs,
|
compat_parse_qs,
|
||||||
compat_urlparse,
|
compat_urlparse,
|
||||||
@@ -32,13 +33,14 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
(?:
|
(?:
|
||||||
video/[aA][vV]|
|
video/[aA][vV]|
|
||||||
anime/(?P<anime_id>\d+)/play\#
|
anime/(?P<anime_id>\d+)/play\#
|
||||||
)(?P<id_bv>\d+)|
|
)(?P<id>\d+)|
|
||||||
video/[bB][vV](?P<id>[^/?#&]+)
|
video/[bB][vV](?P<id_bv>[^/?#&]+)
|
||||||
)
|
)
|
||||||
|
(?:/?\?p=(?P<page>\d+))?
|
||||||
'''
|
'''
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.bilibili.tv/video/av1074402/',
|
'url': 'http://www.bilibili.com/video/av1074402/',
|
||||||
'md5': '5f7d29e1a2872f3df0cf76b1f87d3788',
|
'md5': '5f7d29e1a2872f3df0cf76b1f87d3788',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1074402',
|
'id': '1074402',
|
||||||
@@ -56,6 +58,10 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
# Tested in BiliBiliBangumiIE
|
# Tested in BiliBiliBangumiIE
|
||||||
'url': 'http://bangumi.bilibili.com/anime/1869/play#40062',
|
'url': 'http://bangumi.bilibili.com/anime/1869/play#40062',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# bilibili.tv
|
||||||
|
'url': 'http://www.bilibili.tv/video/av1074402/',
|
||||||
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://bangumi.bilibili.com/anime/5802/play#100643',
|
'url': 'http://bangumi.bilibili.com/anime/5802/play#100643',
|
||||||
'md5': '3f721ad1e75030cc06faf73587cfec57',
|
'md5': '3f721ad1e75030cc06faf73587cfec57',
|
||||||
@@ -124,12 +130,20 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
url, smuggled_data = unsmuggle_url(url, {})
|
url, smuggled_data = unsmuggle_url(url, {})
|
||||||
|
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
video_id = mobj.group('id') or mobj.group('id_bv')
|
video_id = mobj.group('id_bv') or mobj.group('id')
|
||||||
|
|
||||||
|
av_id, bv_id = self._get_video_id_set(video_id, mobj.group('id_bv') is not None)
|
||||||
|
video_id = av_id
|
||||||
|
|
||||||
anime_id = mobj.group('anime_id')
|
anime_id = mobj.group('anime_id')
|
||||||
|
page_id = mobj.group('page')
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
if 'anime/' not in url:
|
if 'anime/' not in url:
|
||||||
cid = self._search_regex(
|
cid = self._search_regex(
|
||||||
|
r'\bcid(?:["\']:|=)(\d+),["\']page(?:["\']:|=)' + str(page_id), webpage, 'cid',
|
||||||
|
default=None
|
||||||
|
) or self._search_regex(
|
||||||
r'\bcid(?:["\']:|=)(\d+)', webpage, 'cid',
|
r'\bcid(?:["\']:|=)(\d+)', webpage, 'cid',
|
||||||
default=None
|
default=None
|
||||||
) or compat_parse_qs(self._search_regex(
|
) or compat_parse_qs(self._search_regex(
|
||||||
@@ -189,7 +203,7 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
formats.append({
|
formats.append({
|
||||||
'url': backup_url,
|
'url': backup_url,
|
||||||
# backup URLs have lower priorities
|
# backup URLs have lower priorities
|
||||||
'preference': -2 if 'hd.mp4' in backup_url else -3,
|
'quality': -2 if 'hd.mp4' in backup_url else -3,
|
||||||
})
|
})
|
||||||
|
|
||||||
for a_format in formats:
|
for a_format in formats:
|
||||||
@@ -207,9 +221,9 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
break
|
break
|
||||||
|
|
||||||
title = self._html_search_regex(
|
title = self._html_search_regex(
|
||||||
('<h1[^>]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1',
|
(r'<h1[^>]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1',
|
||||||
'(?s)<h1[^>]*>(?P<title>.+?)</h1>'), webpage, 'title',
|
r'(?s)<h1[^>]*>(?P<title>.+?)</h1>'), webpage, 'title',
|
||||||
group='title')
|
group='title') + ('_p' + str(page_id) if page_id is not None else '')
|
||||||
description = self._html_search_meta('description', webpage)
|
description = self._html_search_meta('description', webpage)
|
||||||
timestamp = unified_timestamp(self._html_search_regex(
|
timestamp = unified_timestamp(self._html_search_regex(
|
||||||
r'<time[^>]+datetime="([^"]+)"', webpage, 'upload time',
|
r'<time[^>]+datetime="([^"]+)"', webpage, 'upload time',
|
||||||
@@ -219,7 +233,8 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
|
|
||||||
# TODO 'view_count' requires deobfuscating Javascript
|
# TODO 'view_count' requires deobfuscating Javascript
|
||||||
info = {
|
info = {
|
||||||
'id': video_id,
|
'id': str(video_id) if page_id is None else '%s_p%s' % (video_id, page_id),
|
||||||
|
'cid': cid,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'timestamp': timestamp,
|
'timestamp': timestamp,
|
||||||
@@ -235,27 +250,134 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
'uploader': uploader_mobj.group('name'),
|
'uploader': uploader_mobj.group('name'),
|
||||||
'uploader_id': uploader_mobj.group('id'),
|
'uploader_id': uploader_mobj.group('id'),
|
||||||
})
|
})
|
||||||
|
|
||||||
if not info.get('uploader'):
|
if not info.get('uploader'):
|
||||||
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_tags = self._get_tags(video_id)
|
||||||
|
tags = list(map(lambda x: x['tag_name'], raw_tags))
|
||||||
|
|
||||||
|
top_level_info = {
|
||||||
|
'raw_danmaku': raw_danmaku,
|
||||||
|
'comments': comments,
|
||||||
|
'comment_count': len(comments) if comments is not None else None,
|
||||||
|
'tags': tags,
|
||||||
|
'raw_tags': raw_tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
# Requires https://github.com/m13253/danmaku2ass which is licenced under GPL3
|
||||||
|
# See https://github.com/animelover1984/youtube-dl
|
||||||
|
danmaku = NiconicoIE.CreateDanmaku(raw_danmaku, commentType='Bilibili', x=1024, y=576)
|
||||||
|
entries[0]['subtitles'] = {
|
||||||
|
'danmaku': [{
|
||||||
|
'ext': 'ass',
|
||||||
|
'data': danmaku
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
entry.update(info)
|
entry.update(info)
|
||||||
|
|
||||||
if len(entries) == 1:
|
if len(entries) == 1:
|
||||||
|
entries[0].update(top_level_info)
|
||||||
return entries[0]
|
return entries[0]
|
||||||
else:
|
else:
|
||||||
for idx, entry in enumerate(entries):
|
for idx, entry in enumerate(entries):
|
||||||
entry['id'] = '%s_part%d' % (video_id, (idx + 1))
|
entry['id'] = '%s_part%d' % (video_id, (idx + 1))
|
||||||
|
|
||||||
return {
|
global_info = {
|
||||||
'_type': 'multi_video',
|
'_type': 'multi_video',
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
'bv_id': bv_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'entries': entries,
|
'entries': entries,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
global_info.update(info)
|
||||||
|
global_info.update(top_level_info)
|
||||||
|
|
||||||
|
return global_info
|
||||||
|
|
||||||
|
def _get_video_id_set(self, id, is_bv):
|
||||||
|
query = {'bvid': id} if is_bv else {'aid': id}
|
||||||
|
response = self._download_json(
|
||||||
|
"http://api.bilibili.cn/x/web-interface/view",
|
||||||
|
id, query=query,
|
||||||
|
note='Grabbing original ID via API')
|
||||||
|
|
||||||
|
if response['code'] == -400:
|
||||||
|
raise ExtractorError('Video ID does not exist', expected=True, video_id=id)
|
||||||
|
elif response['code'] != 0:
|
||||||
|
raise ExtractorError('Unknown error occurred during API check (code %s)' % response['code'], expected=True, video_id=id)
|
||||||
|
return (response['data']['aid'], response['data']['bvid'])
|
||||||
|
|
||||||
|
# recursive solution to getting every page of comments for the video
|
||||||
|
# we can stop when we reach a page without any comments
|
||||||
|
def _get_all_comment_pages(self, video_id, commentPageNumber=0):
|
||||||
|
comment_url = "https://api.bilibili.com/x/v2/reply?jsonp=jsonp&pn=%s&type=1&oid=%s&sort=2&_=1567227301685" % (commentPageNumber, video_id)
|
||||||
|
json_str = self._download_webpage(
|
||||||
|
comment_url, video_id,
|
||||||
|
note='Extracting comments from page %s' % (commentPageNumber))
|
||||||
|
replies = json.loads(json_str)['data']['replies']
|
||||||
|
if replies is None:
|
||||||
|
return []
|
||||||
|
return self._get_all_children(replies) + self._get_all_comment_pages(video_id, commentPageNumber + 1)
|
||||||
|
|
||||||
|
# extracts all comments in the tree
|
||||||
|
def _get_all_children(self, replies):
|
||||||
|
if replies is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
ret = []
|
||||||
|
for reply in replies:
|
||||||
|
author = reply['member']['uname']
|
||||||
|
author_id = reply['member']['mid']
|
||||||
|
id = reply['rpid']
|
||||||
|
text = reply['content']['message']
|
||||||
|
timestamp = reply['ctime']
|
||||||
|
parent = reply['parent'] if reply['parent'] != 0 else 'root'
|
||||||
|
|
||||||
|
comment = {
|
||||||
|
"author": author,
|
||||||
|
"author_id": author_id,
|
||||||
|
"id": id,
|
||||||
|
"text": text,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"parent": parent,
|
||||||
|
}
|
||||||
|
ret.append(comment)
|
||||||
|
|
||||||
|
# from the JSON, the comment structure seems arbitrarily deep, but I could be wrong.
|
||||||
|
# Regardless, this should work.
|
||||||
|
ret += self._get_all_children(reply['replies'])
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def _get_raw_danmaku(self, video_id, cid):
|
||||||
|
# This will be useful if I decide to scrape all pages instead of doing them individually
|
||||||
|
# cid_url = "https://www.bilibili.com/widget/getPageList?aid=%s" % (video_id)
|
||||||
|
# cid_str = self._download_webpage(cid_url, video_id, note=False)
|
||||||
|
# cid = json.loads(cid_str)[0]['cid']
|
||||||
|
|
||||||
|
danmaku_url = "https://comment.bilibili.com/%s.xml" % (cid)
|
||||||
|
danmaku = self._download_webpage(danmaku_url, video_id, note='Downloading danmaku comments')
|
||||||
|
return danmaku
|
||||||
|
|
||||||
|
def _get_tags(self, video_id):
|
||||||
|
tags_url = "https://api.bilibili.com/x/tag/archive/tags?aid=%s" % (video_id)
|
||||||
|
tags_json = self._download_json(tags_url, video_id, note='Downloading tags')
|
||||||
|
return tags_json['data']
|
||||||
|
|
||||||
|
|
||||||
class BiliBiliBangumiIE(InfoExtractor):
|
class BiliBiliBangumiIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://bangumi\.bilibili\.com/anime/(?P<id>\d+)'
|
_VALID_URL = r'https?://bangumi\.bilibili\.com/anime/(?P<id>\d+)'
|
||||||
@@ -324,6 +446,73 @@ class BiliBiliBangumiIE(InfoExtractor):
|
|||||||
season_info.get('bangumi_title'), season_info.get('evaluate'))
|
season_info.get('bangumi_title'), season_info.get('evaluate'))
|
||||||
|
|
||||||
|
|
||||||
|
class BilibiliChannelIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://space.bilibili\.com/(?P<id>\d+)'
|
||||||
|
# May need to add support for pagination? Need to find a user with many video uploads to test
|
||||||
|
_API_URL = "https://api.bilibili.com/x/space/arc/search?mid=%s&pn=1&ps=25&jsonp=jsonp"
|
||||||
|
_TEST = {} # TODO: Add tests
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
list_id = self._match_id(url)
|
||||||
|
json_str = self._download_webpage(self._API_URL % list_id, "None")
|
||||||
|
|
||||||
|
json_parsed = json.loads(json_str)
|
||||||
|
entries = [{
|
||||||
|
'_type': 'url',
|
||||||
|
'ie_key': BiliBiliIE.ie_key(),
|
||||||
|
'url': ('https://www.bilibili.com/video/%s' %
|
||||||
|
entry['bvid']),
|
||||||
|
'id': entry['bvid'],
|
||||||
|
} for entry in json_parsed['data']['list']['vlist']]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'id': list_id,
|
||||||
|
'entries': entries
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BiliBiliSearchIE(SearchInfoExtractor):
|
||||||
|
IE_DESC = 'Bilibili video search, "bilisearch" keyword'
|
||||||
|
_MAX_RESULTS = 100000
|
||||||
|
_SEARCH_KEY = 'bilisearch'
|
||||||
|
MAX_NUMBER_OF_RESULTS = 1000
|
||||||
|
|
||||||
|
def _get_n_results(self, query, n):
|
||||||
|
"""Get a specified number of results for a query"""
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
pageNumber = 0
|
||||||
|
while True:
|
||||||
|
pageNumber += 1
|
||||||
|
# FIXME
|
||||||
|
api_url = "https://api.bilibili.com/x/web-interface/search/type?context=&page=%s&order=pubdate&keyword=%s&duration=0&tids_2=&__refresh__=true&search_type=video&tids=0&highlight=1" % (pageNumber, query)
|
||||||
|
json_str = self._download_webpage(
|
||||||
|
api_url, "None", query={"Search_key": query},
|
||||||
|
note='Extracting results from page %s' % pageNumber)
|
||||||
|
data = json.loads(json_str)['data']
|
||||||
|
|
||||||
|
# FIXME: this is hideous
|
||||||
|
if "result" not in data:
|
||||||
|
return {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'id': query,
|
||||||
|
'entries': entries[:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
videos = data['result']
|
||||||
|
for video in videos:
|
||||||
|
e = self.url_result(video['arcurl'], 'BiliBili', str(video['aid']))
|
||||||
|
entries.append(e)
|
||||||
|
|
||||||
|
if(len(entries) >= n or len(videos) >= BiliBiliSearchIE.MAX_NUMBER_OF_RESULTS):
|
||||||
|
return {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'id': query,
|
||||||
|
'entries': entries[:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BilibiliAudioBaseIE(InfoExtractor):
|
class BilibiliAudioBaseIE(InfoExtractor):
|
||||||
def _call_api(self, path, sid, query=None):
|
def _call_api(self, path, sid, query=None):
|
||||||
if not query:
|
if not query:
|
||||||
|
|||||||
@@ -90,13 +90,19 @@ class BleacherReportCMSIE(AMPIE):
|
|||||||
_VALID_URL = r'https?://(?:www\.)?bleacherreport\.com/video_embed\?id=(?P<id>[0-9a-f-]{36}|\d{5})'
|
_VALID_URL = r'https?://(?:www\.)?bleacherreport\.com/video_embed\?id=(?P<id>[0-9a-f-]{36}|\d{5})'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://bleacherreport.com/video_embed?id=8fd44c2f-3dc5-4821-9118-2c825a98c0e1&library=video-cms',
|
'url': 'http://bleacherreport.com/video_embed?id=8fd44c2f-3dc5-4821-9118-2c825a98c0e1&library=video-cms',
|
||||||
'md5': '2e4b0a997f9228ffa31fada5c53d1ed1',
|
'md5': '670b2d73f48549da032861130488c681',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '8fd44c2f-3dc5-4821-9118-2c825a98c0e1',
|
'id': '8fd44c2f-3dc5-4821-9118-2c825a98c0e1',
|
||||||
'ext': 'flv',
|
'ext': 'mp4',
|
||||||
'title': 'Cena vs. Rollins Would Expose the Heavyweight Division',
|
'title': 'Cena vs. Rollins Would Expose the Heavyweight Division',
|
||||||
'description': 'md5:984afb4ade2f9c0db35f3267ed88b36e',
|
'description': 'md5:984afb4ade2f9c0db35f3267ed88b36e',
|
||||||
|
'upload_date': '20150723',
|
||||||
|
'timestamp': 1437679032,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
'expected_warnings': [
|
||||||
|
'Unable to download f4m manifest'
|
||||||
|
]
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class BokeCCBaseIE(InfoExtractor):
|
|||||||
formats = [{
|
formats = [{
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
'url': quality.find('./copy').attrib['playurl'],
|
'url': quality.find('./copy').attrib['playurl'],
|
||||||
'preference': int(quality.attrib['value']),
|
'quality': int(quality.attrib['value']),
|
||||||
} for quality in info_xml.findall('./video/quality')]
|
} for quality in info_xml.findall('./video/quality')]
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class BpbIE(InfoExtractor):
|
|||||||
quality = 'high' if '_high' in video_url else 'low'
|
quality = 'high' if '_high' in video_url else 'low'
|
||||||
formats.append({
|
formats.append({
|
||||||
'url': video_url,
|
'url': video_url,
|
||||||
'preference': 10 if quality == 'high' else 0,
|
'quality': 10 if quality == 'high' else 0,
|
||||||
'format_note': quality,
|
'format_note': quality,
|
||||||
'format_id': '%s-%s' % (quality, determine_ext(video_url)),
|
'format_id': '%s-%s' % (quality, determine_ext(video_url)),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class BravoTVIE(AdobePassIE):
|
class BravoTVIE(AdobePassIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?bravotv\.com/(?:[^/]+/)+(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://(?:www\.)?(?P<req_id>bravotv|oxygen)\.com/(?:[^/]+/)+(?P<id>[^/?#]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.bravotv.com/top-chef/season-16/episode-15/videos/the-top-chef-season-16-winner-is',
|
'url': 'https://www.bravotv.com/top-chef/season-16/episode-15/videos/the-top-chef-season-16-winner-is',
|
||||||
'md5': 'e34684cfea2a96cd2ee1ef3a60909de9',
|
'md5': 'e34684cfea2a96cd2ee1ef3a60909de9',
|
||||||
@@ -28,10 +28,13 @@ class BravoTVIE(AdobePassIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://www.bravotv.com/below-deck/season-3/ep-14-reunion-part-1',
|
'url': 'http://www.bravotv.com/below-deck/season-3/ep-14-reunion-part-1',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.oxygen.com/in-ice-cold-blood/season-2/episode-16/videos/handling-the-horwitz-house-after-the-murder-season-2',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
site, display_id = re.match(self._VALID_URL, url).groups()
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
settings = self._parse_json(self._search_regex(
|
settings = self._parse_json(self._search_regex(
|
||||||
r'<script[^>]+data-drupal-selector="drupal-settings-json"[^>]*>({.+?})</script>', webpage, 'drupal settings'),
|
r'<script[^>]+data-drupal-selector="drupal-settings-json"[^>]*>({.+?})</script>', webpage, 'drupal settings'),
|
||||||
@@ -53,11 +56,14 @@ class BravoTVIE(AdobePassIE):
|
|||||||
tp_path = release_pid = tve['release_pid']
|
tp_path = release_pid = tve['release_pid']
|
||||||
if tve.get('entitlement') == 'auth':
|
if tve.get('entitlement') == 'auth':
|
||||||
adobe_pass = settings.get('tve_adobe_auth', {})
|
adobe_pass = settings.get('tve_adobe_auth', {})
|
||||||
|
if site == 'bravotv':
|
||||||
|
site = 'bravo'
|
||||||
resource = self._get_mvpd_resource(
|
resource = self._get_mvpd_resource(
|
||||||
adobe_pass.get('adobePassResourceId', 'bravo'),
|
adobe_pass.get('adobePassResourceId') or site,
|
||||||
tve['title'], release_pid, tve.get('rating'))
|
tve['title'], release_pid, tve.get('rating'))
|
||||||
query['auth'] = self._extract_mvpd_auth(
|
query['auth'] = self._extract_mvpd_auth(
|
||||||
url, release_pid, adobe_pass.get('adobePassRequestorId', 'bravo'), resource)
|
url, release_pid,
|
||||||
|
adobe_pass.get('adobePassRequestorId') or site, resource)
|
||||||
else:
|
else:
|
||||||
shared_playlist = settings['ls_playlist']
|
shared_playlist = settings['ls_playlist']
|
||||||
account_pid = shared_playlist['account_pid']
|
account_pid = shared_playlist['account_pid']
|
||||||
|
|||||||
@@ -478,11 +478,12 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
container = source.get('container')
|
container = source.get('container')
|
||||||
ext = mimetype2ext(source.get('type'))
|
ext = mimetype2ext(source.get('type'))
|
||||||
src = source.get('src')
|
src = source.get('src')
|
||||||
|
skip_unplayable = not self._downloader.params.get('allow_unplayable_formats')
|
||||||
# https://support.brightcove.com/playback-api-video-fields-reference#key_systems_object
|
# https://support.brightcove.com/playback-api-video-fields-reference#key_systems_object
|
||||||
if container == 'WVM' or source.get('key_systems'):
|
if skip_unplayable and (container == 'WVM' or source.get('key_systems')):
|
||||||
num_drm_sources += 1
|
num_drm_sources += 1
|
||||||
continue
|
continue
|
||||||
elif ext == 'ism':
|
elif ext == 'ism' and skip_unplayable:
|
||||||
continue
|
continue
|
||||||
elif ext == 'm3u8' or container == 'M2TS':
|
elif ext == 'm3u8' or container == 'M2TS':
|
||||||
if not src:
|
if not src:
|
||||||
@@ -546,7 +547,8 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
error = errors[0]
|
error = errors[0]
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
error.get('message') or error.get('error_subcode') or error['error_code'], expected=True)
|
error.get('message') or error.get('error_subcode') or error['error_code'], expected=True)
|
||||||
if sources and num_drm_sources == len(sources):
|
if (not self._downloader.params.get('allow_unplayable_formats')
|
||||||
|
and sources and num_drm_sources == len(sources)):
|
||||||
raise ExtractorError('This video is DRM protected.', expected=True)
|
raise ExtractorError('This video is DRM protected.', expected=True)
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class CamModelsIE(InfoExtractor):
|
|||||||
f.update({
|
f.update({
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
# hls skips fragments, preferring rtmp
|
# hls skips fragments, preferring rtmp
|
||||||
'preference': -1,
|
'quality': -10,
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class CanalplusIE(InfoExtractor):
|
|||||||
# the secret extracted from ya function in http://player.canalplus.fr/common/js/canalPlayer.js
|
# the secret extracted from ya function in http://player.canalplus.fr/common/js/canalPlayer.js
|
||||||
'url': format_url + '?secret=pqzerjlsmdkjfoiuerhsdlfknaes',
|
'url': format_url + '?secret=pqzerjlsmdkjfoiuerhsdlfknaes',
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
'preference': preference(format_id),
|
'quality': preference(format_id),
|
||||||
})
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
|||||||
@@ -7,19 +7,21 @@ from .common import InfoExtractor
|
|||||||
from .gigya import GigyaBaseIE
|
from .gigya import GigyaBaseIE
|
||||||
from ..compat import compat_HTTPError
|
from ..compat import compat_HTTPError
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
extract_attributes,
|
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
strip_or_none,
|
clean_html,
|
||||||
|
extract_attributes,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
|
get_element_by_class,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
merge_dicts,
|
merge_dicts,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
|
strip_or_none,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CanvasIE(InfoExtractor):
|
class CanvasIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://mediazone\.vrt\.be/api/v1/(?P<site_id>canvas|een|ketnet|vrt(?:video|nieuws)|sporza)/assets/(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https?://mediazone\.vrt\.be/api/v1/(?P<site_id>canvas|een|ketnet|vrt(?:video|nieuws)|sporza|dako)/assets/(?P<id>[^/?#&]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://mediazone.vrt.be/api/v1/ketnet/assets/md-ast-4ac54990-ce66-4d00-a8ca-9eac86f4c475',
|
'url': 'https://mediazone.vrt.be/api/v1/ketnet/assets/md-ast-4ac54990-ce66-4d00-a8ca-9eac86f4c475',
|
||||||
'md5': '68993eda72ef62386a15ea2cf3c93107',
|
'md5': '68993eda72ef62386a15ea2cf3c93107',
|
||||||
@@ -332,3 +334,51 @@ class VrtNUIE(GigyaBaseIE):
|
|||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'season_number': int_or_none(page.get('episode_season')),
|
'season_number': int_or_none(page.get('episode_season')),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class DagelijkseKostIE(InfoExtractor):
|
||||||
|
IE_DESC = 'dagelijksekost.een.be'
|
||||||
|
_VALID_URL = r'https?://dagelijksekost\.een\.be/gerechten/(?P<id>[^/?#&]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://dagelijksekost.een.be/gerechten/hachis-parmentier-met-witloof',
|
||||||
|
'md5': '30bfffc323009a3e5f689bef6efa2365',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'md-ast-27a4d1ff-7d7b-425e-b84f-a4d227f592fa',
|
||||||
|
'display_id': 'hachis-parmentier-met-witloof',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Hachis parmentier met witloof',
|
||||||
|
'description': 'md5:9960478392d87f63567b5b117688cdc5',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
'duration': 283.02,
|
||||||
|
},
|
||||||
|
'expected_warnings': ['is not a supported codec'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
|
||||||
|
title = strip_or_none(get_element_by_class(
|
||||||
|
'dish-metadata__title', webpage
|
||||||
|
) or self._html_search_meta(
|
||||||
|
'twitter:title', webpage))
|
||||||
|
|
||||||
|
description = clean_html(get_element_by_class(
|
||||||
|
'dish-description', webpage)
|
||||||
|
) or self._html_search_meta(
|
||||||
|
('description', 'twitter:description', 'og:description'),
|
||||||
|
webpage)
|
||||||
|
|
||||||
|
video_id = self._html_search_regex(
|
||||||
|
r'data-url=(["\'])(?P<id>(?:(?!\1).)+)\1', webpage, 'video id',
|
||||||
|
group='id')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'url': 'https://mediazone.vrt.be/api/v1/dako/assets/%s' % video_id,
|
||||||
|
'ie_key': CanvasIE.ie_key(),
|
||||||
|
'id': video_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import calendar
|
||||||
|
import datetime
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
clean_html,
|
clean_html,
|
||||||
|
extract_timezone,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
parse_iso8601,
|
|
||||||
parse_resolution,
|
parse_resolution,
|
||||||
|
try_get,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,8 +27,9 @@ class CCMAIE(InfoExtractor):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'L\'espot de La Marató de TV3',
|
'title': 'L\'espot de La Marató de TV3',
|
||||||
'description': 'md5:f12987f320e2f6e988e9908e4fe97765',
|
'description': 'md5:f12987f320e2f6e988e9908e4fe97765',
|
||||||
'timestamp': 1470918540,
|
'timestamp': 1478608140,
|
||||||
'upload_date': '20160811',
|
'upload_date': '20161108',
|
||||||
|
'age_limit': 0,
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.ccma.cat/catradio/alacarta/programa/el-consell-de-savis-analitza-el-derbi/audio/943685/',
|
'url': 'http://www.ccma.cat/catradio/alacarta/programa/el-consell-de-savis-analitza-el-derbi/audio/943685/',
|
||||||
@@ -35,8 +39,24 @@ class CCMAIE(InfoExtractor):
|
|||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'El Consell de Savis analitza el derbi',
|
'title': 'El Consell de Savis analitza el derbi',
|
||||||
'description': 'md5:e2a3648145f3241cb9c6b4b624033e53',
|
'description': 'md5:e2a3648145f3241cb9c6b4b624033e53',
|
||||||
'upload_date': '20171205',
|
'upload_date': '20170512',
|
||||||
'timestamp': 1512507300,
|
'timestamp': 1494622500,
|
||||||
|
'vcodec': 'none',
|
||||||
|
'categories': ['Esports'],
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.ccma.cat/tv3/alacarta/crims/crims-josep-tallada-lespereu-me-capitol-1/video/6031387/',
|
||||||
|
'md5': 'b43c3d3486f430f3032b5b160d80cbc3',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6031387',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Crims - Josep Talleda, l\'"Espereu-me" (capítol 1)',
|
||||||
|
'description': 'md5:7cbdafb640da9d0d2c0f62bad1e74e60',
|
||||||
|
'timestamp': 1582577700,
|
||||||
|
'upload_date': '20200224',
|
||||||
|
'subtitles': 'mincount:4',
|
||||||
|
'age_limit': 16,
|
||||||
|
'series': 'Crims',
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@@ -72,17 +92,28 @@ class CCMAIE(InfoExtractor):
|
|||||||
|
|
||||||
informacio = media['informacio']
|
informacio = media['informacio']
|
||||||
title = informacio['titol']
|
title = informacio['titol']
|
||||||
durada = informacio.get('durada', {})
|
durada = informacio.get('durada') or {}
|
||||||
duration = int_or_none(durada.get('milisegons'), 1000) or parse_duration(durada.get('text'))
|
duration = int_or_none(durada.get('milisegons'), 1000) or parse_duration(durada.get('text'))
|
||||||
timestamp = parse_iso8601(informacio.get('data_emissio', {}).get('utc'))
|
tematica = try_get(informacio, lambda x: x['tematica']['text'])
|
||||||
|
|
||||||
|
timestamp = None
|
||||||
|
data_utc = try_get(informacio, lambda x: x['data_emissio']['utc'])
|
||||||
|
try:
|
||||||
|
timezone, data_utc = extract_timezone(data_utc)
|
||||||
|
timestamp = calendar.timegm((datetime.datetime.strptime(
|
||||||
|
data_utc, '%Y-%d-%mT%H:%M:%S') - timezone).timetuple())
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
subtitols = media.get('subtitols', {})
|
subtitols = media.get('subtitols') or []
|
||||||
if subtitols:
|
if isinstance(subtitols, dict):
|
||||||
sub_url = subtitols.get('url')
|
subtitols = [subtitols]
|
||||||
|
for st in subtitols:
|
||||||
|
sub_url = st.get('url')
|
||||||
if sub_url:
|
if sub_url:
|
||||||
subtitles.setdefault(
|
subtitles.setdefault(
|
||||||
subtitols.get('iso') or subtitols.get('text') or 'ca', []).append({
|
st.get('iso') or st.get('text') or 'ca', []).append({
|
||||||
'url': sub_url,
|
'url': sub_url,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -97,6 +128,16 @@ class CCMAIE(InfoExtractor):
|
|||||||
'height': int_or_none(imatges.get('alcada')),
|
'height': int_or_none(imatges.get('alcada')),
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
age_limit = None
|
||||||
|
codi_etic = try_get(informacio, lambda x: x['codi_etic']['id'])
|
||||||
|
if codi_etic:
|
||||||
|
codi_etic_s = codi_etic.split('_')
|
||||||
|
if len(codi_etic_s) == 2:
|
||||||
|
if codi_etic_s[1] == 'TP':
|
||||||
|
age_limit = 0
|
||||||
|
else:
|
||||||
|
age_limit = int_or_none(codi_etic_s[1])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': media_id,
|
'id': media_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
@@ -106,4 +147,9 @@ class CCMAIE(InfoExtractor):
|
|||||||
'thumbnails': thumbnails,
|
'thumbnails': thumbnails,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
'age_limit': age_limit,
|
||||||
|
'alt_title': informacio.get('titol_complet'),
|
||||||
|
'episode_number': int_or_none(informacio.get('capitol')),
|
||||||
|
'categories': [tematica] if tematica else None,
|
||||||
|
'series': informacio.get('programa'),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ class CCTVIE(InfoExtractor):
|
|||||||
'url': video_url,
|
'url': video_url,
|
||||||
'format_id': 'http',
|
'format_id': 'http',
|
||||||
'quality': quality,
|
'quality': quality,
|
||||||
'preference': -1,
|
'source_preference': -10
|
||||||
})
|
})
|
||||||
|
|
||||||
hls_url = try_get(data, lambda x: x['hls_url'], compat_str)
|
hls_url = try_get(data, lambda x: x['hls_url'], compat_str)
|
||||||
|
|||||||
@@ -95,8 +95,11 @@ class CDAIE(InfoExtractor):
|
|||||||
if 'Ten film jest dostępny dla użytkowników premium' in webpage:
|
if 'Ten film jest dostępny dla użytkowników premium' in webpage:
|
||||||
raise ExtractorError('This video is only available for premium users.', expected=True)
|
raise ExtractorError('This video is only available for premium users.', expected=True)
|
||||||
|
|
||||||
|
if re.search(r'niedostępn[ey] w(?: |\s+)Twoim kraju\s*<', webpage):
|
||||||
|
self.raise_geo_restricted()
|
||||||
|
|
||||||
need_confirm_age = False
|
need_confirm_age = False
|
||||||
if self._html_search_regex(r'(<form[^>]+action="/a/validatebirth")',
|
if self._html_search_regex(r'(<form[^>]+action="[^"]*/a/validatebirth[^"]*")',
|
||||||
webpage, 'birthday validate form', default=None):
|
webpage, 'birthday validate form', default=None):
|
||||||
webpage = self._download_age_confirm_page(
|
webpage = self._download_age_confirm_page(
|
||||||
url, video_id, note='Confirming age')
|
url, video_id, note='Confirming age')
|
||||||
|
|||||||
@@ -147,7 +147,8 @@ class CeskaTelevizeIE(InfoExtractor):
|
|||||||
is_live = item.get('type') == 'LIVE'
|
is_live = item.get('type') == 'LIVE'
|
||||||
formats = []
|
formats = []
|
||||||
for format_id, stream_url in item.get('streamUrls', {}).items():
|
for format_id, stream_url in item.get('streamUrls', {}).items():
|
||||||
if 'drmOnly=true' in stream_url:
|
if (not self._downloader.params.get('allow_unplayable_formats')
|
||||||
|
and 'drmOnly=true' in stream_url):
|
||||||
continue
|
continue
|
||||||
if 'playerType=flash' in stream_url:
|
if 'playerType=flash' in stream_url:
|
||||||
stream_formats = self._extract_m3u8_formats(
|
stream_formats = self._extract_m3u8_formats(
|
||||||
|
|||||||
@@ -336,9 +336,8 @@ class InfoExtractor(object):
|
|||||||
There must be a key "entries", which is a list, an iterable, or a PagedList
|
There must be a key "entries", which is a list, an iterable, or a PagedList
|
||||||
object, each element of which is a valid dictionary by this specification.
|
object, each element of which is a valid dictionary by this specification.
|
||||||
|
|
||||||
Additionally, playlists can have "id", "title", "description", "uploader",
|
Additionally, playlists can have "id", "title", and any other relevent
|
||||||
"uploader_id", "uploader_url", "duration" attributes with the same semantics
|
attributes with the same semantics as videos (see above).
|
||||||
as videos (see above).
|
|
||||||
|
|
||||||
|
|
||||||
_type "multi_video" indicates that there are multiple videos that
|
_type "multi_video" indicates that there are multiple videos that
|
||||||
@@ -967,15 +966,16 @@ class InfoExtractor(object):
|
|||||||
urls, playlist_id=playlist_id, playlist_title=playlist_title)
|
urls, playlist_id=playlist_id, playlist_title=playlist_title)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def playlist_result(entries, playlist_id=None, playlist_title=None, playlist_description=None):
|
def playlist_result(entries, playlist_id=None, playlist_title=None, playlist_description=None, **kwargs):
|
||||||
"""Returns a playlist"""
|
"""Returns a playlist"""
|
||||||
video_info = {'_type': 'playlist',
|
video_info = {'_type': 'playlist',
|
||||||
'entries': entries}
|
'entries': entries}
|
||||||
|
video_info.update(kwargs)
|
||||||
if playlist_id:
|
if playlist_id:
|
||||||
video_info['id'] = playlist_id
|
video_info['id'] = playlist_id
|
||||||
if playlist_title:
|
if playlist_title:
|
||||||
video_info['title'] = playlist_title
|
video_info['title'] = playlist_title
|
||||||
if playlist_description:
|
if playlist_description is not None:
|
||||||
video_info['description'] = playlist_description
|
video_info['description'] = playlist_description
|
||||||
return video_info
|
return video_info
|
||||||
|
|
||||||
@@ -1366,16 +1366,16 @@ class InfoExtractor(object):
|
|||||||
class FormatSort:
|
class FormatSort:
|
||||||
regex = r' *((?P<reverse>\+)?(?P<field>[a-zA-Z0-9_]+)((?P<seperator>[~:])(?P<limit>.*?))?)? *$'
|
regex = r' *((?P<reverse>\+)?(?P<field>[a-zA-Z0-9_]+)((?P<seperator>[~:])(?P<limit>.*?))?)? *$'
|
||||||
|
|
||||||
default = ('hidden', 'has_video', 'extractor', 'lang', 'quality',
|
default = ('hidden', 'hasvid', 'ie_pref', 'lang', 'quality',
|
||||||
'res', 'fps', 'codec', 'size', 'br', 'asr',
|
'res', 'fps', 'codec:vp9.2', 'size', 'br', 'asr',
|
||||||
'proto', 'ext', 'has_audio', 'source', 'format_id')
|
'proto', 'ext', 'has_audio', 'source', 'format_id') # These must not be aliases
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
'vcodec': {'type': 'ordered', 'regex': True,
|
'vcodec': {'type': 'ordered', 'regex': True,
|
||||||
'order': ['vp9', '(h265|he?vc?)', '(h264|avc)', 'vp8', '(mp4v|h263)', 'theora', '', None, 'none']},
|
'order': ['av0?1', 'vp0?9.2', 'vp0?9', '[hx]265|he?vc?', '[hx]264|avc', 'vp0?8', 'mp4v|h263', 'theora', '', None, 'none']},
|
||||||
'acodec': {'type': 'ordered', 'regex': True,
|
'acodec': {'type': 'ordered', 'regex': True,
|
||||||
'order': ['opus', 'vorbis', 'aac', 'mp?4a?', 'mp3', 'e?a?c-?3', 'dts', '', None, 'none']},
|
'order': ['opus', 'vorbis', 'aac', 'mp?4a?', 'mp3', 'e?a?c-?3', 'dts', '', None, 'none']},
|
||||||
'proto': {'type': 'ordered', 'regex': True,
|
'proto': {'type': 'ordered', 'regex': True, 'field': 'protocol',
|
||||||
'order': ['(ht|f)tps', '(ht|f)tp$', 'm3u8.+', 'm3u8', '.*dash', '', 'mms|rtsp', 'none', 'f4']},
|
'order': ['(ht|f)tps', '(ht|f)tp$', 'm3u8.+', 'm3u8', '.*dash', '', 'mms|rtsp', 'none', 'f4']},
|
||||||
'vext': {'type': 'ordered', 'field': 'video_ext',
|
'vext': {'type': 'ordered', 'field': 'video_ext',
|
||||||
'order': ('mp4', 'webm', 'flv', '', 'none'),
|
'order': ('mp4', 'webm', 'flv', '', 'none'),
|
||||||
@@ -1387,11 +1387,11 @@ class InfoExtractor(object):
|
|||||||
'ie_pref': {'priority': True, 'type': 'extractor'},
|
'ie_pref': {'priority': True, 'type': 'extractor'},
|
||||||
'hasvid': {'priority': True, 'field': 'vcodec', 'type': 'boolean', 'not_in_list': ('none',)},
|
'hasvid': {'priority': True, 'field': 'vcodec', 'type': 'boolean', 'not_in_list': ('none',)},
|
||||||
'hasaud': {'field': 'acodec', 'type': 'boolean', 'not_in_list': ('none',)},
|
'hasaud': {'field': 'acodec', 'type': 'boolean', 'not_in_list': ('none',)},
|
||||||
'lang': {'priority': True, 'convert': 'ignore'},
|
'lang': {'priority': True, 'convert': 'ignore', 'type': 'extractor', 'field': 'language_preference'},
|
||||||
'quality': {'priority': True, 'convert': 'float_none'},
|
'quality': {'convert': 'float_none', 'type': 'extractor'},
|
||||||
'filesize': {'convert': 'bytes'},
|
'filesize': {'convert': 'bytes'},
|
||||||
'fs_approx': {'convert': 'bytes'},
|
'fs_approx': {'convert': 'bytes', 'field': 'filesize_approx'},
|
||||||
'id': {'convert': 'string'},
|
'id': {'convert': 'string', 'field': 'format_id'},
|
||||||
'height': {'convert': 'float_none'},
|
'height': {'convert': 'float_none'},
|
||||||
'width': {'convert': 'float_none'},
|
'width': {'convert': 'float_none'},
|
||||||
'fps': {'convert': 'float_none'},
|
'fps': {'convert': 'float_none'},
|
||||||
@@ -1399,7 +1399,7 @@ class InfoExtractor(object):
|
|||||||
'vbr': {'convert': 'float_none'},
|
'vbr': {'convert': 'float_none'},
|
||||||
'abr': {'convert': 'float_none'},
|
'abr': {'convert': 'float_none'},
|
||||||
'asr': {'convert': 'float_none'},
|
'asr': {'convert': 'float_none'},
|
||||||
'source': {'convert': 'ignore'},
|
'source': {'convert': 'ignore', 'type': 'extractor', 'field': 'source_preference'},
|
||||||
|
|
||||||
'codec': {'type': 'combined', 'field': ('vcodec', 'acodec')},
|
'codec': {'type': 'combined', 'field': ('vcodec', 'acodec')},
|
||||||
'br': {'type': 'combined', 'field': ('tbr', 'vbr', 'abr'), 'same_limit': True},
|
'br': {'type': 'combined', 'field': ('tbr', 'vbr', 'abr'), 'same_limit': True},
|
||||||
@@ -1469,13 +1469,12 @@ class InfoExtractor(object):
|
|||||||
elif conversion == 'bytes':
|
elif conversion == 'bytes':
|
||||||
return FileDownloader.parse_bytes(value)
|
return FileDownloader.parse_bytes(value)
|
||||||
elif conversion == 'order':
|
elif conversion == 'order':
|
||||||
order_free = self._get_field_setting(field, 'order_free')
|
order_list = (self._use_free_order and self._get_field_setting(field, 'order_free')) or self._get_field_setting(field, 'order')
|
||||||
order_list = order_free if order_free and self._use_free_order else self._get_field_setting(field, 'order')
|
|
||||||
use_regex = self._get_field_setting(field, 'regex')
|
use_regex = self._get_field_setting(field, 'regex')
|
||||||
list_length = len(order_list)
|
list_length = len(order_list)
|
||||||
empty_pos = order_list.index('') if '' in order_list else list_length + 1
|
empty_pos = order_list.index('') if '' in order_list else list_length + 1
|
||||||
if use_regex and value is not None:
|
if use_regex and value is not None:
|
||||||
for (i, regex) in enumerate(order_list):
|
for i, regex in enumerate(order_list):
|
||||||
if regex and re.match(regex, value):
|
if regex and re.match(regex, value):
|
||||||
return list_length - i
|
return list_length - i
|
||||||
return list_length - empty_pos # not in list
|
return list_length - empty_pos # not in list
|
||||||
@@ -1544,7 +1543,7 @@ class InfoExtractor(object):
|
|||||||
def print_verbose_info(self, to_screen):
|
def print_verbose_info(self, to_screen):
|
||||||
to_screen('[debug] Sort order given by user: %s' % ','.join(self._sort_user))
|
to_screen('[debug] Sort order given by user: %s' % ','.join(self._sort_user))
|
||||||
if self._sort_extractor:
|
if self._sort_extractor:
|
||||||
to_screen('[debug] Sort order given by extractor: %s' % ','.join(self._sort_extractor))
|
to_screen('[debug] Sort order given by extractor: %s' % ', '.join(self._sort_extractor))
|
||||||
to_screen('[debug] Formats sorted by: %s' % ', '.join(['%s%s%s' % (
|
to_screen('[debug] Formats sorted by: %s' % ', '.join(['%s%s%s' % (
|
||||||
'+' if self._get_field_setting(field, 'reverse') else '', field,
|
'+' if self._get_field_setting(field, 'reverse') else '', field,
|
||||||
'%s%s(%s)' % ('~' if self._get_field_setting(field, 'closest') else ':',
|
'%s%s(%s)' % ('~' if self._get_field_setting(field, 'closest') else ':',
|
||||||
@@ -1561,7 +1560,7 @@ class InfoExtractor(object):
|
|||||||
if type == 'extractor':
|
if type == 'extractor':
|
||||||
maximum = self._get_field_setting(field, 'max')
|
maximum = self._get_field_setting(field, 'max')
|
||||||
if value is None or (maximum is not None and value >= maximum):
|
if value is None or (maximum is not None and value >= maximum):
|
||||||
value = 0
|
value = -1
|
||||||
elif type == 'boolean':
|
elif type == 'boolean':
|
||||||
in_list = self._get_field_setting(field, 'in_list')
|
in_list = self._get_field_setting(field, 'in_list')
|
||||||
not_in_list = self._get_field_setting(field, 'not_in_list')
|
not_in_list = self._get_field_setting(field, 'not_in_list')
|
||||||
@@ -1694,7 +1693,7 @@ class InfoExtractor(object):
|
|||||||
self.to_screen(msg)
|
self.to_screen(msg)
|
||||||
time.sleep(timeout)
|
time.sleep(timeout)
|
||||||
|
|
||||||
def _extract_f4m_formats(self, manifest_url, video_id, preference=None, f4m_id=None,
|
def _extract_f4m_formats(self, manifest_url, video_id, preference=None, quality=None, f4m_id=None,
|
||||||
transform_source=lambda s: fix_xml_ampersands(s).strip(),
|
transform_source=lambda s: fix_xml_ampersands(s).strip(),
|
||||||
fatal=True, m3u8_id=None, data=None, headers={}, query={}):
|
fatal=True, m3u8_id=None, data=None, headers={}, query={}):
|
||||||
manifest = self._download_xml(
|
manifest = self._download_xml(
|
||||||
@@ -1709,10 +1708,10 @@ class InfoExtractor(object):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
return self._parse_f4m_formats(
|
return self._parse_f4m_formats(
|
||||||
manifest, manifest_url, video_id, preference=preference, f4m_id=f4m_id,
|
manifest, manifest_url, video_id, preference=preference, quality=quality, f4m_id=f4m_id,
|
||||||
transform_source=transform_source, fatal=fatal, m3u8_id=m3u8_id)
|
transform_source=transform_source, fatal=fatal, m3u8_id=m3u8_id)
|
||||||
|
|
||||||
def _parse_f4m_formats(self, manifest, manifest_url, video_id, preference=None, f4m_id=None,
|
def _parse_f4m_formats(self, manifest, manifest_url, video_id, preference=None, quality=None, f4m_id=None,
|
||||||
transform_source=lambda s: fix_xml_ampersands(s).strip(),
|
transform_source=lambda s: fix_xml_ampersands(s).strip(),
|
||||||
fatal=True, m3u8_id=None):
|
fatal=True, m3u8_id=None):
|
||||||
if not isinstance(manifest, compat_etree_Element) and not fatal:
|
if not isinstance(manifest, compat_etree_Element) and not fatal:
|
||||||
@@ -1777,7 +1776,7 @@ class InfoExtractor(object):
|
|||||||
ext = determine_ext(manifest_url)
|
ext = determine_ext(manifest_url)
|
||||||
if ext == 'f4m':
|
if ext == 'f4m':
|
||||||
f4m_formats = self._extract_f4m_formats(
|
f4m_formats = self._extract_f4m_formats(
|
||||||
manifest_url, video_id, preference=preference, f4m_id=f4m_id,
|
manifest_url, video_id, preference=preference, quality=quality, f4m_id=f4m_id,
|
||||||
transform_source=transform_source, fatal=fatal)
|
transform_source=transform_source, fatal=fatal)
|
||||||
# Sometimes stream-level manifest contains single media entry that
|
# Sometimes stream-level manifest contains single media entry that
|
||||||
# does not contain any quality metadata (e.g. http://matchtv.ru/#live-player).
|
# does not contain any quality metadata (e.g. http://matchtv.ru/#live-player).
|
||||||
@@ -1797,7 +1796,7 @@ class InfoExtractor(object):
|
|||||||
elif ext == 'm3u8':
|
elif ext == 'm3u8':
|
||||||
formats.extend(self._extract_m3u8_formats(
|
formats.extend(self._extract_m3u8_formats(
|
||||||
manifest_url, video_id, 'mp4', preference=preference,
|
manifest_url, video_id, 'mp4', preference=preference,
|
||||||
m3u8_id=m3u8_id, fatal=fatal))
|
quality=quality, m3u8_id=m3u8_id, fatal=fatal))
|
||||||
continue
|
continue
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
@@ -1810,22 +1809,24 @@ class InfoExtractor(object):
|
|||||||
'height': height,
|
'height': height,
|
||||||
'vcodec': vcodec,
|
'vcodec': vcodec,
|
||||||
'preference': preference,
|
'preference': preference,
|
||||||
|
'quality': quality,
|
||||||
})
|
})
|
||||||
return formats
|
return formats
|
||||||
|
|
||||||
def _m3u8_meta_format(self, m3u8_url, ext=None, preference=None, m3u8_id=None):
|
def _m3u8_meta_format(self, m3u8_url, ext=None, preference=None, quality=None, m3u8_id=None):
|
||||||
return {
|
return {
|
||||||
'format_id': '-'.join(filter(None, [m3u8_id, 'meta'])),
|
'format_id': '-'.join(filter(None, [m3u8_id, 'meta'])),
|
||||||
'url': m3u8_url,
|
'url': m3u8_url,
|
||||||
'ext': ext,
|
'ext': ext,
|
||||||
'protocol': 'm3u8',
|
'protocol': 'm3u8',
|
||||||
'preference': preference - 100 if preference else -100,
|
'preference': preference - 100 if preference else -100,
|
||||||
|
'quality': quality,
|
||||||
'resolution': 'multiple',
|
'resolution': 'multiple',
|
||||||
'format_note': 'Quality selection URL',
|
'format_note': 'Quality selection URL',
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
entry_protocol='m3u8', preference=None, quality=None,
|
||||||
m3u8_id=None, note=None, errnote=None,
|
m3u8_id=None, note=None, errnote=None,
|
||||||
fatal=True, live=False, data=None, headers={},
|
fatal=True, live=False, data=None, headers={},
|
||||||
query={}):
|
query={}):
|
||||||
@@ -1843,10 +1844,10 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
return self._parse_m3u8_formats(
|
return self._parse_m3u8_formats(
|
||||||
m3u8_doc, m3u8_url, ext=ext, entry_protocol=entry_protocol,
|
m3u8_doc, m3u8_url, ext=ext, entry_protocol=entry_protocol,
|
||||||
preference=preference, m3u8_id=m3u8_id, live=live)
|
preference=preference, quality=quality, m3u8_id=m3u8_id, live=live)
|
||||||
|
|
||||||
def _parse_m3u8_formats(self, m3u8_doc, m3u8_url, ext=None,
|
def _parse_m3u8_formats(self, m3u8_doc, m3u8_url, ext=None,
|
||||||
entry_protocol='m3u8', preference=None,
|
entry_protocol='m3u8', preference=None, quality=None,
|
||||||
m3u8_id=None, live=False):
|
m3u8_id=None, live=False):
|
||||||
if '#EXT-X-FAXS-CM:' in m3u8_doc: # Adobe Flash Access
|
if '#EXT-X-FAXS-CM:' in m3u8_doc: # Adobe Flash Access
|
||||||
return []
|
return []
|
||||||
@@ -1884,6 +1885,7 @@ class InfoExtractor(object):
|
|||||||
'ext': ext,
|
'ext': ext,
|
||||||
'protocol': entry_protocol,
|
'protocol': entry_protocol,
|
||||||
'preference': preference,
|
'preference': preference,
|
||||||
|
'quality': quality,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
groups = {}
|
groups = {}
|
||||||
@@ -1912,6 +1914,7 @@ class InfoExtractor(object):
|
|||||||
'ext': ext,
|
'ext': ext,
|
||||||
'protocol': entry_protocol,
|
'protocol': entry_protocol,
|
||||||
'preference': preference,
|
'preference': preference,
|
||||||
|
'quality': quality,
|
||||||
}
|
}
|
||||||
if media_type == 'AUDIO':
|
if media_type == 'AUDIO':
|
||||||
f['vcodec'] = 'none'
|
f['vcodec'] = 'none'
|
||||||
@@ -1971,6 +1974,7 @@ class InfoExtractor(object):
|
|||||||
'fps': float_or_none(last_stream_inf.get('FRAME-RATE')),
|
'fps': float_or_none(last_stream_inf.get('FRAME-RATE')),
|
||||||
'protocol': entry_protocol,
|
'protocol': entry_protocol,
|
||||||
'preference': preference,
|
'preference': preference,
|
||||||
|
'quality': quality,
|
||||||
}
|
}
|
||||||
resolution = last_stream_inf.get('RESOLUTION')
|
resolution = last_stream_inf.get('RESOLUTION')
|
||||||
if resolution:
|
if resolution:
|
||||||
@@ -2264,7 +2268,7 @@ class InfoExtractor(object):
|
|||||||
})
|
})
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
def _extract_mpd_formats(self, mpd_url, video_id, mpd_id=None, note=None, errnote=None, fatal=True, formats_dict={}, data=None, headers={}, query={}):
|
def _extract_mpd_formats(self, mpd_url, video_id, mpd_id=None, note=None, errnote=None, fatal=True, data=None, headers={}, query={}):
|
||||||
res = self._download_xml_handle(
|
res = self._download_xml_handle(
|
||||||
mpd_url, video_id,
|
mpd_url, video_id,
|
||||||
note=note or 'Downloading MPD manifest',
|
note=note or 'Downloading MPD manifest',
|
||||||
@@ -2278,10 +2282,9 @@ class InfoExtractor(object):
|
|||||||
mpd_base_url = base_url(urlh.geturl())
|
mpd_base_url = base_url(urlh.geturl())
|
||||||
|
|
||||||
return self._parse_mpd_formats(
|
return self._parse_mpd_formats(
|
||||||
mpd_doc, mpd_id=mpd_id, mpd_base_url=mpd_base_url,
|
mpd_doc, mpd_id, mpd_base_url, mpd_url)
|
||||||
formats_dict=formats_dict, mpd_url=mpd_url)
|
|
||||||
|
|
||||||
def _parse_mpd_formats(self, mpd_doc, mpd_id=None, mpd_base_url='', formats_dict={}, mpd_url=None):
|
def _parse_mpd_formats(self, mpd_doc, mpd_id=None, mpd_base_url='', mpd_url=None):
|
||||||
"""
|
"""
|
||||||
Parse formats from MPD manifest.
|
Parse formats from MPD manifest.
|
||||||
References:
|
References:
|
||||||
@@ -2359,6 +2362,8 @@ class InfoExtractor(object):
|
|||||||
extract_Initialization(segment_template)
|
extract_Initialization(segment_template)
|
||||||
return ms_info
|
return ms_info
|
||||||
|
|
||||||
|
skip_unplayable = not self._downloader.params.get('allow_unplayable_formats')
|
||||||
|
|
||||||
mpd_duration = parse_duration(mpd_doc.get('mediaPresentationDuration'))
|
mpd_duration = parse_duration(mpd_doc.get('mediaPresentationDuration'))
|
||||||
formats = []
|
formats = []
|
||||||
for period in mpd_doc.findall(_add_ns('Period')):
|
for period in mpd_doc.findall(_add_ns('Period')):
|
||||||
@@ -2368,11 +2373,11 @@ class InfoExtractor(object):
|
|||||||
'timescale': 1,
|
'timescale': 1,
|
||||||
})
|
})
|
||||||
for adaptation_set in period.findall(_add_ns('AdaptationSet')):
|
for adaptation_set in period.findall(_add_ns('AdaptationSet')):
|
||||||
if is_drm_protected(adaptation_set):
|
if skip_unplayable and is_drm_protected(adaptation_set):
|
||||||
continue
|
continue
|
||||||
adaption_set_ms_info = extract_multisegment_info(adaptation_set, period_ms_info)
|
adaption_set_ms_info = extract_multisegment_info(adaptation_set, period_ms_info)
|
||||||
for representation in adaptation_set.findall(_add_ns('Representation')):
|
for representation in adaptation_set.findall(_add_ns('Representation')):
|
||||||
if is_drm_protected(representation):
|
if skip_unplayable and is_drm_protected(representation):
|
||||||
continue
|
continue
|
||||||
representation_attrib = adaptation_set.attrib.copy()
|
representation_attrib = adaptation_set.attrib.copy()
|
||||||
representation_attrib.update(representation.attrib)
|
representation_attrib.update(representation.attrib)
|
||||||
@@ -2560,15 +2565,7 @@ class InfoExtractor(object):
|
|||||||
else:
|
else:
|
||||||
# Assuming direct URL to unfragmented media.
|
# Assuming direct URL to unfragmented media.
|
||||||
f['url'] = base_url
|
f['url'] = base_url
|
||||||
|
formats.append(f)
|
||||||
# According to [1, 5.3.5.2, Table 7, page 35] @id of Representation
|
|
||||||
# is not necessarily unique within a Period thus formats with
|
|
||||||
# the same `format_id` are quite possible. There are numerous examples
|
|
||||||
# of such manifests (see https://github.com/ytdl-org/youtube-dl/issues/15111,
|
|
||||||
# https://github.com/ytdl-org/youtube-dl/issues/13919)
|
|
||||||
full_info = formats_dict.get(representation_id, {}).copy()
|
|
||||||
full_info.update(f)
|
|
||||||
formats.append(full_info)
|
|
||||||
else:
|
else:
|
||||||
self.report_warning('Unknown MIME type %s in DASH manifest' % mime_type)
|
self.report_warning('Unknown MIME type %s in DASH manifest' % mime_type)
|
||||||
return formats
|
return formats
|
||||||
@@ -2594,7 +2591,10 @@ class InfoExtractor(object):
|
|||||||
1. [MS-SSTR]: Smooth Streaming Protocol,
|
1. [MS-SSTR]: Smooth Streaming Protocol,
|
||||||
https://msdn.microsoft.com/en-us/library/ff469518.aspx
|
https://msdn.microsoft.com/en-us/library/ff469518.aspx
|
||||||
"""
|
"""
|
||||||
if ism_doc.get('IsLive') == 'TRUE' or ism_doc.find('Protection') is not None:
|
if ism_doc.get('IsLive') == 'TRUE':
|
||||||
|
return []
|
||||||
|
if (not self._downloader.params.get('allow_unplayable_formats')
|
||||||
|
and ism_doc.find('Protection') is not None):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
duration = int(ism_doc.attrib['Duration'])
|
duration = int(ism_doc.attrib['Duration'])
|
||||||
@@ -2682,7 +2682,7 @@ class InfoExtractor(object):
|
|||||||
})
|
})
|
||||||
return formats
|
return formats
|
||||||
|
|
||||||
def _parse_html5_media_entries(self, base_url, webpage, video_id, m3u8_id=None, m3u8_entry_protocol='m3u8', mpd_id=None, preference=None):
|
def _parse_html5_media_entries(self, base_url, webpage, video_id, m3u8_id=None, m3u8_entry_protocol='m3u8', mpd_id=None, preference=None, quality=None):
|
||||||
def absolute_url(item_url):
|
def absolute_url(item_url):
|
||||||
return urljoin(base_url, item_url)
|
return urljoin(base_url, item_url)
|
||||||
|
|
||||||
@@ -2705,7 +2705,7 @@ class InfoExtractor(object):
|
|||||||
formats = self._extract_m3u8_formats(
|
formats = self._extract_m3u8_formats(
|
||||||
full_url, video_id, ext='mp4',
|
full_url, video_id, ext='mp4',
|
||||||
entry_protocol=m3u8_entry_protocol, m3u8_id=m3u8_id,
|
entry_protocol=m3u8_entry_protocol, m3u8_id=m3u8_id,
|
||||||
preference=preference, fatal=False)
|
preference=preference, quality=quality, fatal=False)
|
||||||
elif ext == 'mpd':
|
elif ext == 'mpd':
|
||||||
is_plain_url = False
|
is_plain_url = False
|
||||||
formats = self._extract_mpd_formats(
|
formats = self._extract_mpd_formats(
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class CoubIE(InfoExtractor):
|
|||||||
'filesize': int_or_none(item.get('size')),
|
'filesize': int_or_none(item.get('size')),
|
||||||
'vcodec': 'none' if kind == 'audio' else None,
|
'vcodec': 'none' if kind == 'audio' else None,
|
||||||
'quality': quality_key(quality),
|
'quality': quality_key(quality),
|
||||||
'preference': preference_key(HTML5),
|
'source_preference': preference_key(HTML5),
|
||||||
})
|
})
|
||||||
|
|
||||||
iphone_url = file_versions.get(IPHONE, {}).get('url')
|
iphone_url = file_versions.get(IPHONE, {}).get('url')
|
||||||
@@ -95,7 +95,7 @@ class CoubIE(InfoExtractor):
|
|||||||
formats.append({
|
formats.append({
|
||||||
'url': iphone_url,
|
'url': iphone_url,
|
||||||
'format_id': IPHONE,
|
'format_id': IPHONE,
|
||||||
'preference': preference_key(IPHONE),
|
'source_preference': preference_key(IPHONE),
|
||||||
})
|
})
|
||||||
|
|
||||||
mobile_url = file_versions.get(MOBILE, {}).get('audio_url')
|
mobile_url = file_versions.get(MOBILE, {}).get('audio_url')
|
||||||
@@ -103,7 +103,7 @@ class CoubIE(InfoExtractor):
|
|||||||
formats.append({
|
formats.append({
|
||||||
'url': mobile_url,
|
'url': mobile_url,
|
||||||
'format_id': '%s-audio' % MOBILE,
|
'format_id': '%s-audio' % MOBILE,
|
||||||
'preference': preference_key(MOBILE),
|
'source_preference': preference_key(MOBILE),
|
||||||
})
|
})
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class CrackleIE(InfoExtractor):
|
|||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for e in media['MediaURLs']:
|
for e in media['MediaURLs']:
|
||||||
if e.get('UseDRM') is True:
|
if not self._downloader.params.get('allow_unplayable_formats') and e.get('UseDRM') is True:
|
||||||
continue
|
continue
|
||||||
format_url = url_or_none(e.get('Path'))
|
format_url = url_or_none(e.get('Path'))
|
||||||
if not format_url:
|
if not format_url:
|
||||||
|
|||||||
@@ -473,15 +473,11 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
stream.get('url'), video_id, stream.get('format'),
|
stream.get('url'), video_id, stream.get('format'),
|
||||||
audio_lang, hardsub_lang)
|
audio_lang, hardsub_lang)
|
||||||
for f in vrv_formats:
|
for f in vrv_formats:
|
||||||
if not hardsub_lang:
|
f['language_preference'] = 1 if audio_lang == language else 0
|
||||||
f['preference'] = 1
|
f['quality'] = (
|
||||||
language_preference = 0
|
1 if not hardsub_lang
|
||||||
if audio_lang == language:
|
else 0 if hardsub_lang == language
|
||||||
language_preference += 1
|
else -1)
|
||||||
if hardsub_lang == language:
|
|
||||||
language_preference += 1
|
|
||||||
if language_preference:
|
|
||||||
f['language_preference'] = language_preference
|
|
||||||
formats.extend(vrv_formats)
|
formats.extend(vrv_formats)
|
||||||
if not formats:
|
if not formats:
|
||||||
available_fmts = []
|
available_fmts = []
|
||||||
@@ -571,7 +567,7 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
})
|
})
|
||||||
formats.append(format_info)
|
formats.append(format_info)
|
||||||
self._sort_formats(formats, ('preference', 'language_preference', 'height', 'width', 'tbr', 'fps'))
|
self._sort_formats(formats)
|
||||||
|
|
||||||
metadata = self._call_rpc_api(
|
metadata = self._call_rpc_api(
|
||||||
'VideoPlayer_GetMediaMetadata', video_id,
|
'VideoPlayer_GetMediaMetadata', video_id,
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ class DigitallySpeakingIE(InfoExtractor):
|
|||||||
'play_path': remove_end(audio.get('url'), '.flv'),
|
'play_path': remove_end(audio.get('url'), '.flv'),
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'vcodec': 'none',
|
'vcodec': 'none',
|
||||||
|
'quality': 1,
|
||||||
'format_id': audio.get('code'),
|
'format_id': audio.get('code'),
|
||||||
})
|
})
|
||||||
slide_video_path = xpath_text(metadata, './slideVideo', fatal=True)
|
slide_video_path = xpath_text(metadata, './slideVideo', fatal=True)
|
||||||
@@ -91,7 +92,6 @@ class DigitallySpeakingIE(InfoExtractor):
|
|||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'format_note': 'slide deck video',
|
'format_note': 'slide deck video',
|
||||||
'quality': -2,
|
'quality': -2,
|
||||||
'preference': -2,
|
|
||||||
'format_id': 'slides',
|
'format_id': 'slides',
|
||||||
'acodec': 'none',
|
'acodec': 'none',
|
||||||
})
|
})
|
||||||
@@ -102,7 +102,6 @@ class DigitallySpeakingIE(InfoExtractor):
|
|||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'format_note': 'speaker video',
|
'format_note': 'speaker video',
|
||||||
'quality': -1,
|
'quality': -1,
|
||||||
'preference': -1,
|
|
||||||
'format_id': 'speaker',
|
'format_id': 'speaker',
|
||||||
})
|
})
|
||||||
return formats
|
return formats
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
@@ -10,11 +11,13 @@ from ..utils import (
|
|||||||
ExtractorError,
|
ExtractorError,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
strip_or_none,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DPlayIE(InfoExtractor):
|
class DPlayIE(InfoExtractor):
|
||||||
|
_PATH_REGEX = r'/(?P<id>[^/]+/[^/?#]+)'
|
||||||
_VALID_URL = r'''(?x)https?://
|
_VALID_URL = r'''(?x)https?://
|
||||||
(?P<domain>
|
(?P<domain>
|
||||||
(?:www\.)?(?P<host>d
|
(?:www\.)?(?P<host>d
|
||||||
@@ -24,7 +27,7 @@ class DPlayIE(InfoExtractor):
|
|||||||
)
|
)
|
||||||
)|
|
)|
|
||||||
(?P<subdomain_country>es|it)\.dplay\.com
|
(?P<subdomain_country>es|it)\.dplay\.com
|
||||||
)/[^/]+/(?P<id>[^/]+/[^/?#]+)'''
|
)/[^/]+''' + _PATH_REGEX
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# non geo restricted, via secure api, unsigned download hls URL
|
# non geo restricted, via secure api, unsigned download hls URL
|
||||||
@@ -151,21 +154,47 @@ class DPlayIE(InfoExtractor):
|
|||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
def _process_errors(self, e, geo_countries):
|
||||||
|
info = self._parse_json(e.cause.read().decode('utf-8'), None)
|
||||||
|
error = info['errors'][0]
|
||||||
|
error_code = error.get('code')
|
||||||
|
if error_code == 'access.denied.geoblocked':
|
||||||
|
self.raise_geo_restricted(countries=geo_countries)
|
||||||
|
elif error_code in ('access.denied.missingpackage', 'invalid.token'):
|
||||||
|
raise ExtractorError(
|
||||||
|
'This video is only available for registered users. You may want to use --cookies.', expected=True)
|
||||||
|
raise ExtractorError(info['errors'][0]['detail'], expected=True)
|
||||||
|
|
||||||
|
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
||||||
|
headers['Authorization'] = 'Bearer ' + self._download_json(
|
||||||
|
disco_base + 'token', display_id, 'Downloading token',
|
||||||
|
query={
|
||||||
|
'realm': realm,
|
||||||
|
})['data']['attributes']['token']
|
||||||
|
|
||||||
|
def _download_video_playback_info(self, disco_base, video_id, headers):
|
||||||
|
streaming = self._download_json(
|
||||||
|
disco_base + 'playback/videoPlaybackInfo/' + video_id,
|
||||||
|
video_id, headers=headers)['data']['attributes']['streaming']
|
||||||
|
streaming_list = []
|
||||||
|
for format_id, format_dict in streaming.items():
|
||||||
|
streaming_list.append({
|
||||||
|
'type': format_id,
|
||||||
|
'url': format_dict.get('url'),
|
||||||
|
})
|
||||||
|
return streaming_list
|
||||||
|
|
||||||
def _get_disco_api_info(self, url, display_id, disco_host, realm, country):
|
def _get_disco_api_info(self, url, display_id, disco_host, realm, country):
|
||||||
geo_countries = [country.upper()]
|
geo_countries = [country.upper()]
|
||||||
self._initialize_geo_bypass({
|
self._initialize_geo_bypass({
|
||||||
'countries': geo_countries,
|
'countries': geo_countries,
|
||||||
})
|
})
|
||||||
disco_base = 'https://%s/' % disco_host
|
disco_base = 'https://%s/' % disco_host
|
||||||
token = self._download_json(
|
|
||||||
disco_base + 'token', display_id, 'Downloading token',
|
|
||||||
query={
|
|
||||||
'realm': realm,
|
|
||||||
})['data']['attributes']['token']
|
|
||||||
headers = {
|
headers = {
|
||||||
'Referer': url,
|
'Referer': url,
|
||||||
'Authorization': 'Bearer ' + token,
|
|
||||||
}
|
}
|
||||||
|
self._update_disco_api_headers(headers, disco_base, display_id, realm)
|
||||||
|
try:
|
||||||
video = self._download_json(
|
video = self._download_json(
|
||||||
disco_base + 'content/videos/' + display_id, display_id,
|
disco_base + 'content/videos/' + display_id, display_id,
|
||||||
headers=headers, query={
|
headers=headers, query={
|
||||||
@@ -176,31 +205,28 @@ class DPlayIE(InfoExtractor):
|
|||||||
'fields[video]': 'description,episodeNumber,name,publishStart,seasonNumber,videoDuration',
|
'fields[video]': 'description,episodeNumber,name,publishStart,seasonNumber,videoDuration',
|
||||||
'include': 'images,primaryChannel,show,tags'
|
'include': 'images,primaryChannel,show,tags'
|
||||||
})
|
})
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
|
||||||
|
self._process_errors(e, geo_countries)
|
||||||
|
raise
|
||||||
video_id = video['data']['id']
|
video_id = video['data']['id']
|
||||||
info = video['data']['attributes']
|
info = video['data']['attributes']
|
||||||
title = info['name'].strip()
|
title = info['name'].strip()
|
||||||
formats = []
|
formats = []
|
||||||
try:
|
try:
|
||||||
streaming = self._download_json(
|
streaming = self._download_video_playback_info(
|
||||||
disco_base + 'playback/videoPlaybackInfo/' + video_id,
|
disco_base, video_id, headers)
|
||||||
display_id, headers=headers)['data']['attributes']['streaming']
|
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
|
||||||
info = self._parse_json(e.cause.read().decode('utf-8'), display_id)
|
self._process_errors(e, geo_countries)
|
||||||
error = info['errors'][0]
|
|
||||||
error_code = error.get('code')
|
|
||||||
if error_code == 'access.denied.geoblocked':
|
|
||||||
self.raise_geo_restricted(countries=geo_countries)
|
|
||||||
elif error_code == 'access.denied.missingpackage':
|
|
||||||
self.raise_login_required()
|
|
||||||
raise ExtractorError(info['errors'][0]['detail'], expected=True)
|
|
||||||
raise
|
raise
|
||||||
for format_id, format_dict in streaming.items():
|
for format_dict in streaming:
|
||||||
if not isinstance(format_dict, dict):
|
if not isinstance(format_dict, dict):
|
||||||
continue
|
continue
|
||||||
format_url = format_dict.get('url')
|
format_url = format_dict.get('url')
|
||||||
if not format_url:
|
if not format_url:
|
||||||
continue
|
continue
|
||||||
|
format_id = format_dict.get('type')
|
||||||
ext = determine_ext(format_url)
|
ext = determine_ext(format_url)
|
||||||
if format_id == 'dash' or ext == 'mpd':
|
if format_id == 'dash' or ext == 'mpd':
|
||||||
formats.extend(self._extract_mpd_formats(
|
formats.extend(self._extract_mpd_formats(
|
||||||
@@ -248,7 +274,7 @@ class DPlayIE(InfoExtractor):
|
|||||||
'id': video_id,
|
'id': video_id,
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': info.get('description'),
|
'description': strip_or_none(info.get('description')),
|
||||||
'duration': float_or_none(info.get('videoDuration'), 1000),
|
'duration': float_or_none(info.get('videoDuration'), 1000),
|
||||||
'timestamp': unified_timestamp(info.get('publishStart')),
|
'timestamp': unified_timestamp(info.get('publishStart')),
|
||||||
'series': series,
|
'series': series,
|
||||||
@@ -268,3 +294,75 @@ class DPlayIE(InfoExtractor):
|
|||||||
host = 'disco-api.' + domain if domain[0] == 'd' else 'eu2-prod.disco-api.com'
|
host = 'disco-api.' + domain if domain[0] == 'd' else 'eu2-prod.disco-api.com'
|
||||||
return self._get_disco_api_info(
|
return self._get_disco_api_info(
|
||||||
url, display_id, host, 'dplay' + country, country)
|
url, display_id, host, 'dplay' + country, country)
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryPlusIE(DPlayIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.com/video' + DPlayIE._PATH_REGEX
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.discoveryplus.com/video/property-brothers-forever-home/food-and-family',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1140794',
|
||||||
|
'display_id': 'property-brothers-forever-home/food-and-family',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Food and Family',
|
||||||
|
'description': 'The brothers help a Richmond family expand their single-level home.',
|
||||||
|
'duration': 2583.113,
|
||||||
|
'timestamp': 1609304400,
|
||||||
|
'upload_date': '20201230',
|
||||||
|
'creator': 'HGTV',
|
||||||
|
'series': 'Property Brothers: Forever Home',
|
||||||
|
'season_number': 1,
|
||||||
|
'episode_number': 1,
|
||||||
|
},
|
||||||
|
'skip': 'Available for Premium users',
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
||||||
|
headers['x-disco-client'] = 'WEB:UNKNOWN:dplus_us:15.0.0'
|
||||||
|
|
||||||
|
def _download_video_playback_info(self, disco_base, video_id, headers):
|
||||||
|
return self._download_json(
|
||||||
|
disco_base + 'playback/v3/videoPlaybackInfo',
|
||||||
|
video_id, headers=headers, data=json.dumps({
|
||||||
|
'deviceInfo': {
|
||||||
|
'adBlocker': False,
|
||||||
|
},
|
||||||
|
'videoId': video_id,
|
||||||
|
'wisteriaProperties': {
|
||||||
|
'platform': 'desktop',
|
||||||
|
},
|
||||||
|
}).encode('utf-8'))['data']['attributes']['streaming']
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
return self._get_disco_api_info(
|
||||||
|
url, display_id, 'us1-prod-direct.discoveryplus.com', 'go', 'us')
|
||||||
|
|
||||||
|
|
||||||
|
class HGTVDeIE(DPlayIE):
|
||||||
|
_VALID_URL = r'https?://de\.hgtv\.com/sendungen' + DPlayIE._PATH_REGEX
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://de.hgtv.com/sendungen/tiny-house-klein-aber-oho/wer-braucht-schon-eine-toilette/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '151205',
|
||||||
|
'display_id': 'tiny-house-klein-aber-oho/wer-braucht-schon-eine-toilette',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Wer braucht schon eine Toilette',
|
||||||
|
'description': 'md5:05b40a27e7aed2c9172de34d459134e2',
|
||||||
|
'duration': 1177.024,
|
||||||
|
'timestamp': 1595705400,
|
||||||
|
'upload_date': '20200725',
|
||||||
|
'creator': 'HGTV',
|
||||||
|
'series': 'Tiny House - klein, aber oho',
|
||||||
|
'season_number': 3,
|
||||||
|
'episode_number': 3,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'format': 'bestvideo',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
return self._get_disco_api_info(
|
||||||
|
url, display_id, 'eu1-prod.disco-api.com', 'hgtv', 'de')
|
||||||
|
|||||||
193
youtube_dlc/extractor/dreisat.py
Normal file
193
youtube_dlc/extractor/dreisat.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
unified_strdate,
|
||||||
|
xpath_text,
|
||||||
|
determine_ext,
|
||||||
|
float_or_none,
|
||||||
|
ExtractorError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DreiSatIE(InfoExtractor):
|
||||||
|
IE_NAME = '3sat'
|
||||||
|
_GEO_COUNTRIES = ['DE']
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?3sat\.de/mediathek/(?:(?:index|mediathek)\.php)?\?(?:(?:mode|display)=[^&]+&)*obj=(?P<id>[0-9]+)'
|
||||||
|
_TESTS = [
|
||||||
|
{
|
||||||
|
'url': 'http://www.3sat.de/mediathek/index.php?mode=play&obj=45918',
|
||||||
|
'md5': 'be37228896d30a88f315b638900a026e',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '45918',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Waidmannsheil',
|
||||||
|
'description': 'md5:cce00ca1d70e21425e72c86a98a56817',
|
||||||
|
'uploader': 'SCHWEIZWEIT',
|
||||||
|
'uploader_id': '100000210',
|
||||||
|
'upload_date': '20140913'
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True, # m3u8 downloads
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://www.3sat.de/mediathek/mediathek.php?mode=play&obj=51066',
|
||||||
|
'only_matching': True,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
def _parse_smil_formats(self, smil, smil_url, video_id, namespace=None, f4m_params=None, transform_rtmp_url=None):
|
||||||
|
param_groups = {}
|
||||||
|
for param_group in smil.findall(self._xpath_ns('./head/paramGroup', namespace)):
|
||||||
|
group_id = param_group.get(self._xpath_ns(
|
||||||
|
'id', 'http://www.w3.org/XML/1998/namespace'))
|
||||||
|
params = {}
|
||||||
|
for param in param_group:
|
||||||
|
params[param.get('name')] = param.get('value')
|
||||||
|
param_groups[group_id] = params
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for video in smil.findall(self._xpath_ns('.//video', namespace)):
|
||||||
|
src = video.get('src')
|
||||||
|
if not src:
|
||||||
|
continue
|
||||||
|
bitrate = int_or_none(self._search_regex(r'_(\d+)k', src, 'bitrate', None)) or float_or_none(video.get('system-bitrate') or video.get('systemBitrate'), 1000)
|
||||||
|
group_id = video.get('paramGroup')
|
||||||
|
param_group = param_groups[group_id]
|
||||||
|
for proto in param_group['protocols'].split(','):
|
||||||
|
formats.append({
|
||||||
|
'url': '%s://%s' % (proto, param_group['host']),
|
||||||
|
'app': param_group['app'],
|
||||||
|
'play_path': src,
|
||||||
|
'ext': 'flv',
|
||||||
|
'format_id': '%s-%d' % (proto, bitrate),
|
||||||
|
'tbr': bitrate,
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
return formats
|
||||||
|
|
||||||
|
def extract_from_xml_url(self, video_id, xml_url):
|
||||||
|
doc = self._download_xml(
|
||||||
|
xml_url, video_id,
|
||||||
|
note='Downloading video info',
|
||||||
|
errnote='Failed to download video info')
|
||||||
|
|
||||||
|
status_code = xpath_text(doc, './status/statuscode')
|
||||||
|
if status_code and status_code != 'ok':
|
||||||
|
if status_code == 'notVisibleAnymore':
|
||||||
|
message = 'Video %s is not available' % video_id
|
||||||
|
else:
|
||||||
|
message = '%s returned error: %s' % (self.IE_NAME, status_code)
|
||||||
|
raise ExtractorError(message, expected=True)
|
||||||
|
|
||||||
|
title = xpath_text(doc, './/information/title', 'title', True)
|
||||||
|
|
||||||
|
urls = []
|
||||||
|
formats = []
|
||||||
|
for fnode in doc.findall('.//formitaeten/formitaet'):
|
||||||
|
video_url = xpath_text(fnode, 'url')
|
||||||
|
if not video_url or video_url in urls:
|
||||||
|
continue
|
||||||
|
urls.append(video_url)
|
||||||
|
|
||||||
|
is_available = 'http://www.metafilegenerator' not in video_url
|
||||||
|
geoloced = 'static_geoloced_online' in video_url
|
||||||
|
if not is_available or geoloced:
|
||||||
|
continue
|
||||||
|
|
||||||
|
format_id = fnode.attrib['basetype']
|
||||||
|
format_m = re.match(r'''(?x)
|
||||||
|
(?P<vcodec>[^_]+)_(?P<acodec>[^_]+)_(?P<container>[^_]+)_
|
||||||
|
(?P<proto>[^_]+)_(?P<index>[^_]+)_(?P<indexproto>[^_]+)
|
||||||
|
''', format_id)
|
||||||
|
|
||||||
|
ext = determine_ext(video_url, None) or format_m.group('container')
|
||||||
|
|
||||||
|
if ext == 'meta':
|
||||||
|
continue
|
||||||
|
elif ext == 'smil':
|
||||||
|
formats.extend(self._extract_smil_formats(
|
||||||
|
video_url, video_id, fatal=False))
|
||||||
|
elif ext == 'm3u8':
|
||||||
|
# the certificates are misconfigured (see
|
||||||
|
# https://github.com/ytdl-org/youtube-dl/issues/8665)
|
||||||
|
if video_url.startswith('https://'):
|
||||||
|
continue
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
video_url, video_id, 'mp4', 'm3u8_native',
|
||||||
|
m3u8_id=format_id, fatal=False))
|
||||||
|
elif ext == 'f4m':
|
||||||
|
formats.extend(self._extract_f4m_formats(
|
||||||
|
video_url, video_id, f4m_id=format_id, fatal=False))
|
||||||
|
else:
|
||||||
|
quality = xpath_text(fnode, './quality')
|
||||||
|
if quality:
|
||||||
|
format_id += '-' + quality
|
||||||
|
|
||||||
|
abr = int_or_none(xpath_text(fnode, './audioBitrate'), 1000)
|
||||||
|
vbr = int_or_none(xpath_text(fnode, './videoBitrate'), 1000)
|
||||||
|
|
||||||
|
tbr = int_or_none(self._search_regex(
|
||||||
|
r'_(\d+)k', video_url, 'bitrate', None))
|
||||||
|
if tbr and vbr and not abr:
|
||||||
|
abr = tbr - vbr
|
||||||
|
|
||||||
|
formats.append({
|
||||||
|
'format_id': format_id,
|
||||||
|
'url': video_url,
|
||||||
|
'ext': ext,
|
||||||
|
'acodec': format_m.group('acodec'),
|
||||||
|
'vcodec': format_m.group('vcodec'),
|
||||||
|
'abr': abr,
|
||||||
|
'vbr': vbr,
|
||||||
|
'tbr': tbr,
|
||||||
|
'width': int_or_none(xpath_text(fnode, './width')),
|
||||||
|
'height': int_or_none(xpath_text(fnode, './height')),
|
||||||
|
'filesize': int_or_none(xpath_text(fnode, './filesize')),
|
||||||
|
'protocol': format_m.group('proto').lower(),
|
||||||
|
})
|
||||||
|
|
||||||
|
geolocation = xpath_text(doc, './/details/geolocation')
|
||||||
|
if not formats and geolocation and geolocation != 'none':
|
||||||
|
self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
thumbnails = []
|
||||||
|
for node in doc.findall('.//teaserimages/teaserimage'):
|
||||||
|
thumbnail_url = node.text
|
||||||
|
if not thumbnail_url:
|
||||||
|
continue
|
||||||
|
thumbnail = {
|
||||||
|
'url': thumbnail_url,
|
||||||
|
}
|
||||||
|
thumbnail_key = node.get('key')
|
||||||
|
if thumbnail_key:
|
||||||
|
m = re.match('^([0-9]+)x([0-9]+)$', thumbnail_key)
|
||||||
|
if m:
|
||||||
|
thumbnail['width'] = int(m.group(1))
|
||||||
|
thumbnail['height'] = int(m.group(2))
|
||||||
|
thumbnails.append(thumbnail)
|
||||||
|
|
||||||
|
upload_date = unified_strdate(xpath_text(doc, './/details/airtime'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': xpath_text(doc, './/information/detail'),
|
||||||
|
'duration': int_or_none(xpath_text(doc, './/details/lengthSec')),
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
'uploader': xpath_text(doc, './/details/originChannelTitle'),
|
||||||
|
'uploader_id': xpath_text(doc, './/details/originChannelId'),
|
||||||
|
'upload_date': upload_date,
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
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)
|
||||||
@@ -242,7 +242,7 @@ class DRTVIE(InfoExtractor):
|
|||||||
elif target == 'HLS':
|
elif target == 'HLS':
|
||||||
formats.extend(self._extract_m3u8_formats(
|
formats.extend(self._extract_m3u8_formats(
|
||||||
uri, video_id, 'mp4', entry_protocol='m3u8_native',
|
uri, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
preference=preference, m3u8_id=format_id,
|
quality=preference, m3u8_id=format_id,
|
||||||
fatal=False))
|
fatal=False))
|
||||||
else:
|
else:
|
||||||
bitrate = link.get('Bitrate')
|
bitrate = link.get('Bitrate')
|
||||||
@@ -254,7 +254,7 @@ class DRTVIE(InfoExtractor):
|
|||||||
'tbr': int_or_none(bitrate),
|
'tbr': int_or_none(bitrate),
|
||||||
'ext': link.get('FileFormat'),
|
'ext': link.get('FileFormat'),
|
||||||
'vcodec': 'none' if kind == 'AudioResource' else None,
|
'vcodec': 'none' if kind == 'AudioResource' else None,
|
||||||
'preference': preference,
|
'quality': preference,
|
||||||
})
|
})
|
||||||
subtitles_list = asset.get('SubtitlesList') or asset.get('Subtitleslist')
|
subtitles_list = asset.get('SubtitlesList') or asset.get('Subtitleslist')
|
||||||
if isinstance(subtitles_list, list):
|
if isinstance(subtitles_list, list):
|
||||||
|
|||||||
@@ -12,7 +12,14 @@ from ..utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class EggheadCourseIE(InfoExtractor):
|
class EggheadBaseIE(InfoExtractor):
|
||||||
|
def _call_api(self, path, video_id, resource, fatal=True):
|
||||||
|
return self._download_json(
|
||||||
|
'https://app.egghead.io/api/v1/' + path,
|
||||||
|
video_id, 'Downloading %s JSON' % resource, fatal=fatal)
|
||||||
|
|
||||||
|
|
||||||
|
class EggheadCourseIE(EggheadBaseIE):
|
||||||
IE_DESC = 'egghead.io course'
|
IE_DESC = 'egghead.io course'
|
||||||
IE_NAME = 'egghead:course'
|
IE_NAME = 'egghead:course'
|
||||||
_VALID_URL = r'https://egghead\.io/courses/(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https://egghead\.io/courses/(?P<id>[^/?#&]+)'
|
||||||
@@ -28,10 +35,9 @@ class EggheadCourseIE(InfoExtractor):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
playlist_id = self._match_id(url)
|
playlist_id = self._match_id(url)
|
||||||
|
series_path = 'series/' + playlist_id
|
||||||
lessons = self._download_json(
|
lessons = self._call_api(
|
||||||
'https://egghead.io/api/v1/series/%s/lessons' % playlist_id,
|
series_path + '/lessons', playlist_id, 'course lessons')
|
||||||
playlist_id, 'Downloading course lessons JSON')
|
|
||||||
|
|
||||||
entries = []
|
entries = []
|
||||||
for lesson in lessons:
|
for lesson in lessons:
|
||||||
@@ -44,9 +50,8 @@ class EggheadCourseIE(InfoExtractor):
|
|||||||
entries.append(self.url_result(
|
entries.append(self.url_result(
|
||||||
lesson_url, ie=EggheadLessonIE.ie_key(), video_id=lesson_id))
|
lesson_url, ie=EggheadLessonIE.ie_key(), video_id=lesson_id))
|
||||||
|
|
||||||
course = self._download_json(
|
course = self._call_api(
|
||||||
'https://egghead.io/api/v1/series/%s' % playlist_id,
|
series_path, playlist_id, 'course', False) or {}
|
||||||
playlist_id, 'Downloading course JSON', fatal=False) or {}
|
|
||||||
|
|
||||||
playlist_id = course.get('id')
|
playlist_id = course.get('id')
|
||||||
if playlist_id:
|
if playlist_id:
|
||||||
@@ -57,7 +62,7 @@ class EggheadCourseIE(InfoExtractor):
|
|||||||
course.get('description'))
|
course.get('description'))
|
||||||
|
|
||||||
|
|
||||||
class EggheadLessonIE(InfoExtractor):
|
class EggheadLessonIE(EggheadBaseIE):
|
||||||
IE_DESC = 'egghead.io lesson'
|
IE_DESC = 'egghead.io lesson'
|
||||||
IE_NAME = 'egghead:lesson'
|
IE_NAME = 'egghead:lesson'
|
||||||
_VALID_URL = r'https://egghead\.io/(?:api/v1/)?lessons/(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https://egghead\.io/(?:api/v1/)?lessons/(?P<id>[^/?#&]+)'
|
||||||
@@ -74,7 +79,7 @@ class EggheadLessonIE(InfoExtractor):
|
|||||||
'upload_date': '20161209',
|
'upload_date': '20161209',
|
||||||
'duration': 304,
|
'duration': 304,
|
||||||
'view_count': 0,
|
'view_count': 0,
|
||||||
'tags': ['javascript', 'free'],
|
'tags': 'count:2',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
@@ -88,8 +93,8 @@ class EggheadLessonIE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
|
|
||||||
lesson = self._download_json(
|
lesson = self._call_api(
|
||||||
'https://egghead.io/api/v1/lessons/%s' % display_id, display_id)
|
'lessons/' + display_id, display_id, 'lesson')
|
||||||
|
|
||||||
lesson_id = compat_str(lesson['id'])
|
lesson_id = compat_str(lesson['id'])
|
||||||
title = lesson['title']
|
title = lesson['title']
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class ESPNIE(OnceIE):
|
|||||||
'tbr': int(mobj.group(3)),
|
'tbr': int(mobj.group(3)),
|
||||||
})
|
})
|
||||||
if source_id == 'mezzanine':
|
if source_id == 'mezzanine':
|
||||||
f['preference'] = 1
|
f['quality'] = 1
|
||||||
formats.append(f)
|
formats.append(f)
|
||||||
|
|
||||||
links = clip.get('links', {})
|
links = clip.get('links', {})
|
||||||
|
|||||||
@@ -90,6 +90,11 @@ from .atvat import ATVAtIE
|
|||||||
from .audimedia import AudiMediaIE
|
from .audimedia import AudiMediaIE
|
||||||
from .audioboom import AudioBoomIE
|
from .audioboom import AudioBoomIE
|
||||||
from .audiomack import AudiomackIE, AudiomackAlbumIE
|
from .audiomack import AudiomackIE, AudiomackAlbumIE
|
||||||
|
from .audius import (
|
||||||
|
AudiusIE,
|
||||||
|
AudiusTrackIE,
|
||||||
|
AudiusPlaylistIE
|
||||||
|
)
|
||||||
from .awaan import (
|
from .awaan import (
|
||||||
AWAANIE,
|
AWAANIE,
|
||||||
AWAANVideoIE,
|
AWAANVideoIE,
|
||||||
@@ -122,10 +127,12 @@ from .bigflix import BigflixIE
|
|||||||
from .bild import BildIE
|
from .bild import BildIE
|
||||||
from .bilibili import (
|
from .bilibili import (
|
||||||
BiliBiliIE,
|
BiliBiliIE,
|
||||||
|
BiliBiliSearchIE,
|
||||||
BiliBiliBangumiIE,
|
BiliBiliBangumiIE,
|
||||||
BilibiliAudioIE,
|
BilibiliAudioIE,
|
||||||
BilibiliAudioAlbumIE,
|
BilibiliAudioAlbumIE,
|
||||||
BiliBiliPlayerIE,
|
BiliBiliPlayerIE,
|
||||||
|
BilibiliChannelIE,
|
||||||
)
|
)
|
||||||
from .biobiochiletv import BioBioChileTVIE
|
from .biobiochiletv import BioBioChileTVIE
|
||||||
from .bitchute import (
|
from .bitchute import (
|
||||||
@@ -175,6 +182,7 @@ from .canvas import (
|
|||||||
CanvasIE,
|
CanvasIE,
|
||||||
CanvasEenIE,
|
CanvasEenIE,
|
||||||
VrtNUIE,
|
VrtNUIE,
|
||||||
|
DagelijkseKostIE,
|
||||||
)
|
)
|
||||||
from .carambatv import (
|
from .carambatv import (
|
||||||
CarambaTVIE,
|
CarambaTVIE,
|
||||||
@@ -302,7 +310,12 @@ from .douyutv import (
|
|||||||
DouyuShowIE,
|
DouyuShowIE,
|
||||||
DouyuTVIE,
|
DouyuTVIE,
|
||||||
)
|
)
|
||||||
from .dplay import DPlayIE
|
from .dplay import (
|
||||||
|
DPlayIE,
|
||||||
|
DiscoveryPlusIE,
|
||||||
|
HGTVDeIE,
|
||||||
|
)
|
||||||
|
from .dreisat import DreiSatIE
|
||||||
from .drbonanza import DRBonanzaIE
|
from .drbonanza import DRBonanzaIE
|
||||||
from .drtuber import DrTuberIE
|
from .drtuber import DrTuberIE
|
||||||
from .drtv import (
|
from .drtv import (
|
||||||
@@ -495,8 +508,8 @@ from .hungama import (
|
|||||||
from .hypem import HypemIE
|
from .hypem import HypemIE
|
||||||
from .ign import (
|
from .ign import (
|
||||||
IGNIE,
|
IGNIE,
|
||||||
OneUPIE,
|
IGNVideoIE,
|
||||||
PCMagIE,
|
IGNArticleIE,
|
||||||
)
|
)
|
||||||
from .iheart import (
|
from .iheart import (
|
||||||
IHeartRadioIE,
|
IHeartRadioIE,
|
||||||
@@ -1100,6 +1113,11 @@ from .shared import (
|
|||||||
VivoIE,
|
VivoIE,
|
||||||
)
|
)
|
||||||
from .showroomlive import ShowRoomLiveIE
|
from .showroomlive import ShowRoomLiveIE
|
||||||
|
from .simplecast import (
|
||||||
|
SimplecastIE,
|
||||||
|
SimplecastEpisodeIE,
|
||||||
|
SimplecastPodcastIE,
|
||||||
|
)
|
||||||
from .sina import SinaIE
|
from .sina import SinaIE
|
||||||
from .sixplay import SixPlayIE
|
from .sixplay import SixPlayIE
|
||||||
from .skyit import (
|
from .skyit import (
|
||||||
@@ -1158,11 +1176,6 @@ from .spike import (
|
|||||||
BellatorIE,
|
BellatorIE,
|
||||||
ParamountNetworkIE,
|
ParamountNetworkIE,
|
||||||
)
|
)
|
||||||
from .storyfire import (
|
|
||||||
StoryFireIE,
|
|
||||||
StoryFireUserIE,
|
|
||||||
StoryFireSeriesIE,
|
|
||||||
)
|
|
||||||
from .stitcher import StitcherIE
|
from .stitcher import StitcherIE
|
||||||
from .sport5 import Sport5IE
|
from .sport5 import Sport5IE
|
||||||
from .sportbox import SportBoxIE
|
from .sportbox import SportBoxIE
|
||||||
@@ -1186,6 +1199,11 @@ from .srgssr import (
|
|||||||
from .srmediathek import SRMediathekIE
|
from .srmediathek import SRMediathekIE
|
||||||
from .stanfordoc import StanfordOpenClassroomIE
|
from .stanfordoc import StanfordOpenClassroomIE
|
||||||
from .steam import SteamIE
|
from .steam import SteamIE
|
||||||
|
from .storyfire import (
|
||||||
|
StoryFireIE,
|
||||||
|
StoryFireUserIE,
|
||||||
|
StoryFireSeriesIE,
|
||||||
|
)
|
||||||
from .streamable import StreamableIE
|
from .streamable import StreamableIE
|
||||||
from .streamcloud import StreamcloudIE
|
from .streamcloud import StreamcloudIE
|
||||||
from .streamcz import StreamCZIE
|
from .streamcz import StreamCZIE
|
||||||
@@ -1301,6 +1319,7 @@ from .tv2 import (
|
|||||||
TV2IE,
|
TV2IE,
|
||||||
TV2ArticleIE,
|
TV2ArticleIE,
|
||||||
KatsomoIE,
|
KatsomoIE,
|
||||||
|
MTVUutisetArticleIE,
|
||||||
)
|
)
|
||||||
from .tv2dk import (
|
from .tv2dk import (
|
||||||
TV2DKIE,
|
TV2DKIE,
|
||||||
@@ -1441,7 +1460,6 @@ from .vidme import (
|
|||||||
VidmeUserIE,
|
VidmeUserIE,
|
||||||
VidmeUserLikesIE,
|
VidmeUserLikesIE,
|
||||||
)
|
)
|
||||||
from .vidzi import VidziIE
|
|
||||||
from .vier import VierIE, VierVideosIE
|
from .vier import VierIE, VierVideosIE
|
||||||
from .viewlift import (
|
from .viewlift import (
|
||||||
ViewLiftIE,
|
ViewLiftIE,
|
||||||
@@ -1501,6 +1519,7 @@ from .vrv import (
|
|||||||
VRVSeriesIE,
|
VRVSeriesIE,
|
||||||
)
|
)
|
||||||
from .vshare import VShareIE
|
from .vshare import VShareIE
|
||||||
|
from .vtm import VTMIE
|
||||||
from .medialaan import MedialaanIE
|
from .medialaan import MedialaanIE
|
||||||
from .vube import VubeIE
|
from .vube import VubeIE
|
||||||
from .vuclip import VuClipIE
|
from .vuclip import VuClipIE
|
||||||
@@ -1644,6 +1663,7 @@ from .zattoo import (
|
|||||||
ZattooLiveIE,
|
ZattooLiveIE,
|
||||||
)
|
)
|
||||||
from .zdf import ZDFIE, ZDFChannelIE
|
from .zdf import ZDFIE, ZDFChannelIE
|
||||||
|
from .zhihu import ZhihuIE
|
||||||
from .zingmp3 import ZingMp3IE
|
from .zingmp3 import ZingMp3IE
|
||||||
from .zoom import ZoomIE
|
from .zoom import ZoomIE
|
||||||
from .zype import ZypeIE
|
from .zype import ZypeIE
|
||||||
|
|||||||
@@ -619,7 +619,7 @@ class FacebookIE(InfoExtractor):
|
|||||||
formats.append({
|
formats.append({
|
||||||
'format_id': '%s_%s_%s' % (format_id, quality, src_type),
|
'format_id': '%s_%s_%s' % (format_id, quality, src_type),
|
||||||
'url': src,
|
'url': src,
|
||||||
'preference': preference,
|
'quality': preference,
|
||||||
})
|
})
|
||||||
extract_dash_manifest(f[0], formats)
|
extract_dash_manifest(f[0], formats)
|
||||||
subtitles_src = f[0].get('subtitles_src')
|
subtitles_src = f[0].get('subtitles_src')
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class FirstTVIE(InfoExtractor):
|
|||||||
'tbr': tbr,
|
'tbr': tbr,
|
||||||
'source_preference': quality(f.get('name')),
|
'source_preference': quality(f.get('name')),
|
||||||
# quality metadata of http formats may be incorrect
|
# quality metadata of http formats may be incorrect
|
||||||
'preference': -1,
|
'preference': -10,
|
||||||
})
|
})
|
||||||
# m3u8 URL format is reverse engineered from [1] (search for
|
# m3u8 URL format is reverse engineered from [1] (search for
|
||||||
# master.m3u8). dashEdges (that is currently balancer-vod.1tv.ru)
|
# master.m3u8). dashEdges (that is currently balancer-vod.1tv.ru)
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class FlickrIE(InfoExtractor):
|
|||||||
formats.append({
|
formats.append({
|
||||||
'format_id': stream_type,
|
'format_id': stream_type,
|
||||||
'url': stream['_content'],
|
'url': stream['_content'],
|
||||||
'preference': preference(stream_type),
|
'quality': preference(stream_type),
|
||||||
})
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,10 @@ from .kinja import KinjaEmbedIE
|
|||||||
from .gedi import GediEmbedsIE
|
from .gedi import GediEmbedsIE
|
||||||
from .rcs import RCSEmbedsIE
|
from .rcs import RCSEmbedsIE
|
||||||
from .bitchute import BitChuteIE
|
from .bitchute import BitChuteIE
|
||||||
|
from .rumble import RumbleEmbedIE
|
||||||
from .arcpublishing import ArcPublishingIE
|
from .arcpublishing import ArcPublishingIE
|
||||||
|
from .medialaan import MedialaanIE
|
||||||
|
from .simplecast import SimplecastIE
|
||||||
|
|
||||||
|
|
||||||
class GenericIE(InfoExtractor):
|
class GenericIE(InfoExtractor):
|
||||||
@@ -2224,6 +2227,29 @@ class GenericIE(InfoExtractor):
|
|||||||
'duration': 1581,
|
'duration': 1581,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
# MyChannels SDK embed
|
||||||
|
# https://www.24kitchen.nl/populair/deskundige-dit-waarom-sommigen-gevoelig-zijn-voor-voedselallergieen
|
||||||
|
'url': 'https://www.demorgen.be/nieuws/burgemeester-rotterdam-richt-zich-in-videoboodschap-tot-relschoppers-voelt-het-goed~b0bcfd741/',
|
||||||
|
'md5': '90c0699c37006ef18e198c032d81739c',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '194165',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Burgemeester Aboutaleb spreekt relschoppers toe',
|
||||||
|
'timestamp': 1611740340,
|
||||||
|
'upload_date': '20210127',
|
||||||
|
'duration': 159,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# Simplecast player embed
|
||||||
|
'url': 'https://www.bio.org/podcast',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'podcast',
|
||||||
|
'title': 'I AM BIO Podcast | BIO',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 52,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
def report_following_redirect(self, new_url):
|
def report_following_redirect(self, new_url):
|
||||||
@@ -2463,6 +2489,9 @@ class GenericIE(InfoExtractor):
|
|||||||
webpage = self._webpage_read_content(
|
webpage = self._webpage_read_content(
|
||||||
full_response, url, video_id, prefix=first_bytes)
|
full_response, url, video_id, prefix=first_bytes)
|
||||||
|
|
||||||
|
if '<title>DPG Media Privacy Gate</title>' in webpage:
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
self.report_extraction(video_id)
|
self.report_extraction(video_id)
|
||||||
|
|
||||||
# Is it an RSS feed, a SMIL file, an XSPF playlist or a MPD manifest?
|
# Is it an RSS feed, a SMIL file, an XSPF playlist or a MPD manifest?
|
||||||
@@ -2594,6 +2623,11 @@ class GenericIE(InfoExtractor):
|
|||||||
if arc_urls:
|
if arc_urls:
|
||||||
return self.playlist_from_matches(arc_urls, video_id, video_title, ie=ArcPublishingIE.ie_key())
|
return self.playlist_from_matches(arc_urls, video_id, video_title, ie=ArcPublishingIE.ie_key())
|
||||||
|
|
||||||
|
mychannels_urls = MedialaanIE._extract_urls(webpage)
|
||||||
|
if mychannels_urls:
|
||||||
|
return self.playlist_from_matches(
|
||||||
|
mychannels_urls, video_id, video_title, ie=MedialaanIE.ie_key())
|
||||||
|
|
||||||
# Look for embedded rtl.nl player
|
# Look for embedded rtl.nl player
|
||||||
matches = re.findall(
|
matches = re.findall(
|
||||||
r'<iframe[^>]+?src="((?:https?:)?//(?:(?:www|static)\.)?rtl\.nl/(?:system/videoplayer/[^"]+(?:video_)?)?embed[^"]+)"',
|
r'<iframe[^>]+?src="((?:https?:)?//(?:(?:www|static)\.)?rtl\.nl/(?:system/videoplayer/[^"]+(?:video_)?)?embed[^"]+)"',
|
||||||
@@ -2770,6 +2804,12 @@ class GenericIE(InfoExtractor):
|
|||||||
return self.playlist_from_matches(
|
return self.playlist_from_matches(
|
||||||
matches, video_id, video_title, getter=unescapeHTML, ie='FunnyOrDie')
|
matches, video_id, video_title, getter=unescapeHTML, ie='FunnyOrDie')
|
||||||
|
|
||||||
|
# Look for Simplecast embeds
|
||||||
|
simplecast_urls = SimplecastIE._extract_urls(webpage)
|
||||||
|
if simplecast_urls:
|
||||||
|
return self.playlist_from_matches(
|
||||||
|
simplecast_urls, video_id, video_title)
|
||||||
|
|
||||||
# Look for BBC iPlayer embed
|
# Look for BBC iPlayer embed
|
||||||
matches = re.findall(r'setPlaylist\("(https?://www\.bbc\.co\.uk/iplayer/[^/]+/[\da-z]{8})"\)', webpage)
|
matches = re.findall(r'setPlaylist\("(https?://www\.bbc\.co\.uk/iplayer/[^/]+/[\da-z]{8})"\)', webpage)
|
||||||
if matches:
|
if matches:
|
||||||
@@ -3315,6 +3355,13 @@ class GenericIE(InfoExtractor):
|
|||||||
return self.playlist_from_matches(
|
return self.playlist_from_matches(
|
||||||
bitchute_urls, video_id, video_title, ie=BitChuteIE.ie_key())
|
bitchute_urls, video_id, video_title, ie=BitChuteIE.ie_key())
|
||||||
|
|
||||||
|
rumble_urls = RumbleEmbedIE._extract_urls(webpage)
|
||||||
|
if len(rumble_urls) == 1:
|
||||||
|
return self.url_result(rumble_urls[0], RumbleEmbedIE.ie_key())
|
||||||
|
if rumble_urls:
|
||||||
|
return self.playlist_from_matches(
|
||||||
|
rumble_urls, video_id, video_title, ie=RumbleEmbedIE.ie_key())
|
||||||
|
|
||||||
# Look for HTML5 media
|
# Look for HTML5 media
|
||||||
entries = self._parse_html5_media_entries(url, webpage, video_id, m3u8_id='hls')
|
entries = self._parse_html5_media_entries(url, webpage, video_id, m3u8_id='hls')
|
||||||
if entries:
|
if entries:
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class GloboIE(InfoExtractor):
|
|||||||
video = self._download_json(
|
video = self._download_json(
|
||||||
'http://api.globovideos.com/videos/%s/playlist' % video_id,
|
'http://api.globovideos.com/videos/%s/playlist' % video_id,
|
||||||
video_id)['videos'][0]
|
video_id)['videos'][0]
|
||||||
if video.get('encrypted') is True:
|
if not self._downloader.params.get('allow_unplayable_formats') and video.get('encrypted') is True:
|
||||||
raise ExtractorError('This video is DRM protected.', expected=True)
|
raise ExtractorError('This video is DRM protected.', expected=True)
|
||||||
|
|
||||||
title = video['title']
|
title = video['title']
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ class GoIE(AdobePassIE):
|
|||||||
if re.search(r'(?:/mp4/source/|_source\.mp4)', asset_url):
|
if re.search(r'(?:/mp4/source/|_source\.mp4)', asset_url):
|
||||||
f.update({
|
f.update({
|
||||||
'format_id': ('%s-' % format_id if format_id else '') + 'SOURCE',
|
'format_id': ('%s-' % format_id if format_id else '') + 'SOURCE',
|
||||||
'preference': 1,
|
'quality': 1,
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
mobj = re.search(r'/(\d+)x(\d+)/', asset_url)
|
mobj = re.search(r'/(\d+)x(\d+)/', asset_url)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from ..compat import compat_parse_qs
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
determine_ext,
|
determine_ext,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
get_element_by_class,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
lowercase_escape,
|
lowercase_escape,
|
||||||
try_get,
|
try_get,
|
||||||
@@ -237,7 +238,7 @@ class GoogleDriveIE(InfoExtractor):
|
|||||||
if confirmation_webpage:
|
if confirmation_webpage:
|
||||||
confirm = self._search_regex(
|
confirm = self._search_regex(
|
||||||
r'confirm=([^&"\']+)', confirmation_webpage,
|
r'confirm=([^&"\']+)', confirmation_webpage,
|
||||||
'confirmation code', fatal=False)
|
'confirmation code', default=None)
|
||||||
if confirm:
|
if confirm:
|
||||||
confirmed_source_url = update_url_query(source_url, {
|
confirmed_source_url = update_url_query(source_url, {
|
||||||
'confirm': confirm,
|
'confirm': confirm,
|
||||||
@@ -245,6 +246,11 @@ class GoogleDriveIE(InfoExtractor):
|
|||||||
urlh = request_source_file(confirmed_source_url, 'confirmed source')
|
urlh = request_source_file(confirmed_source_url, 'confirmed source')
|
||||||
if urlh and urlh.headers.get('Content-Disposition'):
|
if urlh and urlh.headers.get('Content-Disposition'):
|
||||||
add_source_format(urlh)
|
add_source_format(urlh)
|
||||||
|
else:
|
||||||
|
self.report_warning(
|
||||||
|
get_element_by_class('uc-error-subcaption', confirmation_webpage)
|
||||||
|
or get_element_by_class('uc-error-caption', confirmation_webpage)
|
||||||
|
or 'unable to extract confirmation code')
|
||||||
|
|
||||||
if not formats and reason:
|
if not formats and reason:
|
||||||
raise ExtractorError(reason, expected=True)
|
raise ExtractorError(reason, expected=True)
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class HearThisAtIE(InfoExtractor):
|
|||||||
'vcodec': 'none',
|
'vcodec': 'none',
|
||||||
'ext': ext,
|
'ext': ext,
|
||||||
'url': download_url,
|
'url': download_url,
|
||||||
'preference': 2, # Usually better quality
|
'quality': 2, # Usually better quality
|
||||||
})
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ class HotStarIE(HotStarBaseIE):
|
|||||||
|
|
||||||
title = video_data['title']
|
title = video_data['title']
|
||||||
|
|
||||||
if video_data.get('drmProtected'):
|
if not self._downloader.params.get('allow_unplayable_formats') and video_data.get('drmProtected'):
|
||||||
raise ExtractorError('This video is DRM protected.', expected=True)
|
raise ExtractorError('This video is DRM protected.', expected=True)
|
||||||
|
|
||||||
headers = {'Referer': url}
|
headers = {'Referer': url}
|
||||||
|
|||||||
@@ -3,28 +3,39 @@ from __future__ import unicode_literals
|
|||||||
import re
|
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 (
|
||||||
|
HEADRequest,
|
||||||
|
determine_ext,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
|
strip_or_none,
|
||||||
|
try_get,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class IGNIE(InfoExtractor):
|
class IGNBaseIE(InfoExtractor):
|
||||||
|
def _call_api(self, slug):
|
||||||
|
return self._download_json(
|
||||||
|
'http://apis.ign.com/{0}/v3/{0}s/slug/{1}'.format(self._PAGE_TYPE, slug), slug)
|
||||||
|
|
||||||
|
|
||||||
|
class IGNIE(IGNBaseIE):
|
||||||
"""
|
"""
|
||||||
Extractor for some of the IGN sites, like www.ign.com, es.ign.com de.ign.com.
|
Extractor for some of the IGN sites, like www.ign.com, es.ign.com de.ign.com.
|
||||||
Some videos of it.ign.com are also supported
|
Some videos of it.ign.com are also supported
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_VALID_URL = r'https?://.+?\.ign\.com/(?:[^/]+/)?(?P<type>videos|show_videos|articles|feature|(?:[^/]+/\d+/video))(/.+)?/(?P<name_or_id>.+)'
|
_VALID_URL = r'https?://(?:.+?\.ign|www\.pcmag)\.com/videos/(?:\d{4}/\d{2}/\d{2}/)?(?P<id>[^/?&#]+)'
|
||||||
IE_NAME = 'ign.com'
|
IE_NAME = 'ign.com'
|
||||||
|
_PAGE_TYPE = 'video'
|
||||||
|
|
||||||
_API_URL_TEMPLATE = 'http://apis.ign.com/video/v3/videos/%s'
|
_TESTS = [{
|
||||||
_EMBED_RE = r'<iframe[^>]+?["\']((?:https?:)?//.+?\.ign\.com.+?/embed.+?)["\']'
|
|
||||||
|
|
||||||
_TESTS = [
|
|
||||||
{
|
|
||||||
'url': 'http://www.ign.com/videos/2013/06/05/the-last-of-us-review',
|
'url': 'http://www.ign.com/videos/2013/06/05/the-last-of-us-review',
|
||||||
'md5': 'febda82c4bafecd2d44b6e1a18a595f8',
|
'md5': 'd2e1586d9987d40fad7867bf96a018ea',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '8f862beef863986b2785559b9e1aa599',
|
'id': '8f862beef863986b2785559b9e1aa599',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -32,13 +43,147 @@ class IGNIE(InfoExtractor):
|
|||||||
'description': 'md5:c8946d4260a4d43a00d5ae8ed998870c',
|
'description': 'md5:c8946d4260a4d43a00d5ae8ed998870c',
|
||||||
'timestamp': 1370440800,
|
'timestamp': 1370440800,
|
||||||
'upload_date': '20130605',
|
'upload_date': '20130605',
|
||||||
'uploader_id': 'cberidon@ign.com',
|
'tags': 'count:9',
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
{
|
'url': 'http://www.pcmag.com/videos/2015/01/06/010615-whats-new-now-is-gogo-snooping-on-your-data',
|
||||||
|
'md5': 'f1581a6fe8c5121be5b807684aeac3f6',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'ee10d774b508c9b8ec07e763b9125b91',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'What\'s New Now: Is GoGo Snooping on Your Data?',
|
||||||
|
'description': 'md5:817a20299de610bd56f13175386da6fa',
|
||||||
|
'timestamp': 1420571160,
|
||||||
|
'upload_date': '20150106',
|
||||||
|
'tags': 'count:4',
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.ign.com/videos/is-a-resident-evil-4-remake-on-the-way-ign-daily-fix',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
video = self._call_api(display_id)
|
||||||
|
video_id = video['videoId']
|
||||||
|
metadata = video['metadata']
|
||||||
|
title = metadata.get('longTitle') or metadata.get('title') or metadata['name']
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
refs = video.get('refs') or {}
|
||||||
|
|
||||||
|
m3u8_url = refs.get('m3uUrl')
|
||||||
|
if m3u8_url:
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
m3u8_url, video_id, 'mp4', 'm3u8_native',
|
||||||
|
m3u8_id='hls', fatal=False))
|
||||||
|
|
||||||
|
f4m_url = refs.get('f4mUrl')
|
||||||
|
if f4m_url:
|
||||||
|
formats.extend(self._extract_f4m_formats(
|
||||||
|
f4m_url, video_id, f4m_id='hds', fatal=False))
|
||||||
|
|
||||||
|
for asset in (video.get('assets') or []):
|
||||||
|
asset_url = asset.get('url')
|
||||||
|
if not asset_url:
|
||||||
|
continue
|
||||||
|
formats.append({
|
||||||
|
'url': asset_url,
|
||||||
|
'tbr': int_or_none(asset.get('bitrate'), 1000),
|
||||||
|
'fps': int_or_none(asset.get('frame_rate')),
|
||||||
|
'height': int_or_none(asset.get('height')),
|
||||||
|
'width': int_or_none(asset.get('width')),
|
||||||
|
})
|
||||||
|
|
||||||
|
mezzanine_url = try_get(video, lambda x: x['system']['mezzanineUrl'])
|
||||||
|
if mezzanine_url:
|
||||||
|
formats.append({
|
||||||
|
'ext': determine_ext(mezzanine_url, 'mp4'),
|
||||||
|
'format_id': 'mezzanine',
|
||||||
|
'quality': 1,
|
||||||
|
'url': mezzanine_url,
|
||||||
|
})
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
thumbnails = []
|
||||||
|
for thumbnail in (video.get('thumbnails') or []):
|
||||||
|
thumbnail_url = thumbnail.get('url')
|
||||||
|
if not thumbnail_url:
|
||||||
|
continue
|
||||||
|
thumbnails.append({
|
||||||
|
'url': thumbnail_url,
|
||||||
|
})
|
||||||
|
|
||||||
|
tags = []
|
||||||
|
for tag in (video.get('tags') or []):
|
||||||
|
display_name = tag.get('displayName')
|
||||||
|
if not display_name:
|
||||||
|
continue
|
||||||
|
tags.append(display_name)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': strip_or_none(metadata.get('description')),
|
||||||
|
'timestamp': parse_iso8601(metadata.get('publishDate')),
|
||||||
|
'duration': int_or_none(metadata.get('duration')),
|
||||||
|
'display_id': display_id,
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
'formats': formats,
|
||||||
|
'tags': tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class IGNVideoIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://.+?\.ign\.com/(?:[a-z]{2}/)?[^/]+/(?P<id>\d+)/(?:video|trailer)/'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://me.ign.com/en/videos/112203/video/how-hitman-aims-to-be-different-than-every-other-s',
|
||||||
|
'md5': 'dd9aca7ed2657c4e118d8b261e5e9de1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'e9be7ea899a9bbfc0674accc22a36cc8',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'How Hitman Aims to Be Different Than Every Other Stealth Game - NYCC 2015',
|
||||||
|
'description': 'Taking out assassination targets in Hitman has never been more stylish.',
|
||||||
|
'timestamp': 1444665600,
|
||||||
|
'upload_date': '20151012',
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'http://me.ign.com/ar/angry-birds-2/106533/video/lrd-ldyy-lwl-lfylm-angry-birds',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# Youtube embed
|
||||||
|
'url': 'https://me.ign.com/ar/ratchet-clank-rift-apart/144327/trailer/embed',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# Twitter embed
|
||||||
|
'url': 'http://adria.ign.com/sherlock-season-4/9687/trailer/embed',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# Vimeo embed
|
||||||
|
'url': 'https://kr.ign.com/bic-2018/3307/trailer/embed',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
req = HEADRequest(url.rsplit('/', 1)[0] + '/embed')
|
||||||
|
url = self._request_webpage(req, video_id).geturl()
|
||||||
|
ign_url = compat_parse_qs(
|
||||||
|
compat_urllib_parse_urlparse(url).query).get('url', [None])[0]
|
||||||
|
if ign_url:
|
||||||
|
return self.url_result(ign_url, IGNIE.ie_key())
|
||||||
|
return self.url_result(url)
|
||||||
|
|
||||||
|
|
||||||
|
class IGNArticleIE(IGNBaseIE):
|
||||||
|
_VALID_URL = r'https?://.+?\.ign\.com/(?:articles(?:/\d{4}/\d{2}/\d{2})?|(?:[a-z]{2}/)?feature/\d+)/(?P<id>[^/?&#]+)'
|
||||||
|
_PAGE_TYPE = 'article'
|
||||||
|
_TESTS = [{
|
||||||
'url': 'http://me.ign.com/en/feature/15775/100-little-things-in-gta-5-that-will-blow-your-mind',
|
'url': 'http://me.ign.com/en/feature/15775/100-little-things-in-gta-5-that-will-blow-your-mind',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '100-little-things-in-gta-5-that-will-blow-your-mind',
|
'id': '524497489e4e8ff5848ece34',
|
||||||
|
'title': '100 Little Things in GTA 5 That Will Blow Your Mind',
|
||||||
},
|
},
|
||||||
'playlist': [
|
'playlist': [
|
||||||
{
|
{
|
||||||
@@ -49,7 +194,6 @@ class IGNIE(InfoExtractor):
|
|||||||
'description': 'Rockstar drops the mic on this generation of games. Watch our review of the masterly Grand Theft Auto V.',
|
'description': 'Rockstar drops the mic on this generation of games. Watch our review of the masterly Grand Theft Auto V.',
|
||||||
'timestamp': 1379339880,
|
'timestamp': 1379339880,
|
||||||
'upload_date': '20130916',
|
'upload_date': '20130916',
|
||||||
'uploader_id': 'danieljkrupa@gmail.com',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -60,173 +204,54 @@ class IGNIE(InfoExtractor):
|
|||||||
'description': 'The twisted beauty of GTA 5 in stunning slow motion.',
|
'description': 'The twisted beauty of GTA 5 in stunning slow motion.',
|
||||||
'timestamp': 1386878820,
|
'timestamp': 1386878820,
|
||||||
'upload_date': '20131212',
|
'upload_date': '20131212',
|
||||||
'uploader_id': 'togilvie@ign.com',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'params': {
|
'params': {
|
||||||
|
'playlist_items': '2-3',
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
'url': 'http://www.ign.com/articles/2014/08/15/rewind-theater-wild-trailer-gamescom-2014?watch',
|
'url': 'http://www.ign.com/articles/2014/08/15/rewind-theater-wild-trailer-gamescom-2014?watch',
|
||||||
'md5': '618fedb9c901fd086f6f093564ef8558',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '078fdd005f6d3c02f63d795faa1b984f',
|
'id': '53ee806780a81ec46e0790f8',
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Rewind Theater - Wild Trailer Gamescom 2014',
|
'title': 'Rewind Theater - Wild Trailer Gamescom 2014',
|
||||||
'description': 'Brian and Jared explore Michel Ancel\'s captivating new preview.',
|
|
||||||
'timestamp': 1408047180,
|
|
||||||
'upload_date': '20140814',
|
|
||||||
'uploader_id': 'jamesduggan1990@gmail.com',
|
|
||||||
},
|
},
|
||||||
},
|
'playlist_count': 2,
|
||||||
{
|
}, {
|
||||||
'url': 'http://me.ign.com/en/videos/112203/video/how-hitman-aims-to-be-different-than-every-other-s',
|
|
||||||
'only_matching': True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'url': 'http://me.ign.com/ar/angry-birds-2/106533/video/lrd-ldyy-lwl-lfylm-angry-birds',
|
|
||||||
'only_matching': True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
# videoId pattern
|
# videoId pattern
|
||||||
'url': 'http://www.ign.com/articles/2017/06/08/new-ducktales-short-donalds-birthday-doesnt-go-as-planned',
|
'url': 'http://www.ign.com/articles/2017/06/08/new-ducktales-short-donalds-birthday-doesnt-go-as-planned',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
def _find_video_id(self, webpage):
|
|
||||||
res_id = [
|
|
||||||
r'"video_id"\s*:\s*"(.*?)"',
|
|
||||||
r'class="hero-poster[^"]*?"[^>]*id="(.+?)"',
|
|
||||||
r'data-video-id="(.+?)"',
|
|
||||||
r'<object id="vid_(.+?)"',
|
|
||||||
r'<meta name="og:image" content=".*/(.+?)-(.+?)/.+.jpg"',
|
|
||||||
r'videoId"\s*:\s*"(.+?)"',
|
|
||||||
r'videoId["\']\s*:\s*["\']([^"\']+?)["\']',
|
|
||||||
]
|
|
||||||
return self._search_regex(res_id, webpage, 'video id', default=None)
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
mobj = re.match(self._VALID_URL, url)
|
|
||||||
name_or_id = mobj.group('name_or_id')
|
|
||||||
page_type = mobj.group('type')
|
|
||||||
webpage = self._download_webpage(url, name_or_id)
|
|
||||||
if page_type != 'video':
|
|
||||||
multiple_urls = re.findall(
|
|
||||||
r'<param name="flashvars"[^>]*value="[^"]*?url=(https?://www\.ign\.com/videos/.*?)["&]',
|
|
||||||
webpage)
|
|
||||||
if multiple_urls:
|
|
||||||
entries = [self.url_result(u, ie='IGN') for u in multiple_urls]
|
|
||||||
return {
|
|
||||||
'_type': 'playlist',
|
|
||||||
'id': name_or_id,
|
|
||||||
'entries': entries,
|
|
||||||
}
|
|
||||||
|
|
||||||
video_id = self._find_video_id(webpage)
|
|
||||||
if not video_id:
|
|
||||||
return self.url_result(self._search_regex(
|
|
||||||
self._EMBED_RE, webpage, 'embed url'))
|
|
||||||
return self._get_video_info(video_id)
|
|
||||||
|
|
||||||
def _get_video_info(self, video_id):
|
|
||||||
api_data = self._download_json(
|
|
||||||
self._API_URL_TEMPLATE % video_id, video_id)
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
m3u8_url = api_data['refs'].get('m3uUrl')
|
|
||||||
if m3u8_url:
|
|
||||||
formats.extend(self._extract_m3u8_formats(
|
|
||||||
m3u8_url, video_id, 'mp4', 'm3u8_native',
|
|
||||||
m3u8_id='hls', fatal=False))
|
|
||||||
f4m_url = api_data['refs'].get('f4mUrl')
|
|
||||||
if f4m_url:
|
|
||||||
formats.extend(self._extract_f4m_formats(
|
|
||||||
f4m_url, video_id, f4m_id='hds', fatal=False))
|
|
||||||
for asset in api_data['assets']:
|
|
||||||
formats.append({
|
|
||||||
'url': asset['url'],
|
|
||||||
'tbr': asset.get('actual_bitrate_kbps'),
|
|
||||||
'fps': asset.get('frame_rate'),
|
|
||||||
'height': int_or_none(asset.get('height')),
|
|
||||||
'width': int_or_none(asset.get('width')),
|
|
||||||
})
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
thumbnails = [{
|
|
||||||
'url': thumbnail['url']
|
|
||||||
} for thumbnail in api_data.get('thumbnails', [])]
|
|
||||||
|
|
||||||
metadata = api_data['metadata']
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': api_data.get('videoId') or video_id,
|
|
||||||
'title': metadata.get('longTitle') or metadata.get('name') or metadata.get['title'],
|
|
||||||
'description': metadata.get('description'),
|
|
||||||
'timestamp': parse_iso8601(metadata.get('publishDate')),
|
|
||||||
'duration': int_or_none(metadata.get('duration')),
|
|
||||||
'display_id': metadata.get('slug') or video_id,
|
|
||||||
'uploader_id': metadata.get('creator'),
|
|
||||||
'thumbnails': thumbnails,
|
|
||||||
'formats': formats,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class OneUPIE(IGNIE):
|
|
||||||
_VALID_URL = r'https?://gamevideos\.1up\.com/(?P<type>video)/id/(?P<name_or_id>.+)\.html'
|
|
||||||
IE_NAME = '1up.com'
|
|
||||||
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'http://gamevideos.1up.com/video/id/34976.html',
|
|
||||||
'md5': 'c9cc69e07acb675c31a16719f909e347',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '34976',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Sniper Elite V2 - Trailer',
|
|
||||||
'description': 'md5:bf0516c5ee32a3217aa703e9b1bc7826',
|
|
||||||
'timestamp': 1313099220,
|
|
||||||
'upload_date': '20110811',
|
|
||||||
'uploader_id': 'IGN',
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
mobj = re.match(self._VALID_URL, url)
|
|
||||||
result = super(OneUPIE, self)._real_extract(url)
|
|
||||||
result['id'] = mobj.group('name_or_id')
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class PCMagIE(IGNIE):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?pcmag\.com/(?P<type>videos|article2)(/.+)?/(?P<name_or_id>.+)'
|
|
||||||
IE_NAME = 'pcmag'
|
|
||||||
|
|
||||||
_EMBED_RE = r'iframe\.setAttribute\("src",\s*__util.objToUrlString\("http://widgets\.ign\.com/video/embed/content\.html?[^"]*url=([^"]+)["&]'
|
|
||||||
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'http://www.pcmag.com/videos/2015/01/06/010615-whats-new-now-is-gogo-snooping-on-your-data',
|
|
||||||
'md5': '212d6154fd0361a2781075f1febbe9ad',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'ee10d774b508c9b8ec07e763b9125b91',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': '010615_What\'s New Now: Is GoGo Snooping on Your Data?',
|
|
||||||
'description': 'md5:a7071ae64d2f68cc821c729d4ded6bb3',
|
|
||||||
'timestamp': 1420571160,
|
|
||||||
'upload_date': '20150106',
|
|
||||||
'uploader_id': 'cozzipix@gmail.com',
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.pcmag.com/article2/0,2817,2470156,00.asp',
|
# Youtube embed
|
||||||
'md5': '94130c1ca07ba0adb6088350681f16c1',
|
'url': 'https://www.ign.com/articles/2021-mvp-named-in-puppy-bowl-xvii',
|
||||||
'info_dict': {
|
'only_matching': True,
|
||||||
'id': '042e560ba94823d43afcb12ddf7142ca',
|
}, {
|
||||||
'ext': 'mp4',
|
# IMDB embed
|
||||||
'title': 'HTC\'s Weird New Re Camera - What\'s New Now',
|
'url': 'https://www.ign.com/articles/2014/08/07/sons-of-anarchy-final-season-trailer',
|
||||||
'description': 'md5:53433c45df96d2ea5d0fda18be2ca908',
|
'only_matching': True,
|
||||||
'timestamp': 1412953920,
|
}, {
|
||||||
'upload_date': '20141010',
|
# Facebook embed
|
||||||
'uploader_id': 'chris_snyder@pcmag.com',
|
'url': 'https://www.ign.com/articles/2017/09/20/marvels-the-punisher-watch-the-new-trailer-for-the-netflix-series',
|
||||||
}
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# Brightcove embed
|
||||||
|
'url': 'https://www.ign.com/articles/2016/01/16/supergirl-goes-flying-with-martian-manhunter-in-new-clip',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
article = self._call_api(display_id)
|
||||||
|
|
||||||
|
def entries():
|
||||||
|
media_url = try_get(article, lambda x: x['mediaRelations'][0]['media']['metadata']['url'])
|
||||||
|
if media_url:
|
||||||
|
yield self.url_result(media_url, IGNIE.ie_key())
|
||||||
|
for content in (article.get('content') or []):
|
||||||
|
for video_url in re.findall(r'(?:\[(?:ignvideo\s+url|youtube\s+clip_id)|<iframe[^>]+src)="([^"]+)"', content):
|
||||||
|
yield self.url_result(video_url)
|
||||||
|
|
||||||
|
return self.playlist_result(
|
||||||
|
entries(), article.get('articleId'),
|
||||||
|
strip_or_none(try_get(article, lambda x: x['metadata']['headline'])))
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class ImgurIE(InfoExtractor):
|
|||||||
gif_json, video_id, transform_source=js_to_json)
|
gif_json, video_id, transform_source=js_to_json)
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': 'gif',
|
'format_id': 'gif',
|
||||||
'preference': -10,
|
'preference': -10, # gifs are worse than videos
|
||||||
'width': width,
|
'width': width,
|
||||||
'height': height,
|
'height': height,
|
||||||
'ext': 'gif',
|
'ext': 'gif',
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ class IqiyiIE(InfoExtractor):
|
|||||||
'url': stream['m3utx'],
|
'url': stream['m3utx'],
|
||||||
'format_id': vd,
|
'format_id': vd,
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'preference': self._FORMATS_MAP.get(vd, -1),
|
'quality': self._FORMATS_MAP.get(vd, -1),
|
||||||
'protocol': 'm3u8_native',
|
'protocol': 'm3u8_native',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -163,7 +163,10 @@ class IviIE(InfoExtractor):
|
|||||||
for f in result.get('files', []):
|
for f in result.get('files', []):
|
||||||
f_url = f.get('url')
|
f_url = f.get('url')
|
||||||
content_format = f.get('content_format')
|
content_format = f.get('content_format')
|
||||||
if not f_url or '-MDRM-' in content_format or '-FPS-' in content_format:
|
if not f_url:
|
||||||
|
continue
|
||||||
|
if (not self._downloader.params.get('allow_unplayable_formats')
|
||||||
|
and ('-MDRM-' in content_format or '-FPS-' in content_format)):
|
||||||
continue
|
continue
|
||||||
formats.append({
|
formats.append({
|
||||||
'url': f_url,
|
'url': f_url,
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ class KalturaIE(InfoExtractor):
|
|||||||
if f.get('fileExt') == 'chun':
|
if f.get('fileExt') == 'chun':
|
||||||
continue
|
continue
|
||||||
# DRM-protected video, cannot be decrypted
|
# DRM-protected video, cannot be decrypted
|
||||||
if f.get('fileExt') == 'wvm':
|
if not self._downloader.params.get('allow_unplayable_formats') and f.get('fileExt') == 'wvm':
|
||||||
continue
|
continue
|
||||||
if not f.get('fileExt'):
|
if not f.get('fileExt'):
|
||||||
# QT indicates QuickTime; some videos have broken fileExt
|
# QT indicates QuickTime; some videos have broken fileExt
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class KuwoBaseIE(InfoExtractor):
|
|||||||
'url': song_url,
|
'url': song_url,
|
||||||
'format_id': file_format['format'],
|
'format_id': file_format['format'],
|
||||||
'format': file_format['format'],
|
'format': file_format['format'],
|
||||||
'preference': file_format['preference'],
|
'quality': file_format['preference'],
|
||||||
'abr': file_format.get('abr'),
|
'abr': file_format.get('abr'),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ class LeIE(InfoExtractor):
|
|||||||
f['height'] = int_or_none(format_id[:-1])
|
f['height'] = int_or_none(format_id[:-1])
|
||||||
|
|
||||||
formats.append(f)
|
formats.append(f)
|
||||||
self._sort_formats(formats, ('height', 'quality', 'format_id'))
|
self._sort_formats(formats, ('res', 'quality'))
|
||||||
|
|
||||||
publish_time = parse_iso8601(self._html_search_regex(
|
publish_time = parse_iso8601(self._html_search_regex(
|
||||||
r'发布时间 ([^<>]+) ', page, 'publish time', default=None),
|
r'发布时间 ([^<>]+) ', page, 'publish time', default=None),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user