mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-08-08 13:18:35 +03:00
Compare commits
115
Commits
2021.09.02
...
2021.09.25
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fed277349 | ||
|
|
0ef787d773 | ||
|
|
a5de4099cb | ||
|
|
ff1c7fc9d3 | ||
|
|
600e900300 | ||
|
|
20b91b9b63 | ||
|
|
4c88ff87fc | ||
|
|
e27cc5d864 | ||
|
|
eb6d4ad1ca | ||
|
|
99e9e001de | ||
|
|
51ff9ca0b0 | ||
|
|
b19404591a | ||
|
|
1f8471e22c | ||
|
|
77c4a9ef68 | ||
|
|
8f70b0b82f | ||
|
|
be867b03f5 | ||
|
|
1813a6ccd4 | ||
|
|
8100c77223 | ||
|
|
9ada988bfc | ||
|
|
d1a7768432 | ||
|
|
49fa4d9af7 | ||
|
|
ee2b3563f3 | ||
|
|
bdc196a444 | ||
|
|
388bc4a640 | ||
|
|
50eff38c1c | ||
|
|
4be9dbdc24 | ||
|
|
a21e0ab1a1 | ||
|
|
a76e2e0f88 | ||
|
|
bd50a52b0d | ||
|
|
c12977bdc4 | ||
|
|
f6d8776d34 | ||
|
|
d806c9fd97 | ||
|
|
5e3f2f8fc4 | ||
|
|
1009f67c2a | ||
|
|
bd6f722de8 | ||
|
|
d9d8b85747 | ||
|
|
daf7ac2b92 | ||
|
|
96933fc1b6 | ||
|
|
0d32e124c6 | ||
|
|
cb2ec90e91 | ||
|
|
3cd786dbd7 | ||
|
|
1b629e1b4c | ||
|
|
8f8e8eba24 | ||
|
|
09906f554d | ||
|
|
a63d9bd0b0 | ||
|
|
f137e4c27c | ||
|
|
4762621925 | ||
|
|
57aa7b8511 | ||
|
|
9c1c3ec016 | ||
|
|
f9cc0161e6 | ||
|
|
c6af2dd8e5 | ||
|
|
7738bd3272 | ||
|
|
7c37ff97d3 | ||
|
|
d47f46e17e | ||
|
|
298bf1d275 | ||
|
|
d1b39ad844 | ||
|
|
edf65256aa | ||
|
|
7303f84abe | ||
|
|
f5aa5cfbff | ||
|
|
f1f6ca78b4 | ||
|
|
2fac2e9136 | ||
|
|
23dd2d9a32 | ||
|
|
b89378a69a | ||
|
|
0001fcb586 | ||
|
|
c589c1d395 | ||
|
|
f7590d4764 | ||
|
|
dbf7eca917 | ||
|
|
d21bba7853 | ||
|
|
a8cb7eca61 | ||
|
|
92790da2bb | ||
|
|
b5a39ed43b | ||
|
|
cc33cc4395 | ||
|
|
1722099ded | ||
|
|
40b18348e7 | ||
|
|
e9a30b181e | ||
|
|
9c95ac677e | ||
|
|
ea706726d6 | ||
|
|
f60990ddfc | ||
|
|
ad226b1dc9 | ||
|
|
ca46b94134 | ||
|
|
67ad7759af | ||
|
|
d5fe04f5c7 | ||
|
|
03c862794f | ||
|
|
0fd6661edb | ||
|
|
02c7ae8104 | ||
|
|
16f7e6be3a | ||
|
|
ffecd3034b | ||
|
|
1c5ce74c04 | ||
|
|
81a136b80f | ||
|
|
eab3f867e2 | ||
|
|
a7e999beec | ||
|
|
71407b3eca | ||
|
|
dc9de9cbd2 | ||
|
|
92ddaa415e | ||
|
|
b6de707d13 | ||
|
|
bccdbd22d5 | ||
|
|
bd9ff55bcd | ||
|
|
526d74ec5a | ||
|
|
e04a1ff92e | ||
|
|
aa6c25309a | ||
|
|
d98b006b85 | ||
|
|
265a7a8ee5 | ||
|
|
826446bd82 | ||
|
|
bc79491368 | ||
|
|
421ddcb8b4 | ||
|
|
c0ac49bcca | ||
|
|
02def2714c | ||
|
|
f9be9cb9fd | ||
|
|
4614bc22c1 | ||
|
|
8e5fecc88c | ||
|
|
165efb823b | ||
|
|
dd594deb2a | ||
|
|
409e18286e | ||
|
|
8113999995 | ||
|
|
8026e50152 |
@@ -21,7 +21,7 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.08.10. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.09.25. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
||||
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/yt-dlp/yt-dlp.
|
||||
- Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
|
||||
@@ -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've verified that I'm running yt-dlp version **2021.08.10**
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.09.25**
|
||||
- [ ] 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 searched the bugtracker for similar issues including closed ones
|
||||
@@ -44,7 +44,7 @@ Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your com
|
||||
[debug] User config: []
|
||||
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKc']
|
||||
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
||||
[debug] yt-dlp version 2021.08.10
|
||||
[debug] yt-dlp version 2021.09.25
|
||||
[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] Proxy map: {}
|
||||
|
||||
@@ -21,7 +21,7 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.08.10. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.09.25. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
||||
- Make sure that site you are requesting is not dedicated to copyright infringement, see https://github.com/yt-dlp/yt-dlp. yt-dlp does not support such sites. In order for site support request to be accepted all provided example URLs should not violate any copyrights.
|
||||
- Search the bugtracker for similar site support requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
|
||||
@@ -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've verified that I'm running yt-dlp version **2021.08.10**
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.09.25**
|
||||
- [ ] 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
|
||||
- [ ] The provided URLs do not contain any DRM to the best of my knowledge
|
||||
|
||||
@@ -21,13 +21,13 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.08.10. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.09.25. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- Search the bugtracker for similar site feature requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
|
||||
- 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've verified that I'm running yt-dlp version **2021.08.10**
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.09.25**
|
||||
- [ ] I've searched the bugtracker for similar site feature requests including closed ones
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.08.10. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.09.25. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
||||
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/yt-dlp/yt-dlp.
|
||||
- Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
|
||||
@@ -30,7 +30,7 @@ Carefully read and work through this check list in order to prevent the most com
|
||||
-->
|
||||
|
||||
- [ ] I'm reporting a bug unrelated to a specific site
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.08.10**
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.09.25**
|
||||
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
||||
- [ ] The provided URLs do not contain any DRM to the best of my knowledge
|
||||
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
|
||||
@@ -47,7 +47,7 @@ Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your com
|
||||
[debug] User config: []
|
||||
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKc']
|
||||
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
||||
[debug] yt-dlp version 2021.08.10
|
||||
[debug] yt-dlp version 2021.09.25
|
||||
[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] Proxy map: {}
|
||||
|
||||
@@ -21,13 +21,13 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.08.10. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.09.25. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- Search the bugtracker for similar feature requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
|
||||
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
||||
-->
|
||||
|
||||
- [ ] I'm reporting a feature request
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.08.10**
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.09.25**
|
||||
- [ ] I've searched the bugtracker for similar feature requests including closed ones
|
||||
|
||||
|
||||
|
||||
+132
-54
@@ -12,11 +12,15 @@ jobs:
|
||||
outputs:
|
||||
ytdlp_version: ${{ steps.bump_version.outputs.ytdlp_version }}
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
sha256_unix: ${{ steps.sha256_file.outputs.sha256_unix }}
|
||||
sha512_unix: ${{ steps.sha512_file.outputs.sha512_unix }}
|
||||
sha256_bin: ${{ steps.sha256_bin.outputs.sha256_bin }}
|
||||
sha512_bin: ${{ steps.sha512_bin.outputs.sha512_bin }}
|
||||
sha256_tar: ${{ steps.sha256_tar.outputs.sha256_tar }}
|
||||
sha512_tar: ${{ steps.sha512_tar.outputs.sha512_tar }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
@@ -25,11 +29,76 @@ jobs:
|
||||
run: sudo apt-get -y install zip pandoc man
|
||||
- name: Bump version
|
||||
id: bump_version
|
||||
run: python devscripts/update-version.py
|
||||
run: |
|
||||
python devscripts/update-version.py
|
||||
make issuetemplates
|
||||
- name: Print version
|
||||
run: echo "${{ steps.bump_version.outputs.ytdlp_version }}"
|
||||
- name: Update master
|
||||
id: push_update
|
||||
run: |
|
||||
git config --global user.email "${{ github.event.pusher.email }}"
|
||||
git config --global user.name "${{ github.event.pusher.name }}"
|
||||
git add -u
|
||||
git commit -m "[version] update" -m ":ci skip all"
|
||||
git pull --rebase origin ${{ github.event.repository.master_branch }}
|
||||
git push origin ${{ github.event.ref }}:${{ github.event.repository.master_branch }}
|
||||
echo ::set-output name=head_sha::$(git rev-parse HEAD)
|
||||
- name: Get Changelog
|
||||
id: get_changelog
|
||||
run: |
|
||||
changelog=$(cat Changelog.md | grep -oPz '(?s)(?<=### ${{ steps.bump_version.outputs.ytdlp_version }}\n{2}).+?(?=\n{2,3}###)')
|
||||
echo "changelog<<EOF" >> $GITHUB_ENV
|
||||
echo "$changelog" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
- name: Run Make
|
||||
run: make all tar
|
||||
- name: Get SHA2-256SUMS for yt-dlp
|
||||
id: sha256_bin
|
||||
run: echo "::set-output name=sha256_bin::$(sha256sum yt-dlp | awk '{print $1}')"
|
||||
- name: Get SHA2-256SUMS for yt-dlp.tar.gz
|
||||
id: sha256_tar
|
||||
run: echo "::set-output name=sha256_tar::$(sha256sum yt-dlp.tar.gz | awk '{print $1}')"
|
||||
- name: Get SHA2-512SUMS for yt-dlp
|
||||
id: sha512_bin
|
||||
run: echo "::set-output name=sha512_bin::$(sha512sum yt-dlp | awk '{print $1}')"
|
||||
- name: Get SHA2-512SUMS for yt-dlp.tar.gz
|
||||
id: sha512_tar
|
||||
run: echo "::set-output name=sha512_tar::$(sha512sum yt-dlp.tar.gz | awk '{print $1}')"
|
||||
- name: Install dependencies for pypi
|
||||
env:
|
||||
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||
if: "env.PYPI_TOKEN != ''"
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
- name: Build and publish on pypi
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
||||
if: "env.TWINE_PASSWORD != ''"
|
||||
run: |
|
||||
rm -rf dist/*
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
- name: Install SSH private key
|
||||
env:
|
||||
BREW_TOKEN: ${{ secrets.BREW_TOKEN }}
|
||||
if: "env.BREW_TOKEN != ''"
|
||||
uses: webfactory/ssh-agent@v0.5.3
|
||||
with:
|
||||
ssh-private-key: ${{ env.BREW_TOKEN }}
|
||||
- name: Update Homebrew Formulae
|
||||
env:
|
||||
BREW_TOKEN: ${{ secrets.BREW_TOKEN }}
|
||||
if: "env.BREW_TOKEN != ''"
|
||||
run: |
|
||||
git clone git@github.com:yt-dlp/homebrew-taps taps/
|
||||
python3 devscripts/update-formulae.py taps/Formula/yt-dlp.rb "${{ steps.bump_version.outputs.ytdlp_version }}"
|
||||
git -C taps/ config user.name github-actions
|
||||
git -C taps/ config user.email github-actions@example.com
|
||||
git -C taps/ commit -am 'yt-dlp: ${{ steps.bump_version.outputs.ytdlp_version }}'
|
||||
git -C taps/ push
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
@@ -38,9 +107,10 @@ jobs:
|
||||
with:
|
||||
tag_name: ${{ steps.bump_version.outputs.ytdlp_version }}
|
||||
release_name: yt-dlp ${{ steps.bump_version.outputs.ytdlp_version }}
|
||||
commitish: ${{ steps.push_update.outputs.head_sha }}
|
||||
body: |
|
||||
Changelog:
|
||||
PLACEHOLDER
|
||||
${{ env.changelog }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
- name: Upload yt-dlp Unix binary
|
||||
@@ -62,36 +132,16 @@ jobs:
|
||||
asset_path: ./yt-dlp.tar.gz
|
||||
asset_name: yt-dlp.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
- name: Get SHA2-256SUMS for yt-dlp
|
||||
id: sha256_file
|
||||
run: echo "::set-output name=sha256_unix::$(sha256sum yt-dlp | awk '{print $1}')"
|
||||
- name: Get SHA2-512SUMS for yt-dlp
|
||||
id: sha512_file
|
||||
run: echo "::set-output name=sha512_unix::$(sha512sum yt-dlp | awk '{print $1}')"
|
||||
- name: Install dependencies for pypi
|
||||
env:
|
||||
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||
if: "env.PYPI_TOKEN != ''"
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
- name: Build and publish on pypi
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
||||
if: "env.TWINE_PASSWORD != ''"
|
||||
run: |
|
||||
rm -rf dist/*
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
|
||||
build_windows:
|
||||
runs-on: windows-latest
|
||||
needs: build_unix
|
||||
|
||||
outputs:
|
||||
sha256_windows: ${{ steps.sha256_file_win.outputs.sha256_windows }}
|
||||
sha512_windows: ${{ steps.sha512_file_win.outputs.sha512_windows }}
|
||||
sha256_win: ${{ steps.sha256_win.outputs.sha256_win }}
|
||||
sha512_win: ${{ steps.sha512_win.outputs.sha512_win }}
|
||||
sha256_win_zip: ${{ steps.sha256_win_zip.outputs.sha256_win_zip }}
|
||||
sha512_win_zip: ${{ steps.sha512_win_zip.outputs.sha512_win_zip }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -104,7 +154,7 @@ jobs:
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
- name: Install Requirements
|
||||
# Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
|
||||
run: pip install "https://yt-dlp.github.io/pyinstaller-builds/x86_64/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodome websockets
|
||||
run: pip install "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodome websockets
|
||||
- name: Bump version
|
||||
id: bump_version
|
||||
run: python devscripts/update-version.py
|
||||
@@ -123,19 +173,41 @@ jobs:
|
||||
asset_name: yt-dlp.exe
|
||||
asset_content_type: application/vnd.microsoft.portable-executable
|
||||
- name: Get SHA2-256SUMS for yt-dlp.exe
|
||||
id: sha256_file_win
|
||||
run: echo "::set-output name=sha256_windows::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA256).Hash.ToLower())"
|
||||
id: sha256_win
|
||||
run: echo "::set-output name=sha256_win::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA256).Hash.ToLower())"
|
||||
- name: Get SHA2-512SUMS for yt-dlp.exe
|
||||
id: sha512_file_win
|
||||
run: echo "::set-output name=sha512_windows::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA512).Hash.ToLower())"
|
||||
id: sha512_win
|
||||
run: echo "::set-output name=sha512_win::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA512).Hash.ToLower())"
|
||||
- name: Run PyInstaller Script with --onedir
|
||||
run: python pyinst.py 64 --onedir
|
||||
- uses: papeloto/action-zip@v1
|
||||
with:
|
||||
files: ./dist/yt-dlp
|
||||
dest: ./dist/yt-dlp.zip
|
||||
- name: Upload yt-dlp.zip Windows onedir
|
||||
id: upload-release-windows-zip
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.build_unix.outputs.upload_url }}
|
||||
asset_path: ./dist/yt-dlp.zip
|
||||
asset_name: yt-dlp.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Get SHA2-256SUMS for yt-dlp.zip
|
||||
id: sha256_win_zip
|
||||
run: echo "::set-output name=sha256_win_zip::$((Get-FileHash dist\yt-dlp.zip -Algorithm SHA256).Hash.ToLower())"
|
||||
- name: Get SHA2-512SUMS for yt-dlp.zip
|
||||
id: sha512_win_zip
|
||||
run: echo "::set-output name=sha512_win_zip::$((Get-FileHash dist\yt-dlp.zip -Algorithm SHA512).Hash.ToLower())"
|
||||
|
||||
build_windows32:
|
||||
runs-on: windows-latest
|
||||
needs: [build_unix, build_windows]
|
||||
|
||||
outputs:
|
||||
sha256_windows32: ${{ steps.sha256_file_win32.outputs.sha256_windows32 }}
|
||||
sha512_windows32: ${{ steps.sha512_file_win32.outputs.sha512_windows32 }}
|
||||
sha256_win32: ${{ steps.sha256_win32.outputs.sha256_win32 }}
|
||||
sha512_win32: ${{ steps.sha512_win32.outputs.sha512_win32 }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -148,7 +220,7 @@ jobs:
|
||||
- name: Upgrade pip and enable wheel support
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
- name: Install Requirements
|
||||
run: pip install "https://yt-dlp.github.io/pyinstaller-builds/i686/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodome websockets
|
||||
run: pip install "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodome websockets
|
||||
- name: Bump version
|
||||
id: bump_version
|
||||
run: python devscripts/update-version.py
|
||||
@@ -167,11 +239,11 @@ jobs:
|
||||
asset_name: yt-dlp_x86.exe
|
||||
asset_content_type: application/vnd.microsoft.portable-executable
|
||||
- name: Get SHA2-256SUMS for yt-dlp_x86.exe
|
||||
id: sha256_file_win32
|
||||
run: echo "::set-output name=sha256_windows32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA256).Hash.ToLower())"
|
||||
id: sha256_win32
|
||||
run: echo "::set-output name=sha256_win32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA256).Hash.ToLower())"
|
||||
- name: Get SHA2-512SUMS for yt-dlp_x86.exe
|
||||
id: sha512_file_win32
|
||||
run: echo "::set-output name=sha512_windows32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA512).Hash.ToLower())"
|
||||
id: sha512_win32
|
||||
run: echo "::set-output name=sha512_win32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA512).Hash.ToLower())"
|
||||
|
||||
finish:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -180,15 +252,17 @@ jobs:
|
||||
steps:
|
||||
- name: Make SHA2-256SUMS file
|
||||
env:
|
||||
SHA256_WINDOWS: ${{ needs.build_windows.outputs.sha256_windows }}
|
||||
SHA256_WINDOWS32: ${{ needs.build_windows32.outputs.sha256_windows32 }}
|
||||
SHA256_UNIX: ${{ needs.build_unix.outputs.sha256_unix }}
|
||||
YTDLP_VERSION: ${{ needs.build_unix.outputs.ytdlp_version }}
|
||||
SHA256_WIN: ${{ needs.build_windows.outputs.sha256_win }}
|
||||
SHA256_WIN_ZIP: ${{ needs.build_windows.outputs.sha256_win_zip }}
|
||||
SHA256_WIN32: ${{ needs.build_windows32.outputs.sha256_win32 }}
|
||||
SHA256_BIN: ${{ needs.build_unix.outputs.sha256_bin }}
|
||||
SHA256_TAR: ${{ needs.build_unix.outputs.sha256_tar }}
|
||||
run: |
|
||||
echo "version:${{ env.YTDLP_VERSION }}" >> SHA2-256SUMS
|
||||
echo "yt-dlp.exe:${{ env.SHA256_WINDOWS }}" >> SHA2-256SUMS
|
||||
echo "yt-dlp_x86.exe:${{ env.SHA256_WINDOWS32 }}" >> SHA2-256SUMS
|
||||
echo "yt-dlp:${{ env.SHA256_UNIX }}" >> SHA2-256SUMS
|
||||
echo "${{ env.SHA256_WIN }} yt-dlp.exe" >> SHA2-256SUMS
|
||||
echo "${{ env.SHA256_WIN32 }} yt-dlp_x86.exe" >> SHA2-256SUMS
|
||||
echo "${{ env.SHA256_BIN }} yt-dlp" >> SHA2-256SUMS
|
||||
echo "${{ env.SHA256_TAR }} yt-dlp.tar.gz" >> SHA2-256SUMS
|
||||
echo "${{ env.SHA256_WIN_ZIP }} yt-dlp.zip" >> SHA2-256SUMS
|
||||
- name: Upload 256SUMS file
|
||||
id: upload-sums
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -201,13 +275,17 @@ jobs:
|
||||
asset_content_type: text/plain
|
||||
- name: Make SHA2-512SUMS file
|
||||
env:
|
||||
SHA512_WINDOWS: ${{ needs.build_windows.outputs.sha512_windows }}
|
||||
SHA512_WINDOWS32: ${{ needs.build_windows32.outputs.sha512_windows32 }}
|
||||
SHA512_UNIX: ${{ needs.build_unix.outputs.sha512_unix }}
|
||||
SHA512_WIN: ${{ needs.build_windows.outputs.sha512_win }}
|
||||
SHA512_WIN_ZIP: ${{ needs.build_windows.outputs.sha512_win_zip }}
|
||||
SHA512_WIN32: ${{ needs.build_windows32.outputs.sha512_win32 }}
|
||||
SHA512_BIN: ${{ needs.build_unix.outputs.sha512_bin }}
|
||||
SHA512_TAR: ${{ needs.build_unix.outputs.sha512_tar }}
|
||||
run: |
|
||||
echo "${{ env.SHA512_WINDOWS }} yt-dlp.exe" >> SHA2-512SUMS
|
||||
echo "${{ env.SHA512_WINDOWS32 }} yt-dlp_x86.exe" >> SHA2-512SUMS
|
||||
echo "${{ env.SHA512_UNIX }} yt-dlp" >> SHA2-512SUMS
|
||||
echo "${{ env.SHA512_WIN }} yt-dlp.exe" >> SHA2-512SUMS
|
||||
echo "${{ env.SHA512_WIN32 }} yt-dlp_x86.exe" >> SHA2-512SUMS
|
||||
echo "${{ env.SHA512_BIN }} yt-dlp" >> SHA2-512SUMS
|
||||
echo "${{ env.SHA512_TAR }} yt-dlp.tar.gz" >> SHA2-512SUMS
|
||||
echo "${{ env.SHA512_WIN_ZIP }} yt-dlp.zip" >> SHA2-512SUMS
|
||||
- name: Upload 512SUMS file
|
||||
id: upload-512sums
|
||||
uses: actions/upload-release-asset@v1
|
||||
|
||||
+3
-2
@@ -2,7 +2,8 @@
|
||||
*.conf
|
||||
*.spec
|
||||
cookies
|
||||
cookies.txt
|
||||
*cookies.txt
|
||||
.netrc
|
||||
|
||||
# Downloaded
|
||||
*.srt
|
||||
@@ -42,7 +43,7 @@ cookies.txt
|
||||
*.description
|
||||
|
||||
# Allow config/media files in testdata
|
||||
!test/testdata/**
|
||||
!test/**
|
||||
|
||||
# Python
|
||||
*.pyc
|
||||
|
||||
@@ -100,3 +100,21 @@ korli
|
||||
octotherp
|
||||
CeruleanSky
|
||||
zootedb0t
|
||||
chao813
|
||||
ChillingPepper
|
||||
ConquerorDopy
|
||||
dalanmiller
|
||||
DigitalDJ
|
||||
f4pp3rk1ng
|
||||
gesa
|
||||
Jules-A
|
||||
makeworld-the-better-one
|
||||
MKSherbini
|
||||
mrx23dot
|
||||
poschi3
|
||||
raphaeldore
|
||||
renalid
|
||||
sleaux-meaux
|
||||
sulyi
|
||||
tmarki
|
||||
Vangelis66
|
||||
|
||||
+115
-13
@@ -7,18 +7,123 @@
|
||||
* Update Changelog.md and CONTRIBUTORS
|
||||
* Change "Merged with ytdl" version in Readme.md if needed
|
||||
* Add new/fixed extractors in "new features" section of Readme.md
|
||||
* Commit to master as `Release <version>`
|
||||
* Commit as `Release <version>`
|
||||
* Push to origin/release using `git push origin master:release`
|
||||
build task will now run
|
||||
* Update version.py using `devscripts\update-version.py`
|
||||
* Run `make issuetemplates`
|
||||
* Commit to master as `[version] update :ci skip all`
|
||||
* Push to origin/master
|
||||
* Update changelog in /releases
|
||||
|
||||
-->
|
||||
|
||||
|
||||
### 2021.09.25
|
||||
|
||||
* Add new option `--netrc-location`
|
||||
* [outtmpl] Allow alternate fields using `,`
|
||||
* [outtmpl] Add format type `B` to treat the value as bytes (eg: to limit the filename to a certain number of bytes)
|
||||
* Separate the options `--ignore-errors` and `--no-abort-on-error`
|
||||
* Basic framework for simultaneous download of multiple formats by [nao20010128nao](https://github.com/nao20010128nao)
|
||||
* [17live] Add 17.live extractor by [nao20010128nao](https://github.com/nao20010128nao)
|
||||
* [bilibili] Add BiliIntlIE and BiliIntlSeriesIE by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [CAM4] Add extractor by [alerikaisattera](https://github.com/alerikaisattera)
|
||||
* [Chingari] Add extractors by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [CGTN] Add extractor by [chao813](https://github.com/chao813)
|
||||
* [damtomo] Add extractor by [nao20010128nao](https://github.com/nao20010128nao)
|
||||
* [gotostage] Add extractor by [poschi3](https://github.com/poschi3)
|
||||
* [Koo] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [Mediaite] Add Extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [Mediaklikk] Add Extractor by [tmarki](https://github.com/tmarki), [mrx23dot](https://github.com/mrx23dot), [coletdjnz](https://github.com/coletdjnz)
|
||||
* [MuseScore] Add Extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [Newgrounds] Add NewgroundsUserIE and improve extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [nzherald] Add NZHeraldIE by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [Olympics] Add replay extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [Peertube] Add channel and playlist extractors by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [radlive] Add extractor by [nyuszika7h](https://github.com/nyuszika7h)
|
||||
* [SovietsCloset] Add extractor by [ChillingPepper](https://github.com/ChillingPepper)
|
||||
* [Streamanity] Add Extractor by [alerikaisattera](https://github.com/alerikaisattera)
|
||||
* [Theta] Add extractor by [alerikaisattera](https://github.com/alerikaisattera)
|
||||
* [Yandex] Add ZenYandexIE and ZenYandexChannelIE by [Ashish0804](https://github.com/Ashish0804)
|
||||
|
||||
* [9Now] handle episodes of series by [dalanmiller](https://github.com/dalanmiller)
|
||||
* [AnimalPlanet] Fix extractor by [Sipherdrakon](https://github.com/Sipherdrakon)
|
||||
* [Arte] Improve description extraction by [renalid](https://github.com/renalid)
|
||||
* [atv.at] Use jwt for API by [NeroBurner](https://github.com/NeroBurner)
|
||||
* [brightcove] Extract subtitles from manifests
|
||||
* [CBC] Fix CBC Gem extractors by [makeworld-the-better-one](https://github.com/makeworld-the-better-one)
|
||||
* [cbs] Report appropriate error for DRM
|
||||
* [comedycentral] Support `collection-playlist` by [nixxo](https://github.com/nixxo)
|
||||
* [DIYNetwork] Support new format by [Sipherdrakon](https://github.com/Sipherdrakon)
|
||||
* [downloader/niconico] Pass custom headers by [nao20010128nao](https://github.com/nao20010128nao)
|
||||
* [dw] Fix extractor
|
||||
* [Fancode] Fix live streams by [zenerdi0de](https://github.com/zenerdi0de)
|
||||
* [funimation] Fix for locations outside US by [Jules-A](https://github.com/Jules-A), [pukkandan](https://github.com/pukkandan)
|
||||
* [globo] Fix GloboIE by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [HiDive] Fix extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [Hotstar] Add referer for subs by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [itv] Fix extractor, add subtitles and thumbnails by [coletdjnz](https://github.com/coletdjnz), [sleaux-meaux](https://github.com/sleaux-meaux), [Vangelis66](https://github.com/Vangelis66)
|
||||
* [lbry] Show error message from API response
|
||||
* [Mxplayer] Use mobile API by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [NDR] Rewrite NDRIE by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [Nuvid] Fix extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [Oreilly] Handle new web url by [MKSherbini](https://github.com/MKSherbini)
|
||||
* [pbs] Fix subtitle extraction by [coletdjnz](https://github.com/coletdjnz), [gesa](https://github.com/gesa), [raphaeldore](https://github.com/raphaeldore)
|
||||
* [peertube] Update instances by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [plutotv] Fix extractor for URLs with `/en`
|
||||
* [reddit] Workaround for 429 by redirecting to old.reddit.com
|
||||
* [redtube] Fix exts
|
||||
* [soundcloud] Make playlist extraction lazy
|
||||
* [soundcloud] Retry playlist pages on `502` error and update `_CLIENT_ID`
|
||||
* [southpark] Fix SouthParkDE by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [SovietsCloset] Fix playlists for games with only named categories by [ConquerorDopy](https://github.com/ConquerorDopy)
|
||||
* [SpankBang] Fix uploader by [f4pp3rk1ng](https://github.com/f4pp3rk1ng), [coletdjnz](https://github.com/coletdjnz)
|
||||
* [tiktok] Use API to fetch higher quality video by [MinePlayersPE](https://github.com/MinePlayersPE), [llacb47](https://github.com/llacb47)
|
||||
* [TikTokUser] Fix extractor using mobile API by [MinePlayersPE](https://github.com/MinePlayersPE), [llacb47](https://github.com/llacb47)
|
||||
* [videa] Fix some extraction errors by [nyuszika7h](https://github.com/nyuszika7h)
|
||||
* [VrtNU] Handle login errors by [llacb47](https://github.com/llacb47)
|
||||
* [vrv] Don't raise error when thumbnails are missing
|
||||
* [youtube] Cleanup authentication code by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [youtube] Fix `--mark-watched` with `--cookies-from-browser`
|
||||
* [youtube] Improvements to JS player extraction and add extractor-args to skip it by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [youtube] Retry on 'Unknown Error' by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [youtube] Return full URL instead of just ID
|
||||
* [youtube] Warn when trying to download clips
|
||||
* [zdf] Improve format sorting
|
||||
* [zype] Extract subtitles from the m3u8 manifest by [fstirlitz](https://github.com/fstirlitz)
|
||||
* Allow `--force-write-archive` to work with `--flat-playlist`
|
||||
* Download subtitles in order of `--sub-langs`
|
||||
* Allow `0` in `--playlist-items`
|
||||
* Handle more playlist errors with `-i`
|
||||
* Fix `--no-get-comments`
|
||||
* Fix `extra_info` being reused across runs
|
||||
* Fix compat options `no-direct-merge` and `playlist-index`
|
||||
* Dump files should obey `--trim-filename` by [sulyi](https://github.com/sulyi)
|
||||
* [aes] Add `aes_gcm_decrypt_and_verify` by [sulyi](https://github.com/sulyi), [pukkandan](https://github.com/pukkandan)
|
||||
* [aria2c] Fix IV for some AES-128 streams by [shirt](https://github.com/shirt-dev)
|
||||
* [compat] Don't ignore `HOME` (if set) on windows
|
||||
* [cookies] Make browser names case insensitive
|
||||
* [cookies] Print warning for cookie decoding error only once
|
||||
* [extractor] Fix root-relative URLs in MPD by [DigitalDJ](https://github.com/DigitalDJ)
|
||||
* [ffmpeg] Add `aac_adtstoasc` when merging if needed
|
||||
* [fragment,aria2c] Generalize and refactor some code
|
||||
* [fragment] Avoid repeated request for AES key
|
||||
* [fragment] Fix range header when using `-N` and media sequence by [shirt](https://github.com/shirt-dev)
|
||||
* [hls,aes] Fallback to native implementation for AES-CBC and detect `Cryptodome` in addition to `Crypto`
|
||||
* [hls] Byterange + AES128 is supported by native downloader
|
||||
* [ModifyChapters] Improve sponsor chapter merge algorithm by [nihil-admirari](https://github.com/nihil-admirari)
|
||||
* [ModifyChapters] Minor fixes
|
||||
* [WebVTT] Adjust parser to accommodate PBS subtitles
|
||||
* [utils] Improve `extract_timezone` by [dirkf](https://github.com/dirkf)
|
||||
* [options] Fix `--no-config` and refactor reading of config files
|
||||
* [options] Strip spaces and ignore empty entries in list-like switches
|
||||
* [test/cookies] Improve logging
|
||||
* [build] Automate more of the release process by [animelover1984](https://github.com/animelover1984), [pukkandan](https://github.com/pukkandan)
|
||||
* [build] Fix sha256 by [nihil-admirari](https://github.com/nihil-admirari)
|
||||
* [build] Bring back brew taps by [nao20010128nao](https://github.com/nao20010128nao)
|
||||
* [build] Provide `--onedir` zip for windows by [pukkandan](https://github.com/pukkandan)
|
||||
* [cleanup,docs] Add deprecation warning in docs for some counter intuitive behaviour
|
||||
* [cleanup] Fix line endings for `nebula.py` by [glenn-slayden](https://github.com/glenn-slayden)
|
||||
* [cleanup] Improve `make clean-test` by [sulyi](https://github.com/sulyi)
|
||||
* [cleanup] Misc
|
||||
|
||||
|
||||
### 2021.09.02
|
||||
|
||||
* **Native SponsorBlock** implementation by [nihil-admirari](https://github.com/nihil-admirari), [pukkandan](https://github.com/pukkandan)
|
||||
@@ -30,7 +135,7 @@
|
||||
* The fetched sponsor sections are written to the infojson
|
||||
* Deprecates: `--sponskrub`, `--no-sponskrub`, `--sponskrub-cut`, `--no-sponskrub-cut`, `--sponskrub-force`, `--no-sponskrub-force`, `--sponskrub-location`, `--sponskrub-args`
|
||||
* Split `--embed-chapters` from `--embed-metadata` (it still implies the former by default)
|
||||
* Add option `--remove-chapters` to remove arbitrary chapters by [nihil-admirari](https://github.com/nihil-admirari), pukkandan
|
||||
* Add option `--remove-chapters` to remove arbitrary chapters by [nihil-admirari](https://github.com/nihil-admirari), [pukkandan](https://github.com/pukkandan)
|
||||
* Add option `--force-keyframes-at-cuts` for more accurate cuts when removing and splitting chapters by [nihil-admirari](https://github.com/nihil-admirari)
|
||||
* Let `--match-filter` reject entries early
|
||||
* Makes redundant: `--match-title`, `--reject-title`, `--min-views`, `--max-views`
|
||||
@@ -42,7 +147,6 @@
|
||||
* [downloader/ffmpeg] Experimental support for DASH manifests (including live)
|
||||
* Your ffmpeg must have [this patch](https://github.com/FFmpeg/FFmpeg/commit/3249c757aed678780e22e99a1a49f4672851bca9) applied for YouTube DASH to work
|
||||
* [downloader/ffmpeg] Allow passing custom arguments before `-i`
|
||||
|
||||
* [BannedVideo] Add extractor by [smege1001](https://github.com/smege1001), [blackjack4494](https://github.com/blackjack4494), [pukkandan](https://github.com/pukkandan)
|
||||
* [bilibili] Add category extractor by [animelover1984](https://github.com/animelover1984)
|
||||
* [Epicon] Add extractors by [Ashish0804](https://github.com/Ashish0804)
|
||||
@@ -60,7 +164,6 @@
|
||||
* [Tokentube] Add extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [TV2Hu] Fix `TV2HuIE` and add `TV2HuSeriesIE` by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [voicy] Add extractor by [nao20010128nao](https://github.com/nao20010128nao)
|
||||
|
||||
* [adobepass] Fix Verizon SAML login by [nyuszika7h](https://github.com/nyuszika7h), [ParadoxGBB](https://github.com/ParadoxGBB)
|
||||
* [afreecatv] Fix adult VODs by [wlritchi](https://github.com/wlritchi)
|
||||
* [afreecatv] Tolerate failure to parse date string by [wlritchi](https://github.com/wlritchi)
|
||||
@@ -100,7 +203,6 @@
|
||||
* [youtube] Prefer audio stream that YouTube considers default
|
||||
* [youtube] Remove annotations and deprecate `--write-annotations` by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [Zee5] Fix extractor and add subtitles by [Ashish0804](https://github.com/Ashish0804)
|
||||
|
||||
* [aria2c] Obey `--rate-limit`
|
||||
* [EmbedSubtitle] Continue even if some files are missing
|
||||
* [extractor] Better error message for DRM
|
||||
@@ -185,8 +287,8 @@
|
||||
### 2021.08.02
|
||||
|
||||
* Add logo, banner and donate links
|
||||
* Expand and escape environment variables correctly in output template
|
||||
* Add format types `j` (json), `l` (comma delimited list), `q` (quoted for terminal) in output template
|
||||
* [outtmpl] Expand and escape environment variables
|
||||
* [outtmpl] Add format types `j` (json), `l` (comma delimited list), `q` (quoted for terminal)
|
||||
* [downloader] Allow streaming some unmerged formats to stdout using ffmpeg
|
||||
* [youtube] **Age-gate bypass**
|
||||
* Add `agegate` clients by [pukkandan](https://github.com/pukkandan), [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||
@@ -391,7 +493,7 @@
|
||||
### 2021.06.09
|
||||
|
||||
* Fix bug where `%(field)d` in filename template throws error
|
||||
* Improve offset parsing in outtmpl
|
||||
* [outtmpl] Improve offset parsing
|
||||
* [test] More rigorous tests for `prepare_filename`
|
||||
|
||||
### 2021.06.08
|
||||
|
||||
@@ -13,7 +13,9 @@ pypi-files: AUTHORS Changelog.md LICENSE README.md README.txt supportedsites com
|
||||
.PHONY: all clean install test tar pypi-files completions ot offlinetest codetest supportedsites
|
||||
|
||||
clean-test:
|
||||
rm -rf *.dump *.part* *.ytdl *.info.json *.mp4 *.m4a *.flv *.mp3 *.avi *.mkv *.webm *.3gp *.wav *.ape *.swf *.jpg *.png *.frag *.frag.urls *.frag.aria2 test/testdata/player-*.js *.opus *.webp *.ttml *.vtt *.jpeg
|
||||
rm -rf *.3gp *.annotations.xml *.ape *.avi *.description *.dump *.flac *.flv *.frag *.frag.aria2 *.frag.urls \
|
||||
*.info.json *.jpeg *.jpg *.live_chat.json *.m4a *.m4v *.mkv *.mp3 *.mp4 *.ogg *.opus *.part* *.png *.sbv *.srt \
|
||||
*.swf *.swp *.ttml *.vtt *.wav *.webm *.webp *.ytdl test/testdata/player-*.js
|
||||
clean-dist:
|
||||
rm -rf yt-dlp.1.temp.md yt-dlp.1 README.txt MANIFEST build/ dist/ .coverage cover/ yt-dlp.tar.gz completions/ yt_dlp/extractor/lazy_extractors.py *.spec CONTRIBUTING.md.tmp yt-dlp yt-dlp.exe yt_dlp.egg-info/ AUTHORS .mailmap
|
||||
clean-cache:
|
||||
|
||||
@@ -88,9 +88,9 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
|
||||
|
||||
* **Aria2c with HLS/DASH**: You can use `aria2c` as the external downloader for DASH(mpd) and HLS(m3u8) formats
|
||||
|
||||
* **New extractors**: AnimeLab, Philo MSO, Spectrum MSO, SlingTV MSO, Cablevision MSO, Rcs, Gedi, bitwave.tv, mildom, audius, zee5, mtv.it, wimtv, pluto.tv, niconico users, discoveryplus.in, mediathek, NFHSNetwork, nebula, ukcolumn, whowatch, MxplayerShow, parlview (au), YoutubeWebArchive, fancode, Saitosan, ShemarooMe, telemundo, VootSeries, SonyLIVSeries, HotstarSeries, VidioPremier, VidioLive, RCTIPlus, TBS Live, douyin, pornflip, ParamountPlusSeries, ScienceChannel, Utreon, OpenRec, BandcampMusic, blackboardcollaborate, eroprofile albums, mirrativ, BannedVideo, bilibili categories, Epicon, filmmodu, GabTV, HungamaAlbum, ManotoTV, Niconico search, Patreon User, peloton, ProjectVeritas, radiko, StarTV, tiktok user, Tokentube, voicy, TV2HuSeries
|
||||
* **New extractors**: AnimeLab, Philo MSO, Spectrum MSO, SlingTV MSO, Cablevision MSO, Rcs, Gedi, bitwave.tv, mildom, audius, zee5, mtv.it, wimtv, pluto.tv, niconico users, discoveryplus.in, mediathek, NFHSNetwork, nebula, ukcolumn, whowatch, MxplayerShow, parlview (au), YoutubeWebArchive, fancode, Saitosan, ShemarooMe, telemundo, VootSeries, SonyLIVSeries, HotstarSeries, VidioPremier, VidioLive, RCTIPlus, TBS Live, douyin, pornflip, ParamountPlusSeries, ScienceChannel, Utreon, OpenRec, BandcampMusic, blackboardcollaborate, eroprofile albums, mirrativ, BannedVideo, bilibili categories, Epicon, filmmodu, GabTV, HungamaAlbum, ManotoTV, Niconico search, Patreon User, peloton, ProjectVeritas, radiko, StarTV, tiktok user, Tokentube, voicy, TV2HuSeries, biliintl, 17live, NewgroundsUser, peertube channel/playlist, ZenYandex, CAM4, CGTN, damtomo, gotostage, Koo, Mediaite, Mediaklikk, MuseScore, nzherald, Olympics replay, radlive, SovietsCloset, Streamanity, Theta, Chingari
|
||||
|
||||
* **Fixed/improved extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, akamai, ina, rumble, tennistv, amcnetworks, la7 podcasts, linuxacadamy, nitter, twitcasting, viu, crackle, curiositystream, mediasite, rmcdecouverte, sonyliv, tubi, tenplay, patreon, videa, yahoo, BravoTV, crunchyroll playlist, RTP, viki, Hotstar, vidio, vimeo, mediaset, Mxplayer, nbcolympics, ParamountPlus, Newgrounds, SAML Verizon login, Hungama, afreecatv, aljazeera, ATV, bitchute, camtube, CDA, eroprofile, facebook, HearThisAtIE, iwara, kakao, Motherless, Nova, peertube, pornhub, reddit, tiktok, TV2, TV2Hu, tv5mondeplus, VH1, Viafree, XHamster
|
||||
* **Fixed/improved extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, akamai, ina, rumble, tennistv, amcnetworks, la7 podcasts, linuxacadamy, nitter, twitcasting, viu, crackle, curiositystream, mediasite, rmcdecouverte, sonyliv, tubi, tenplay, patreon, videa, yahoo, BravoTV, crunchyroll playlist, RTP, viki, Hotstar, vidio, vimeo, mediaset, Mxplayer, nbcolympics, ParamountPlus, Newgrounds, SAML Verizon login, Hungama, afreecatv, aljazeera, ATV, bitchute, camtube, CDA, eroprofile, facebook, HearThisAtIE, iwara, kakao, Motherless, Nova, peertube, pornhub, reddit, tiktok, TV2, TV2Hu, tv5mondeplus, VH1, Viafree, XHamster, 9Now, AnimalPlanet, Arte, CBC, Chingari, comedycentral, DIYNetwork, niconico, dw, funimation, globo, HiDive, NDR, Nuvid, Oreilly, pbs, plutotv, reddit, redtube, soundcloud, SpankBang, VrtNU
|
||||
|
||||
* **Subtitle extraction from manifests**: Subtitles can be extracted from streaming media manifests. See [commit/be6202f](https://github.com/yt-dlp/yt-dlp/commit/be6202f12b97858b9d716e608394b51065d0419f) for details
|
||||
|
||||
@@ -243,9 +243,12 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
|
||||
-U, --update Update this program to latest version. Make
|
||||
sure that you have sufficient permissions
|
||||
(run with sudo if needed)
|
||||
-i, --ignore-errors Continue on download errors, for example to
|
||||
skip unavailable videos in a playlist
|
||||
(default) (Alias: --no-abort-on-error)
|
||||
-i, --ignore-errors Ignore download and postprocessing errors.
|
||||
The download will be considered successfull
|
||||
even if the postprocessing fails
|
||||
--no-abort-on-error Continue with next video on download
|
||||
errors; e.g. to skip unavailable videos in
|
||||
a playlist (default)
|
||||
--abort-on-error Abort downloading of further videos if an
|
||||
error occurs (Alias: --no-ignore-errors)
|
||||
--dump-user-agent Display the current user-agent and exit
|
||||
@@ -530,10 +533,10 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
|
||||
--cookies-from-browser BROWSER[:PROFILE]
|
||||
Load cookies from a user profile of the
|
||||
given web browser. Currently supported
|
||||
browsers are: brave|chrome|chromium|edge|fi
|
||||
refox|opera|safari|vivaldi. You can specify
|
||||
the user profile name or directory using
|
||||
"BROWSER:PROFILE_NAME" or
|
||||
browsers are: brave, chrome, chromium,
|
||||
edge, firefox, opera, safari, vivaldi. You
|
||||
can specify the user profile name or
|
||||
directory using "BROWSER:PROFILE_NAME" or
|
||||
"BROWSER:PROFILE_PATH". If no profile is
|
||||
given, the most recently accessed one is
|
||||
used
|
||||
@@ -695,6 +698,9 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
|
||||
out, yt-dlp will ask interactively
|
||||
-2, --twofactor TWOFACTOR Two-factor authentication code
|
||||
-n, --netrc Use .netrc authentication data
|
||||
--netrc-location PATH Location of .netrc authentication data;
|
||||
either the path or its containing
|
||||
directory. Defaults to ~/.netrc
|
||||
--video-password PASSWORD Video password (vimeo, youku)
|
||||
--ap-mso MSO Adobe Pass multiple-system operator (TV
|
||||
provider) identifier, use --ap-list-mso for
|
||||
@@ -897,7 +903,7 @@ You can configure yt-dlp by placing any supported command line option to a confi
|
||||
* `~/yt-dlp.conf`
|
||||
* `~/yt-dlp.conf.txt`
|
||||
|
||||
Note that `~` points to `C:\Users\<user name>` on windows. Also, `%XDG_CONFIG_HOME%` defaults to `~/.config` if undefined
|
||||
`%XDG_CONFIG_HOME%` defaults to `~/.config` if undefined. On windows, `~` points to %HOME% if present, `%USERPROFILE%` (generally `C:\Users\<user name>`) or `%HOMEDRIVE%%HOMEPATH%`.
|
||||
1. **System Configuration**: `/etc/yt-dlp.conf`
|
||||
|
||||
For example, with the following configuration file yt-dlp will always extract the audio, not copy the mtime, use a proxy and save all videos under `YouTube` directory in your home directory:
|
||||
@@ -923,14 +929,14 @@ You can use `--ignore-config` if you want to disable all configuration files for
|
||||
|
||||
### Authentication with `.netrc` file
|
||||
|
||||
You may also want to configure automatic credentials storage for extractors that support authentication (by providing login and password with `--username` and `--password`) in order not to pass credentials as command line arguments on every yt-dlp execution and prevent tracking plain text passwords in the shell command history. You can achieve this using a [`.netrc` file](https://stackoverflow.com/tags/.netrc/info) on a per extractor basis. For that you will need to create a `.netrc` file in your `$HOME` and restrict permissions to read/write by only you:
|
||||
You may also want to configure automatic credentials storage for extractors that support authentication (by providing login and password with `--username` and `--password`) in order not to pass credentials as command line arguments on every yt-dlp execution and prevent tracking plain text passwords in the shell command history. You can achieve this using a [`.netrc` file](https://stackoverflow.com/tags/.netrc/info) on a per extractor basis. For that you will need to create a `.netrc` file in `--netrc-location` and restrict permissions to read/write by only you:
|
||||
```
|
||||
touch $HOME/.netrc
|
||||
chmod a-rwx,u+rw $HOME/.netrc
|
||||
```
|
||||
After that you can add credentials for an extractor in the following format, where *extractor* is the name of the extractor in lowercase:
|
||||
```
|
||||
machine <extractor> login <login> password <password>
|
||||
machine <extractor> login <username> password <password>
|
||||
```
|
||||
For example:
|
||||
```
|
||||
@@ -939,10 +945,7 @@ machine twitch login my_twitch_account_name password my_twitch_password
|
||||
```
|
||||
To activate authentication with the `.netrc` file you should pass `--netrc` to yt-dlp or place it in the [configuration file](#configuration).
|
||||
|
||||
On Windows you may also need to setup the `%HOME%` environment variable manually. For example:
|
||||
```
|
||||
set HOME=%USERPROFILE%
|
||||
```
|
||||
The default location of the .netrc file is `$HOME` (`~`) in UNIX. On Windows, it is `%HOME%` if present, `%USERPROFILE%` (generally `C:\Users\<user name>`) or `%HOMEDRIVE%%HOMEPATH%`
|
||||
|
||||
# OUTPUT TEMPLATE
|
||||
|
||||
@@ -952,21 +955,22 @@ The `-o` option is used to indicate a template for the output file names while `
|
||||
|
||||
The simplest usage of `-o` is not to set any template arguments when downloading a single file, like in `yt-dlp -o funny_video.flv "https://some/video"` (hard-coding file extension like this is _not_ recommended and could break some post-processing).
|
||||
|
||||
It may however also 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.
|
||||
It may however also 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/3/library/stdtypes.html#printf-style-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.
|
||||
|
||||
The field names themselves (the part inside the parenthesis) can also have some special formatting:
|
||||
1. **Object traversal**: The dictionaries and lists available in metadata can be traversed by using a `.` (dot) separator. You can also do python slicing using `:`. Eg: `%(tags.0)s`, `%(subtitles.en.-1.ext)s`, `%(id.3:7:-1)s`, `%(formats.:.format_id)s`. `%()s` refers to the entire infodict. Note that all the fields that become available using this method are not listed below. Use `-j` to see such fields
|
||||
1. **Addition**: Addition and subtraction of numeric fields can be done using `+` and `-` respectively. Eg: `%(playlist_index+10)03d`, `%(n_entries+1-playlist_index)d`
|
||||
1. **Date/time Formatting**: Date/time fields can be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it separated from the field name using a `>`. Eg: `%(duration>%H-%M-%S)s`, `%(upload_date>%Y-%m-%d)s`, `%(epoch-3600>%H-%M-%S)s`
|
||||
1. **Default**: A default value can be specified for when the field is empty using a `|` seperator. This overrides `--output-na-template`. Eg: `%(uploader|Unknown)s`
|
||||
1. **More Conversions**: In addition to the normal format types `diouxXeEfFgGcrs`, `j`, `l`, `q` can be used for converting to **j**son, a comma seperated **l**ist and a string **q**uoted for the terminal respectively
|
||||
1. **Alternatives**: Alternate fields can be specified seperated with a `,`. Eg: `%(release_date>%Y,upload_date>%Y|Unknown)s`
|
||||
1. **Default**: A literal default value can be specified for when the field is empty using a `|` seperator. This overrides `--output-na-template`. Eg: `%(uploader|Unknown)s`
|
||||
1. **More Conversions**: In addition to the normal format types `diouxXeEfFgGcrs`, `B`, `j`, `l`, `q` can be used for converting to **B**ytes, **j**son, a comma seperated **l**ist and a string **q**uoted for the terminal respectively
|
||||
|
||||
To summarize, the general syntax for a field is:
|
||||
```
|
||||
%(name[.keys][addition][>strf][|default])[flags][width][.precision][length]type
|
||||
%(name[.keys][addition][>strf][,alternate][|default])[flags][width][.precision][length]type
|
||||
```
|
||||
|
||||
Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation`, `infojson`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video.
|
||||
Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation` (deprecated), `infojson`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video.
|
||||
|
||||
The available fields are:
|
||||
|
||||
@@ -1169,7 +1173,11 @@ 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`.
|
||||
|
||||
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. Unless `--video-multistreams` is used, all formats with a video stream except the first one are ignored. Similarly, unless `--audio-multistreams` is used, all formats with an audio stream except the first one are ignored. For example, `-f bestvideo+best+bestaudio --video-multistreams --audio-multistreams` 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.
|
||||
|
||||
**Deprecation warning**: Since the *below* described behavior is complex and counter-intuitive, this will be removed and multistreams will be enabled by default in the future. A new operator will be instead added to limit formats to single audio/video
|
||||
|
||||
Unless `--video-multistreams` is used, all formats with a video stream except the first one are ignored. Similarly, unless `--audio-multistreams` is used, all formats with an audio stream except the first one are ignored. For example, `-f bestvideo+best+bestaudio --video-multistreams --audio-multistreams` 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
|
||||
|
||||
@@ -1235,6 +1243,8 @@ The available fields are:
|
||||
- `abr`: Average audio bitrate in KBit/s
|
||||
- `br`: Equivalent to using `tbr,vbr,abr`
|
||||
- `asr`: Audio sample rate in Hz
|
||||
|
||||
**Deprecation warning**: Many of these fields have (currently undocumented) aliases, that may be removed in a future version. It is recommended to use only the documented field names.
|
||||
|
||||
All fields, unless specified otherwise, are sorted in descending order. To reverse this, prefix the field with a `+`. Eg: `+res` prefers format with the smallest resolution. Additionally, you can suffix a preferred value for the fields, separated 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 preferred 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.
|
||||
|
||||
@@ -1436,7 +1446,7 @@ The following extractors use this feature:
|
||||
* **youtube**
|
||||
* `skip`: `hls` or `dash` (or both) to skip download of the respective manifests
|
||||
* `player_client`: Clients to extract video data from. The main clients are `web`, `android`, `ios`, `mweb`. These also have `_music`, `_embedded`, `_agegate`, and `_creator` variants (Eg: `web_embedded`) (`mweb` has only `_agegate`). By default, `android,web` is used, but the agegate and creator variants are added as required for age-gated videos. Similarly the music variants are added for `music.youtube.com` urls. You can also use `all` to use all the clients
|
||||
* `player_skip`: `configs` - skip any requests for client configs and use defaults
|
||||
* `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause some issues. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) for more details
|
||||
* `include_live_dash`: Include live dash formats (These formats don't download properly)
|
||||
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side).
|
||||
* `max_comments`: Maximum amount of comments to download (default all).
|
||||
|
||||
@@ -15,9 +15,11 @@ import PyInstaller.__main__
|
||||
|
||||
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 ''
|
||||
|
||||
opts = sys.argv[2:] or ['--onefile']
|
||||
print(f'Building {arch}bit version with options {opts}')
|
||||
|
||||
FILE_DESCRIPTION = 'yt-dlp%s' % (' (32 Bit)' if _x86 else '')
|
||||
|
||||
# root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
@@ -72,11 +74,12 @@ excluded_modules = ['test', 'ytdlp_plugins', 'youtube-dl', 'youtube-dlc']
|
||||
|
||||
PyInstaller.__main__.run([
|
||||
'--name=yt-dlp%s' % _x86,
|
||||
'--onefile',
|
||||
'--icon=devscripts/logo.ico',
|
||||
*[f'--exclude-module={module}' for module in excluded_modules],
|
||||
*[f'--hidden-import={module}' for module in dependancies],
|
||||
'--upx-exclude=vcruntime140.dll',
|
||||
'--noconfirm',
|
||||
*opts,
|
||||
'yt_dlp/__main__.py',
|
||||
])
|
||||
SetVersion('dist/yt-dlp%s.exe' % _x86, VERSION_FILE)
|
||||
|
||||
+34
-4
@@ -1,4 +1,6 @@
|
||||
# Supported sites
|
||||
- **17live**
|
||||
- **17live:clip**
|
||||
- **1tv**: Первый канал
|
||||
- **20min**
|
||||
- **220.ro**
|
||||
@@ -50,6 +52,7 @@
|
||||
- **AmericasTestKitchen**
|
||||
- **AmericasTestKitchenSeason**
|
||||
- **anderetijden**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
|
||||
- **AnimalPlanet**
|
||||
- **AnimeLab**
|
||||
- **AnimeLabShows**
|
||||
- **AnimeOnDemand**
|
||||
@@ -125,6 +128,8 @@
|
||||
- **BilibiliChannel**
|
||||
- **BiliBiliPlayer**
|
||||
- **BiliBiliSearch**: Bilibili video search, "bilisearch" keyword
|
||||
- **BiliIntl**
|
||||
- **BiliIntlSeries**
|
||||
- **BioBioChileTV**
|
||||
- **Biography**
|
||||
- **BIQLE**
|
||||
@@ -152,6 +157,7 @@
|
||||
- **BusinessInsider**
|
||||
- **BuzzFeed**
|
||||
- **BYUtv**
|
||||
- **CAM4**
|
||||
- **Camdemy**
|
||||
- **CamdemyFolder**
|
||||
- **CamModels**
|
||||
@@ -164,10 +170,7 @@
|
||||
- **CarambaTVPage**
|
||||
- **CartoonNetwork**
|
||||
- **cbc.ca**
|
||||
- **cbc.ca:olympics**
|
||||
- **cbc.ca:player**
|
||||
- **cbc.ca:watch**
|
||||
- **cbc.ca:watch:video**
|
||||
- **CBS**
|
||||
- **CBSInteractive**
|
||||
- **CBSLocal**
|
||||
@@ -182,10 +185,13 @@
|
||||
- **CDA**
|
||||
- **CeskaTelevize**
|
||||
- **CeskaTelevizePorady**
|
||||
- **CGTN**
|
||||
- **channel9**: Channel 9
|
||||
- **CharlieRose**
|
||||
- **Chaturbate**
|
||||
- **Chilloutzone**
|
||||
- **Chingari**
|
||||
- **ChingariUser**
|
||||
- **chirbit**
|
||||
- **chirbit:profile**
|
||||
- **cielotv.it**
|
||||
@@ -235,6 +241,8 @@
|
||||
- **dailymotion**
|
||||
- **dailymotion:playlist**
|
||||
- **dailymotion:user**
|
||||
- **damtomo:record**
|
||||
- **damtomo:video**
|
||||
- **daum.net**
|
||||
- **daum.net:clip**
|
||||
- **daum.net:playlist**
|
||||
@@ -258,6 +266,7 @@
|
||||
- **DiscoveryPlusIndiaShow**
|
||||
- **DiscoveryVR**
|
||||
- **Disney**
|
||||
- **DIYNetwork**
|
||||
- **dlive:stream**
|
||||
- **dlive:vod**
|
||||
- **DoodStream**
|
||||
@@ -366,6 +375,9 @@
|
||||
- **Gazeta**
|
||||
- **GDCVault**
|
||||
- **GediDigital**
|
||||
- **gem.cbc.ca**
|
||||
- **gem.cbc.ca:live**
|
||||
- **gem.cbc.ca:playlist**
|
||||
- **generic**: Generic downloader that works on some sites
|
||||
- **Gfycat**
|
||||
- **GiantBomb**
|
||||
@@ -381,6 +393,7 @@
|
||||
- **google:podcasts:feed**
|
||||
- **GoogleDrive**
|
||||
- **Goshgay**
|
||||
- **GoToStage**
|
||||
- **GPUTechConf**
|
||||
- **Groupon**
|
||||
- **hbo**
|
||||
@@ -466,6 +479,7 @@
|
||||
- **KinjaEmbed**
|
||||
- **KinoPoisk**
|
||||
- **KonserthusetPlay**
|
||||
- **Koo**
|
||||
- **KrasView**: Красвью
|
||||
- **Ku6**
|
||||
- **KUSI**
|
||||
@@ -539,6 +553,8 @@
|
||||
- **MedalTV**
|
||||
- **media.ccc.de**
|
||||
- **media.ccc.de:lists**
|
||||
- **Mediaite**
|
||||
- **MediaKlikk**
|
||||
- **Medialaan**
|
||||
- **Mediaset**
|
||||
- **Mediasite**
|
||||
@@ -597,6 +613,7 @@
|
||||
- **mtvservices:embedded**
|
||||
- **MTVUutisetArticle**
|
||||
- **MuenchenTV**: münchen.tv
|
||||
- **MuseScore**
|
||||
- **mva**: Microsoft Virtual Academy videos
|
||||
- **mva:course**: Microsoft Virtual Academy courses
|
||||
- **Mwave**
|
||||
@@ -646,7 +663,8 @@
|
||||
- **NetPlus**
|
||||
- **Netzkino**
|
||||
- **Newgrounds**
|
||||
- **NewgroundsPlaylist**
|
||||
- **Newgrounds:playlist**
|
||||
- **Newgrounds:user**
|
||||
- **Newstube**
|
||||
- **NextMedia**: 蘋果日報
|
||||
- **NextMediaActionNews**: 蘋果日報 - 動新聞
|
||||
@@ -707,11 +725,13 @@
|
||||
- **NYTimes**
|
||||
- **NYTimesArticle**
|
||||
- **NYTimesCooking**
|
||||
- **nzherald**
|
||||
- **NZZ**
|
||||
- **ocw.mit.edu**
|
||||
- **OdaTV**
|
||||
- **Odnoklassniki**
|
||||
- **OktoberfestTV**
|
||||
- **OlympicsReplay**
|
||||
- **OnDemandKorea**
|
||||
- **onet.pl**
|
||||
- **onet.tv**
|
||||
@@ -756,6 +776,7 @@
|
||||
- **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)
|
||||
- **PearVideo**
|
||||
- **PeerTube**
|
||||
- **PeerTube:Playlist**
|
||||
- **peloton**
|
||||
- **peloton:live**: Peloton Live
|
||||
- **People**
|
||||
@@ -831,6 +852,9 @@
|
||||
- **radiocanada:audiovideo**
|
||||
- **radiofrance**
|
||||
- **RadioJavan**
|
||||
- **radlive**
|
||||
- **radlive:channel**
|
||||
- **radlive:season**
|
||||
- **Rai**
|
||||
- **RaiPlay**
|
||||
- **RaiPlayLive**
|
||||
@@ -955,6 +979,8 @@
|
||||
- **southpark.de**
|
||||
- **southpark.nl**
|
||||
- **southparkstudios.dk**
|
||||
- **SovietsCloset**
|
||||
- **SovietsClosetPlaylist**
|
||||
- **SpankBang**
|
||||
- **SpankBangPlaylist**
|
||||
- **Spankwire**
|
||||
@@ -983,6 +1009,7 @@
|
||||
- **StoryFireSeries**
|
||||
- **StoryFireUser**
|
||||
- **Streamable**
|
||||
- **Streamanity**
|
||||
- **streamcloud.eu**
|
||||
- **StreamCZ**
|
||||
- **StreetVoice**
|
||||
@@ -1038,6 +1065,7 @@
|
||||
- **TheScene**
|
||||
- **TheStar**
|
||||
- **TheSun**
|
||||
- **Theta**
|
||||
- **TheWeatherChannel**
|
||||
- **ThisAmericanLife**
|
||||
- **ThisAV**
|
||||
@@ -1325,6 +1353,8 @@
|
||||
- **ZDFChannel**
|
||||
- **Zee5**
|
||||
- **zee5:series**
|
||||
- **ZenYandex**
|
||||
- **ZenYandexChannel**
|
||||
- **Zhihu**
|
||||
- **zingmp3**: mp3.zing.vn
|
||||
- **zingmp3:album**
|
||||
|
||||
@@ -649,6 +649,7 @@ class TestYoutubeDL(unittest.TestCase):
|
||||
'title2': '%PATH%',
|
||||
'title3': 'foo/bar\\test',
|
||||
'title4': 'foo "bar" test',
|
||||
'title5': 'áéí',
|
||||
'timestamp': 1618488000,
|
||||
'duration': 100000,
|
||||
'playlist_index': 1,
|
||||
@@ -767,6 +768,7 @@ class TestYoutubeDL(unittest.TestCase):
|
||||
test('%(ext)l', 'mp4')
|
||||
test('%(formats.:.id) 15l', ' id1, id2, id3')
|
||||
test('%(formats)j', (json.dumps(FORMATS), sanitize(json.dumps(FORMATS))))
|
||||
test('%(title5).3B', 'á')
|
||||
if compat_os_name == 'nt':
|
||||
test('%(title4)q', ('"foo \\"bar\\" test"', "'foo _'bar_' test'"))
|
||||
else:
|
||||
@@ -788,6 +790,12 @@ class TestYoutubeDL(unittest.TestCase):
|
||||
test('%(formats.0.id.-1+id)f', '1235.000000')
|
||||
test('%(formats.0.id.-1+formats.1.id.-1)d', '3')
|
||||
|
||||
# Alternates
|
||||
test('%(title,id)s', '1234')
|
||||
test('%(width-100,height+20|def)d', '1100')
|
||||
test('%(width-100,height+width|def)s', 'def')
|
||||
test('%(timestamp-x>%H\\,%M\\,%S,timestamp>%H\\,%M\\,%S)s', '12,00,00')
|
||||
|
||||
# Laziness
|
||||
def gen():
|
||||
yield from range(5)
|
||||
@@ -992,6 +1000,7 @@ class TestYoutubeDL(unittest.TestCase):
|
||||
test_selection({'playlist_items': '2-4'}, [2, 3, 4])
|
||||
test_selection({'playlist_items': '2,4'}, [2, 4])
|
||||
test_selection({'playlist_items': '10'}, [])
|
||||
test_selection({'playlist_items': '0'}, [])
|
||||
|
||||
# Tests for https://github.com/ytdl-org/youtube-dl/issues/10591
|
||||
test_selection({'playlist_items': '2-4,3-4,3'}, [2, 3, 4])
|
||||
|
||||
+43
-6
@@ -7,7 +7,19 @@ import sys
|
||||
import unittest
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from yt_dlp.aes import aes_decrypt, aes_encrypt, aes_cbc_decrypt, aes_cbc_encrypt, aes_decrypt_text
|
||||
from yt_dlp.aes import (
|
||||
aes_decrypt,
|
||||
aes_encrypt,
|
||||
aes_cbc_decrypt,
|
||||
aes_cbc_decrypt_bytes,
|
||||
aes_cbc_encrypt,
|
||||
aes_ctr_decrypt,
|
||||
aes_ctr_encrypt,
|
||||
aes_gcm_decrypt_and_verify,
|
||||
aes_gcm_decrypt_and_verify_bytes,
|
||||
aes_decrypt_text
|
||||
)
|
||||
from yt_dlp.compat import compat_pycrypto_AES
|
||||
from yt_dlp.utils import bytes_to_intlist, intlist_to_bytes
|
||||
import base64
|
||||
|
||||
@@ -27,18 +39,43 @@ class TestAES(unittest.TestCase):
|
||||
self.assertEqual(decrypted, msg)
|
||||
|
||||
def test_cbc_decrypt(self):
|
||||
data = bytes_to_intlist(
|
||||
b"\x97\x92+\xe5\x0b\xc3\x18\x91ky9m&\xb3\xb5@\xe6'\xc2\x96.\xc8u\x88\xab9-[\x9e|\xf1\xcd"
|
||||
)
|
||||
decrypted = intlist_to_bytes(aes_cbc_decrypt(data, self.key, self.iv))
|
||||
data = b'\x97\x92+\xe5\x0b\xc3\x18\x91ky9m&\xb3\xb5@\xe6\x27\xc2\x96.\xc8u\x88\xab9-[\x9e|\xf1\xcd'
|
||||
decrypted = intlist_to_bytes(aes_cbc_decrypt(bytes_to_intlist(data), self.key, self.iv))
|
||||
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||
if compat_pycrypto_AES:
|
||||
decrypted = aes_cbc_decrypt_bytes(data, intlist_to_bytes(self.key), intlist_to_bytes(self.iv))
|
||||
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||
|
||||
def test_cbc_encrypt(self):
|
||||
data = bytes_to_intlist(self.secret_msg)
|
||||
encrypted = intlist_to_bytes(aes_cbc_encrypt(data, self.key, self.iv))
|
||||
self.assertEqual(
|
||||
encrypted,
|
||||
b"\x97\x92+\xe5\x0b\xc3\x18\x91ky9m&\xb3\xb5@\xe6'\xc2\x96.\xc8u\x88\xab9-[\x9e|\xf1\xcd")
|
||||
b'\x97\x92+\xe5\x0b\xc3\x18\x91ky9m&\xb3\xb5@\xe6\'\xc2\x96.\xc8u\x88\xab9-[\x9e|\xf1\xcd')
|
||||
|
||||
def test_ctr_decrypt(self):
|
||||
data = bytes_to_intlist(b'\x03\xc7\xdd\xd4\x8e\xb3\xbc\x1a*O\xdc1\x12+8Aio\xd1z\xb5#\xaf\x08')
|
||||
decrypted = intlist_to_bytes(aes_ctr_decrypt(data, self.key, self.iv))
|
||||
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||
|
||||
def test_ctr_encrypt(self):
|
||||
data = bytes_to_intlist(self.secret_msg)
|
||||
encrypted = intlist_to_bytes(aes_ctr_encrypt(data, self.key, self.iv))
|
||||
self.assertEqual(
|
||||
encrypted,
|
||||
b'\x03\xc7\xdd\xd4\x8e\xb3\xbc\x1a*O\xdc1\x12+8Aio\xd1z\xb5#\xaf\x08')
|
||||
|
||||
def test_gcm_decrypt(self):
|
||||
data = b'\x159Y\xcf5eud\x90\x9c\x85&]\x14\x1d\x0f.\x08\xb4T\xe4/\x17\xbd'
|
||||
authentication_tag = b'\xe8&I\x80rI\x07\x9d}YWuU@:e'
|
||||
|
||||
decrypted = intlist_to_bytes(aes_gcm_decrypt_and_verify(
|
||||
bytes_to_intlist(data), self.key, bytes_to_intlist(authentication_tag), self.iv[:12]))
|
||||
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||
if compat_pycrypto_AES:
|
||||
decrypted = aes_gcm_decrypt_and_verify_bytes(
|
||||
data, intlist_to_bytes(self.key), authentication_tag, intlist_to_bytes(self.iv[:12]))
|
||||
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||
|
||||
def test_decrypt_text(self):
|
||||
password = intlist_to_bytes(self.key).decode('utf-8')
|
||||
|
||||
+18
-7
@@ -3,16 +3,28 @@ from datetime import datetime, timezone
|
||||
|
||||
from yt_dlp import cookies
|
||||
from yt_dlp.cookies import (
|
||||
CRYPTO_AVAILABLE,
|
||||
LinuxChromeCookieDecryptor,
|
||||
MacChromeCookieDecryptor,
|
||||
WindowsChromeCookieDecryptor,
|
||||
YDLLogger,
|
||||
parse_safari_cookies,
|
||||
pbkdf2_sha1,
|
||||
)
|
||||
|
||||
|
||||
class Logger:
|
||||
def debug(self, message):
|
||||
print(f'[verbose] {message}')
|
||||
|
||||
def info(self, message):
|
||||
print(message)
|
||||
|
||||
def warning(self, message, only_once=False):
|
||||
self.error(message)
|
||||
|
||||
def error(self, message):
|
||||
raise Exception(message)
|
||||
|
||||
|
||||
class MonkeyPatch:
|
||||
def __init__(self, module, temporary_values):
|
||||
self._module = module
|
||||
@@ -42,7 +54,7 @@ class TestCookies(unittest.TestCase):
|
||||
with MonkeyPatch(cookies, {'_get_linux_keyring_password': lambda *args, **kwargs: b''}):
|
||||
encrypted_value = b'v10\xccW%\xcd\xe6\xe6\x9fM" \xa7\xb0\xca\xe4\x07\xd6'
|
||||
value = 'USD'
|
||||
decryptor = LinuxChromeCookieDecryptor('Chrome', YDLLogger())
|
||||
decryptor = LinuxChromeCookieDecryptor('Chrome', Logger())
|
||||
self.assertEqual(decryptor.decrypt(encrypted_value), value)
|
||||
|
||||
def test_chrome_cookie_decryptor_linux_v11(self):
|
||||
@@ -50,24 +62,23 @@ class TestCookies(unittest.TestCase):
|
||||
'KEYRING_AVAILABLE': True}):
|
||||
encrypted_value = b'v11#\x81\x10>`w\x8f)\xc0\xb2\xc1\r\xf4\x1al\xdd\x93\xfd\xf8\xf8N\xf2\xa9\x83\xf1\xe9o\x0elVQd'
|
||||
value = 'tz=Europe.London'
|
||||
decryptor = LinuxChromeCookieDecryptor('Chrome', YDLLogger())
|
||||
decryptor = LinuxChromeCookieDecryptor('Chrome', Logger())
|
||||
self.assertEqual(decryptor.decrypt(encrypted_value), value)
|
||||
|
||||
@unittest.skipIf(not CRYPTO_AVAILABLE, 'cryptography library not available')
|
||||
def test_chrome_cookie_decryptor_windows_v10(self):
|
||||
with MonkeyPatch(cookies, {
|
||||
'_get_windows_v10_key': lambda *args, **kwargs: b'Y\xef\xad\xad\xeerp\xf0Y\xe6\x9b\x12\xc2<z\x16]\n\xbb\xb8\xcb\xd7\x9bA\xc3\x14e\x99{\xd6\xf4&'
|
||||
}):
|
||||
encrypted_value = b'v10T\xb8\xf3\xb8\x01\xa7TtcV\xfc\x88\xb8\xb8\xef\x05\xb5\xfd\x18\xc90\x009\xab\xb1\x893\x85)\x87\xe1\xa9-\xa3\xad='
|
||||
value = '32101439'
|
||||
decryptor = WindowsChromeCookieDecryptor('', YDLLogger())
|
||||
decryptor = WindowsChromeCookieDecryptor('', Logger())
|
||||
self.assertEqual(decryptor.decrypt(encrypted_value), value)
|
||||
|
||||
def test_chrome_cookie_decryptor_mac_v10(self):
|
||||
with MonkeyPatch(cookies, {'_get_mac_keyring_password': lambda *args, **kwargs: b'6eIDUdtKAacvlHwBVwvg/Q=='}):
|
||||
encrypted_value = b'v10\xb3\xbe\xad\xa1[\x9fC\xa1\x98\xe0\x9a\x01\xd9\xcf\xbfc'
|
||||
value = '2021-06-01-22'
|
||||
decryptor = MacChromeCookieDecryptor('', YDLLogger())
|
||||
decryptor = MacChromeCookieDecryptor('', Logger())
|
||||
self.assertEqual(decryptor.decrypt(encrypted_value), value)
|
||||
|
||||
def test_safari_cookie_parsing(self):
|
||||
|
||||
@@ -461,11 +461,23 @@ class TestModifyChaptersPP(unittest.TestCase):
|
||||
self._remove_marked_arrange_sponsors_test_impl(
|
||||
chapters, self._chapters([2, 2.5], ['c1', 'c3']), cuts)
|
||||
|
||||
def test_remove_marked_arrange_sponsors_SingleTinyChapterIsPreserved(self):
|
||||
cuts = [self._chapter(0.5, 2, remove=True)]
|
||||
chapters = self._chapters([2], ['c']) + cuts
|
||||
self._remove_marked_arrange_sponsors_test_impl(
|
||||
chapters, self._chapters([0.5], ['c']), cuts)
|
||||
|
||||
def test_remove_marked_arrange_sponsors_TinyChapterAtTheStartPrependedToTheNext(self):
|
||||
cuts = [self._chapter(0.5, 2, remove=True)]
|
||||
chapters = self._chapters([2, 4], ['c1', 'c2']) + cuts
|
||||
self._remove_marked_arrange_sponsors_test_impl(
|
||||
chapters, self._chapters([2.5], ['c2']), cuts)
|
||||
|
||||
def test_remove_marked_arrange_sponsors_TinyChaptersResultingFromSponsorOverlapAreIgnored(self):
|
||||
chapters = self._chapters([1, 3, 4], ['c1', 'c2', 'c3']) + [
|
||||
self._sponsor_chapter(1.5, 2.5, 'sponsor')]
|
||||
self._remove_marked_arrange_sponsors_test_impl(
|
||||
chapters, self._chapters([1.5, 3, 4], ['c1', '[SponsorBlock]: Sponsor', 'c3']), [])
|
||||
chapters, self._chapters([1.5, 2.5, 4], ['c1', '[SponsorBlock]: Sponsor', 'c3']), [])
|
||||
|
||||
def test_remove_marked_arrange_sponsors_TinySponsorsOverlapsAreIgnored(self):
|
||||
chapters = self._chapters([2, 3, 5], ['c1', 'c2', 'c3']) + [
|
||||
@@ -476,6 +488,26 @@ class TestModifyChaptersPP(unittest.TestCase):
|
||||
chapters, self._chapters([1, 3, 4, 5], [
|
||||
'c1', '[SponsorBlock]: Sponsor', '[SponsorBlock]: Unpaid/Self Promotion', 'c3']), [])
|
||||
|
||||
def test_remove_marked_arrange_sponsors_TinySponsorsPrependedToTheNextSponsor(self):
|
||||
chapters = self._chapters([4], ['c']) + [
|
||||
self._sponsor_chapter(1.5, 2, 'sponsor'),
|
||||
self._sponsor_chapter(2, 4, 'selfpromo')
|
||||
]
|
||||
self._remove_marked_arrange_sponsors_test_impl(
|
||||
chapters, self._chapters([1.5, 4], ['c', '[SponsorBlock]: Unpaid/Self Promotion']), [])
|
||||
|
||||
def test_remove_marked_arrange_sponsors_SmallestSponsorInTheOverlapGetsNamed(self):
|
||||
self._pp._sponsorblock_chapter_title = '[SponsorBlock]: %(name)s'
|
||||
chapters = self._chapters([10], ['c']) + [
|
||||
self._sponsor_chapter(2, 8, 'sponsor'),
|
||||
self._sponsor_chapter(4, 6, 'selfpromo')
|
||||
]
|
||||
self._remove_marked_arrange_sponsors_test_impl(
|
||||
chapters, self._chapters([2, 4, 6, 8, 10], [
|
||||
'c', '[SponsorBlock]: Sponsor', '[SponsorBlock]: Unpaid/Self Promotion',
|
||||
'[SponsorBlock]: Sponsor', 'c'
|
||||
]), [])
|
||||
|
||||
def test_make_concat_opts_CommonCase(self):
|
||||
sponsor_chapters = [self._chapter(1, 2, 's1'), self._chapter(10, 20, 's2')]
|
||||
expected = '''ffconcat version 1.0
|
||||
|
||||
@@ -19,6 +19,7 @@ from yt_dlp.extractor import (
|
||||
CeskaTelevizeIE,
|
||||
LyndaIE,
|
||||
NPOIE,
|
||||
PBSIE,
|
||||
ComedyCentralIE,
|
||||
NRKTVIE,
|
||||
RaiPlayIE,
|
||||
@@ -372,5 +373,42 @@ class TestDemocracynowSubtitles(BaseTestSubtitles):
|
||||
self.assertEqual(md5(subtitles['en']), 'acaca989e24a9e45a6719c9b3d60815c')
|
||||
|
||||
|
||||
@is_download_test
|
||||
class TestPBSSubtitles(BaseTestSubtitles):
|
||||
url = 'https://www.pbs.org/video/how-fantasy-reflects-our-world-picecq/'
|
||||
IE = PBSIE
|
||||
|
||||
def test_allsubtitles(self):
|
||||
self.DL.params['writesubtitles'] = True
|
||||
self.DL.params['allsubtitles'] = True
|
||||
subtitles = self.getSubtitles()
|
||||
self.assertEqual(set(subtitles.keys()), set(['en']))
|
||||
|
||||
def test_subtitles_dfxp_format(self):
|
||||
self.DL.params['writesubtitles'] = True
|
||||
self.DL.params['subtitlesformat'] = 'dfxp'
|
||||
subtitles = self.getSubtitles()
|
||||
self.assertIn(md5(subtitles['en']), ['643b034254cdc3768ff1e750b6b5873b'])
|
||||
|
||||
def test_subtitles_vtt_format(self):
|
||||
self.DL.params['writesubtitles'] = True
|
||||
self.DL.params['subtitlesformat'] = 'vtt'
|
||||
subtitles = self.getSubtitles()
|
||||
self.assertIn(
|
||||
md5(subtitles['en']), ['937a05711555b165d4c55a9667017045', 'f49ea998d6824d94959c8152a368ff73'])
|
||||
|
||||
def test_subtitles_srt_format(self):
|
||||
self.DL.params['writesubtitles'] = True
|
||||
self.DL.params['subtitlesformat'] = 'srt'
|
||||
subtitles = self.getSubtitles()
|
||||
self.assertIn(md5(subtitles['en']), ['2082c21b43759d9bf172931b2f2ca371'])
|
||||
|
||||
def test_subtitles_sami_format(self):
|
||||
self.DL.params['writesubtitles'] = True
|
||||
self.DL.params['subtitlesformat'] = 'sami'
|
||||
subtitles = self.getSubtitles()
|
||||
self.assertIn(md5(subtitles['en']), ['4256b16ac7da6a6780fafd04294e85cd'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
+82
-49
@@ -27,7 +27,6 @@ import traceback
|
||||
import random
|
||||
|
||||
from string import ascii_letters
|
||||
from zipimport import zipimporter
|
||||
|
||||
from .compat import (
|
||||
compat_basestring,
|
||||
@@ -35,6 +34,7 @@ from .compat import (
|
||||
compat_kwargs,
|
||||
compat_numeric_types,
|
||||
compat_os_name,
|
||||
compat_pycrypto_AES,
|
||||
compat_shlex_quote,
|
||||
compat_str,
|
||||
compat_tokenize_tokenize,
|
||||
@@ -142,6 +142,7 @@ from .postprocessor import (
|
||||
FFmpegPostProcessor,
|
||||
MoveFilesAfterDownloadPP,
|
||||
)
|
||||
from .update import detect_variant
|
||||
from .version import __version__
|
||||
|
||||
if compat_os_name == 'nt':
|
||||
@@ -225,9 +226,9 @@ class YoutubeDL(object):
|
||||
restrictfilenames: Do not allow "&" and spaces in file names
|
||||
trim_file_name: Limit length of filename (extension excluded)
|
||||
windowsfilenames: Force the filenames to be windows compatible
|
||||
ignoreerrors: Do not stop on download errors
|
||||
(Default True when running yt-dlp,
|
||||
but False when directly accessing YoutubeDL class)
|
||||
ignoreerrors: Do not stop on download/postprocessing errors.
|
||||
Can be 'only_download' to ignore only download errors.
|
||||
Default is 'only_download' for CLI, but False for API
|
||||
skip_playlist_after_errors: Number of allowed failures until the rest of
|
||||
the playlist is skipped
|
||||
force_generic_extractor: Force downloader to use the generic extractor
|
||||
@@ -775,7 +776,7 @@ class YoutubeDL(object):
|
||||
tb = ''.join(tb_data)
|
||||
if tb:
|
||||
self.to_stderr(tb)
|
||||
if not self.params.get('ignoreerrors', False):
|
||||
if not self.params.get('ignoreerrors'):
|
||||
if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
|
||||
exc_info = sys.exc_info()[1].exc_info
|
||||
else:
|
||||
@@ -907,7 +908,7 @@ class YoutubeDL(object):
|
||||
def validate_outtmpl(cls, outtmpl):
|
||||
''' @return None or Exception object '''
|
||||
outtmpl = re.sub(
|
||||
STR_FORMAT_RE_TMPL.format('[^)]*', '[ljq]'),
|
||||
STR_FORMAT_RE_TMPL.format('[^)]*', '[ljqB]'),
|
||||
lambda mobj: f'{mobj.group(0)[:-1]}s',
|
||||
cls._outtmpl_expandpath(outtmpl))
|
||||
try:
|
||||
@@ -939,7 +940,7 @@ class YoutubeDL(object):
|
||||
}
|
||||
|
||||
TMPL_DICT = {}
|
||||
EXTERNAL_FORMAT_RE = re.compile(STR_FORMAT_RE_TMPL.format('[^)]*', f'[{STR_FORMAT_TYPES}ljq]'))
|
||||
EXTERNAL_FORMAT_RE = re.compile(STR_FORMAT_RE_TMPL.format('[^)]*', f'[{STR_FORMAT_TYPES}ljqB]'))
|
||||
MATH_FUNCTIONS = {
|
||||
'+': float.__add__,
|
||||
'-': float.__sub__,
|
||||
@@ -954,6 +955,7 @@ class YoutubeDL(object):
|
||||
(?P<fields>{field})
|
||||
(?P<maths>(?:{math_op}{math_field})*)
|
||||
(?:>(?P<strf_format>.+?))?
|
||||
(?P<alternate>(?<!\\),[^|)]+)?
|
||||
(?:\|(?P<default>.*?))?
|
||||
$'''.format(field=FIELD_RE, math_op=MATH_OPERATORS_RE, math_field=MATH_FIELD_RE))
|
||||
|
||||
@@ -995,7 +997,7 @@ class YoutubeDL(object):
|
||||
operator = None
|
||||
# Datetime formatting
|
||||
if mdict['strf_format']:
|
||||
value = strftime_or_none(value, mdict['strf_format'])
|
||||
value = strftime_or_none(value, mdict['strf_format'].replace('\\,', ','))
|
||||
|
||||
return value
|
||||
|
||||
@@ -1011,12 +1013,16 @@ class YoutubeDL(object):
|
||||
return f'%{outer_mobj.group(0)}'
|
||||
key = outer_mobj.group('key')
|
||||
mobj = re.match(INTERNAL_FORMAT_RE, key)
|
||||
if mobj is None:
|
||||
value, default, mobj = None, na, {'fields': ''}
|
||||
else:
|
||||
initial_field = mobj.group('fields').split('.')[-1] if mobj else ''
|
||||
value, default = None, na
|
||||
while mobj:
|
||||
mobj = mobj.groupdict()
|
||||
default = mobj['default'] if mobj['default'] is not None else na
|
||||
default = mobj['default'] if mobj['default'] is not None else default
|
||||
value = get_value(mobj)
|
||||
if value is None and mobj['alternate']:
|
||||
mobj = re.match(INTERNAL_FORMAT_RE, mobj['alternate'][1:])
|
||||
else:
|
||||
break
|
||||
|
||||
fmt = outer_mobj.group('format')
|
||||
if fmt == 's' and value is not None and key in field_size_compat_map.keys():
|
||||
@@ -1031,6 +1037,9 @@ class YoutubeDL(object):
|
||||
value, fmt = json.dumps(value, default=_dumpjson_default), str_fmt
|
||||
elif fmt[-1] == 'q':
|
||||
value, fmt = compat_shlex_quote(str(value)), str_fmt
|
||||
elif fmt[-1] == 'B':
|
||||
value = f'%{str_fmt}'.encode('utf-8') % str(value).encode('utf-8')
|
||||
value, fmt = value.decode('utf-8', 'ignore'), 's'
|
||||
elif fmt[-1] == 'c':
|
||||
value = str(value)
|
||||
if value is None:
|
||||
@@ -1048,7 +1057,7 @@ class YoutubeDL(object):
|
||||
# So we convert it to repr first
|
||||
value, fmt = repr(value), str_fmt
|
||||
if fmt[-1] in 'csr':
|
||||
value = sanitize(mobj['fields'].split('.')[-1], value)
|
||||
value = sanitize(initial_field, value)
|
||||
|
||||
key = '%s\0%s' % (key.replace('%', '%\0'), outer_mobj.group('format'))
|
||||
TMPL_DICT[key] = value
|
||||
@@ -1166,7 +1175,7 @@ class YoutubeDL(object):
|
||||
for key, value in extra_info.items():
|
||||
info_dict.setdefault(key, value)
|
||||
|
||||
def extract_info(self, url, download=True, ie_key=None, extra_info={},
|
||||
def extract_info(self, url, download=True, ie_key=None, extra_info=None,
|
||||
process=True, force_generic_extractor=False):
|
||||
"""
|
||||
Return a list with a dictionary for each video extracted.
|
||||
@@ -1183,6 +1192,9 @@ class YoutubeDL(object):
|
||||
force_generic_extractor -- force using the generic extractor
|
||||
"""
|
||||
|
||||
if extra_info is None:
|
||||
extra_info = {}
|
||||
|
||||
if not ie_key and force_generic_extractor:
|
||||
ie_key = 'Generic'
|
||||
|
||||
@@ -1208,7 +1220,8 @@ class YoutubeDL(object):
|
||||
else:
|
||||
self.report_error('no suitable InfoExtractor for URL %s' % url)
|
||||
|
||||
def __handle_extraction_exceptions(func, handle_all_errors=True):
|
||||
def __handle_extraction_exceptions(func):
|
||||
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
@@ -1225,10 +1238,10 @@ class YoutubeDL(object):
|
||||
self.to_stderr('\r')
|
||||
self.report_warning('The download speed is below throttle limit. Re-extracting data')
|
||||
return wrapper(self, *args, **kwargs)
|
||||
except (MaxDownloadsReached, ExistingVideoReached, RejectedVideoReached):
|
||||
except (MaxDownloadsReached, ExistingVideoReached, RejectedVideoReached, LazyList.IndexError):
|
||||
raise
|
||||
except Exception as e:
|
||||
if handle_all_errors and self.params.get('ignoreerrors', False):
|
||||
if self.params.get('ignoreerrors'):
|
||||
self.report_error(error_to_compat_str(e), tb=encode_compat_str(traceback.format_exc()))
|
||||
else:
|
||||
raise
|
||||
@@ -1287,10 +1300,14 @@ class YoutubeDL(object):
|
||||
if ((extract_flat == 'in_playlist' and 'playlist' in extra_info)
|
||||
or extract_flat is True):
|
||||
info_copy = ie_result.copy()
|
||||
self.add_extra_info(info_copy, extra_info)
|
||||
ie = try_get(ie_result.get('ie_key'), self.get_info_extractor)
|
||||
if not ie_result.get('id'):
|
||||
info_copy['id'] = ie.get_temp_id(ie_result['url'])
|
||||
self.add_default_extra_info(info_copy, ie, ie_result['url'])
|
||||
self.add_extra_info(info_copy, extra_info)
|
||||
self.__forced_printings(info_copy, self.prepare_filename(info_copy), incomplete=True)
|
||||
if self.params.get('force_write_download_archive', False):
|
||||
self.record_download_archive(info_copy)
|
||||
return ie_result
|
||||
|
||||
if result_type == 'video':
|
||||
@@ -1433,17 +1450,24 @@ class YoutubeDL(object):
|
||||
msg = (
|
||||
'Downloading %d videos' if not isinstance(ie_entries, list)
|
||||
else 'Collected %d videos; downloading %%d of them' % len(ie_entries))
|
||||
if not isinstance(ie_entries, (list, PagedList)):
|
||||
ie_entries = LazyList(ie_entries)
|
||||
|
||||
def get_entry(i):
|
||||
return YoutubeDL.__handle_extraction_exceptions(
|
||||
lambda self, i: ie_entries[i - 1],
|
||||
False
|
||||
)(self, i)
|
||||
if isinstance(ie_entries, list):
|
||||
def get_entry(i):
|
||||
return ie_entries[i - 1]
|
||||
else:
|
||||
if not isinstance(ie_entries, PagedList):
|
||||
ie_entries = LazyList(ie_entries)
|
||||
|
||||
def get_entry(i):
|
||||
return YoutubeDL.__handle_extraction_exceptions(
|
||||
lambda self, i: ie_entries[i - 1]
|
||||
)(self, i)
|
||||
|
||||
entries = []
|
||||
for i in playlistitems or itertools.count(playliststart):
|
||||
items = playlistitems if playlistitems is not None else itertools.count(playliststart)
|
||||
for i in items:
|
||||
if i == 0:
|
||||
continue
|
||||
if playlistitems is None and playlistend is not None and playlistend < i:
|
||||
break
|
||||
entry = None
|
||||
@@ -1531,8 +1555,8 @@ class YoutubeDL(object):
|
||||
max_failures = self.params.get('skip_playlist_after_errors') or float('inf')
|
||||
for i, entry_tuple in enumerate(entries, 1):
|
||||
playlist_index, entry = entry_tuple
|
||||
if 'playlist-index' in self.params.get('compat_options', []):
|
||||
playlist_index = playlistitems[i - 1] if playlistitems else i
|
||||
if 'playlist-index' in self.params.get('compat_opts', []):
|
||||
playlist_index = playlistitems[i - 1] if playlistitems else i + playliststart - 1
|
||||
self.to_screen('[download] Downloading video %s of %s' % (i, n_entries))
|
||||
# This __x_forwarded_for_ip thing is a bit ugly but requires
|
||||
# minimal changes
|
||||
@@ -2342,20 +2366,24 @@ class YoutubeDL(object):
|
||||
if self.params.get('allsubtitles', False):
|
||||
requested_langs = all_sub_langs
|
||||
elif self.params.get('subtitleslangs', False):
|
||||
requested_langs = set()
|
||||
for lang in self.params.get('subtitleslangs'):
|
||||
if lang == 'all':
|
||||
requested_langs.update(all_sub_langs)
|
||||
# A list is used so that the order of languages will be the same as
|
||||
# given in subtitleslangs. See https://github.com/yt-dlp/yt-dlp/issues/1041
|
||||
requested_langs = []
|
||||
for lang_re in self.params.get('subtitleslangs'):
|
||||
if lang_re == 'all':
|
||||
requested_langs.extend(all_sub_langs)
|
||||
continue
|
||||
discard = lang[0] == '-'
|
||||
discard = lang_re[0] == '-'
|
||||
if discard:
|
||||
lang = lang[1:]
|
||||
current_langs = filter(re.compile(lang + '$').match, all_sub_langs)
|
||||
lang_re = lang_re[1:]
|
||||
current_langs = filter(re.compile(lang_re + '$').match, all_sub_langs)
|
||||
if discard:
|
||||
for lang in current_langs:
|
||||
requested_langs.discard(lang)
|
||||
while lang in requested_langs:
|
||||
requested_langs.remove(lang)
|
||||
else:
|
||||
requested_langs.update(current_langs)
|
||||
requested_langs.extend(current_langs)
|
||||
requested_langs = orderedSet(requested_langs)
|
||||
elif 'en' in available_subs:
|
||||
requested_langs = ['en']
|
||||
else:
|
||||
@@ -2573,7 +2601,9 @@ class YoutubeDL(object):
|
||||
return
|
||||
else:
|
||||
try:
|
||||
self.dl(sub_filename, sub_info.copy(), subtitle=True)
|
||||
sub_copy = sub_info.copy()
|
||||
sub_copy.setdefault('http_headers', info_dict.get('http_headers'))
|
||||
self.dl(sub_filename, sub_copy, subtitle=True)
|
||||
sub_info['filepath'] = sub_filename
|
||||
files_to_move[sub_filename] = sub_filename_final
|
||||
except (ExtractorError, IOError, OSError, ValueError) + network_exceptions as err:
|
||||
@@ -2738,7 +2768,7 @@ class YoutubeDL(object):
|
||||
_protocols = set(determine_protocol(f) for f in requested_formats)
|
||||
if len(_protocols) == 1: # All requested formats have same protocol
|
||||
info_dict['protocol'] = _protocols.pop()
|
||||
directly_mergable = FFmpegFD.can_merge_formats(info_dict)
|
||||
directly_mergable = FFmpegFD.can_merge_formats(info_dict, self.params)
|
||||
if dl_filename is not None:
|
||||
self.report_file_already_downloaded(dl_filename)
|
||||
elif (directly_mergable and get_suitable_downloader(
|
||||
@@ -2777,6 +2807,7 @@ class YoutubeDL(object):
|
||||
'f%s' % f['format_id'], new_info['ext'])
|
||||
if not self._ensure_dir_exists(fname):
|
||||
return
|
||||
f['filepath'] = fname
|
||||
downloaded.append(fname)
|
||||
partial_success, real_download = self.dl(fname, new_info)
|
||||
info_dict['__real_download'] = info_dict['__real_download'] or real_download
|
||||
@@ -2961,10 +2992,17 @@ class YoutubeDL(object):
|
||||
files_to_delete = []
|
||||
if '__files_to_move' not in infodict:
|
||||
infodict['__files_to_move'] = {}
|
||||
files_to_delete, infodict = pp.run(infodict)
|
||||
try:
|
||||
files_to_delete, infodict = pp.run(infodict)
|
||||
except PostProcessingError as e:
|
||||
# Must be True and not 'only_download'
|
||||
if self.params.get('ignoreerrors') is True:
|
||||
self.report_error(e)
|
||||
return infodict
|
||||
raise
|
||||
|
||||
if not files_to_delete:
|
||||
return infodict
|
||||
|
||||
if self.params.get('keepvideo', False):
|
||||
for f in files_to_delete:
|
||||
infodict['__files_to_move'].setdefault(f, '')
|
||||
@@ -3231,12 +3269,8 @@ class YoutubeDL(object):
|
||||
self.get_encoding()))
|
||||
write_string(encoding_str, encoding=None)
|
||||
|
||||
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))
|
||||
source = detect_variant()
|
||||
self._write_string('[debug] yt-dlp version %s%s\n' % (__version__, '' if source == 'unknown' else f' ({source})'))
|
||||
if _LAZY_LOADER:
|
||||
self._write_string('[debug] Lazy loading extractors enabled\n')
|
||||
if _PLUGIN_CLASSES:
|
||||
@@ -3280,13 +3314,12 @@ class YoutubeDL(object):
|
||||
) or 'none'
|
||||
self._write_string('[debug] exe versions: %s\n' % exe_str)
|
||||
|
||||
from .downloader.fragment import can_decrypt_frag
|
||||
from .downloader.websocket import has_websockets
|
||||
from .postprocessor.embedthumbnail import has_mutagen
|
||||
from .cookies import SQLITE_AVAILABLE, KEYRING_AVAILABLE
|
||||
|
||||
lib_str = ', '.join(sorted(filter(None, (
|
||||
can_decrypt_frag and 'pycryptodome',
|
||||
compat_pycrypto_AES and compat_pycrypto_AES.__name__.split('.')[0],
|
||||
has_websockets and 'websockets',
|
||||
has_mutagen and 'mutagen',
|
||||
SQLITE_AVAILABLE and 'sqlite',
|
||||
|
||||
+3
-2
@@ -248,7 +248,7 @@ def _real_main(argv=None):
|
||||
if opts.cookiesfrombrowser is not None:
|
||||
opts.cookiesfrombrowser = [
|
||||
part.strip() or None for part in opts.cookiesfrombrowser.split(':', 1)]
|
||||
if opts.cookiesfrombrowser[0] not in SUPPORTED_BROWSERS:
|
||||
if opts.cookiesfrombrowser[0].lower() not in SUPPORTED_BROWSERS:
|
||||
parser.error('unsupported browser specified for cookies')
|
||||
|
||||
if opts.date is not None:
|
||||
@@ -279,7 +279,7 @@ def _real_main(argv=None):
|
||||
setattr(opts, opt_name, default)
|
||||
return None
|
||||
|
||||
set_default_compat('abort-on-error', 'ignoreerrors')
|
||||
set_default_compat('abort-on-error', 'ignoreerrors', 'only_download')
|
||||
set_default_compat('no-playlist-metafiles', 'allow_playlist_files')
|
||||
set_default_compat('no-clean-infojson', 'clean_infojson')
|
||||
if 'format-sort' in compat_opts:
|
||||
@@ -575,6 +575,7 @@ def _real_main(argv=None):
|
||||
|
||||
ydl_opts = {
|
||||
'usenetrc': opts.usenetrc,
|
||||
'netrc_location': opts.netrc_location,
|
||||
'username': opts.username,
|
||||
'password': opts.password,
|
||||
'twofactor': opts.twofactor,
|
||||
|
||||
+178
-45
@@ -2,36 +2,68 @@ from __future__ import unicode_literals
|
||||
|
||||
from math import ceil
|
||||
|
||||
from .compat import compat_b64decode
|
||||
from .compat import compat_b64decode, compat_pycrypto_AES
|
||||
from .utils import bytes_to_intlist, intlist_to_bytes
|
||||
|
||||
|
||||
if compat_pycrypto_AES:
|
||||
def aes_cbc_decrypt_bytes(data, key, iv):
|
||||
""" Decrypt bytes with AES-CBC using pycryptodome """
|
||||
return compat_pycrypto_AES.new(key, compat_pycrypto_AES.MODE_CBC, iv).decrypt(data)
|
||||
|
||||
def aes_gcm_decrypt_and_verify_bytes(data, key, tag, nonce):
|
||||
""" Decrypt bytes with AES-GCM using pycryptodome """
|
||||
return compat_pycrypto_AES.new(key, compat_pycrypto_AES.MODE_GCM, nonce).decrypt_and_verify(data, tag)
|
||||
|
||||
else:
|
||||
def aes_cbc_decrypt_bytes(data, key, iv):
|
||||
""" Decrypt bytes with AES-CBC using native implementation since pycryptodome is unavailable """
|
||||
return intlist_to_bytes(aes_cbc_decrypt(*map(bytes_to_intlist, (data, key, iv))))
|
||||
|
||||
def aes_gcm_decrypt_and_verify_bytes(data, key, tag, nonce):
|
||||
""" Decrypt bytes with AES-GCM using native implementation since pycryptodome is unavailable """
|
||||
return intlist_to_bytes(aes_gcm_decrypt_and_verify(*map(bytes_to_intlist, (data, key, tag, nonce))))
|
||||
|
||||
|
||||
BLOCK_SIZE_BYTES = 16
|
||||
|
||||
|
||||
def aes_ctr_decrypt(data, key, counter):
|
||||
def aes_ctr_decrypt(data, key, iv):
|
||||
"""
|
||||
Decrypt with aes in counter mode
|
||||
|
||||
@param {int[]} data cipher
|
||||
@param {int[]} key 16/24/32-Byte cipher key
|
||||
@param {instance} counter Instance whose next_value function (@returns {int[]} 16-Byte block)
|
||||
returns the next counter block
|
||||
@param {int[]} iv 16-Byte initialization vector
|
||||
@returns {int[]} decrypted data
|
||||
"""
|
||||
return aes_ctr_encrypt(data, key, iv)
|
||||
|
||||
|
||||
def aes_ctr_encrypt(data, key, iv):
|
||||
"""
|
||||
Encrypt with aes in counter mode
|
||||
|
||||
@param {int[]} data cleartext
|
||||
@param {int[]} key 16/24/32-Byte cipher key
|
||||
@param {int[]} iv 16-Byte initialization vector
|
||||
@returns {int[]} encrypted data
|
||||
"""
|
||||
expanded_key = key_expansion(key)
|
||||
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||
counter = iter_vector(iv)
|
||||
|
||||
decrypted_data = []
|
||||
encrypted_data = []
|
||||
for i in range(block_count):
|
||||
counter_block = counter.next_value()
|
||||
counter_block = next(counter)
|
||||
block = data[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES]
|
||||
block += [0] * (BLOCK_SIZE_BYTES - len(block))
|
||||
|
||||
cipher_counter_block = aes_encrypt(counter_block, expanded_key)
|
||||
decrypted_data += xor(block, cipher_counter_block)
|
||||
decrypted_data = decrypted_data[:len(data)]
|
||||
encrypted_data += xor(block, cipher_counter_block)
|
||||
encrypted_data = encrypted_data[:len(data)]
|
||||
|
||||
return decrypted_data
|
||||
return encrypted_data
|
||||
|
||||
|
||||
def aes_cbc_decrypt(data, key, iv):
|
||||
@@ -88,39 +120,47 @@ def aes_cbc_encrypt(data, key, iv):
|
||||
return encrypted_data
|
||||
|
||||
|
||||
def key_expansion(data):
|
||||
def aes_gcm_decrypt_and_verify(data, key, tag, nonce):
|
||||
"""
|
||||
Generate key schedule
|
||||
Decrypt with aes in GBM mode and checks authenticity using tag
|
||||
|
||||
@param {int[]} data 16/24/32-Byte cipher key
|
||||
@returns {int[]} 176/208/240-Byte expanded key
|
||||
@param {int[]} data cipher
|
||||
@param {int[]} key 16-Byte cipher key
|
||||
@param {int[]} tag authentication tag
|
||||
@param {int[]} nonce IV (recommended 12-Byte)
|
||||
@returns {int[]} decrypted data
|
||||
"""
|
||||
data = data[:] # copy
|
||||
rcon_iteration = 1
|
||||
key_size_bytes = len(data)
|
||||
expanded_key_size_bytes = (key_size_bytes // 4 + 7) * BLOCK_SIZE_BYTES
|
||||
|
||||
while len(data) < expanded_key_size_bytes:
|
||||
temp = data[-4:]
|
||||
temp = key_schedule_core(temp, rcon_iteration)
|
||||
rcon_iteration += 1
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
# XXX: check aes, gcm param
|
||||
|
||||
for _ in range(3):
|
||||
temp = data[-4:]
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
hash_subkey = aes_encrypt([0] * BLOCK_SIZE_BYTES, key_expansion(key))
|
||||
|
||||
if key_size_bytes == 32:
|
||||
temp = data[-4:]
|
||||
temp = sub_bytes(temp)
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
if len(nonce) == 12:
|
||||
j0 = nonce + [0, 0, 0, 1]
|
||||
else:
|
||||
fill = (BLOCK_SIZE_BYTES - (len(nonce) % BLOCK_SIZE_BYTES)) % BLOCK_SIZE_BYTES + 8
|
||||
ghash_in = nonce + [0] * fill + bytes_to_intlist((8 * len(nonce)).to_bytes(8, 'big'))
|
||||
j0 = ghash(hash_subkey, ghash_in)
|
||||
|
||||
for _ in range(3 if key_size_bytes == 32 else 2 if key_size_bytes == 24 else 0):
|
||||
temp = data[-4:]
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
data = data[:expanded_key_size_bytes]
|
||||
# TODO: add nonce support to aes_ctr_decrypt
|
||||
|
||||
return data
|
||||
# nonce_ctr = j0[:12]
|
||||
iv_ctr = inc(j0)
|
||||
|
||||
decrypted_data = aes_ctr_decrypt(data, key, iv_ctr + [0] * (BLOCK_SIZE_BYTES - len(iv_ctr)))
|
||||
pad_len = len(data) // 16 * 16
|
||||
s_tag = ghash(
|
||||
hash_subkey,
|
||||
data
|
||||
+ [0] * (BLOCK_SIZE_BYTES - len(data) + pad_len) # pad
|
||||
+ bytes_to_intlist((0 * 8).to_bytes(8, 'big') # length of associated data
|
||||
+ ((len(data) * 8).to_bytes(8, 'big'))) # length of data
|
||||
)
|
||||
|
||||
if tag != aes_ctr_encrypt(s_tag, key, j0):
|
||||
raise ValueError("Mismatching authentication tag")
|
||||
|
||||
return decrypted_data
|
||||
|
||||
|
||||
def aes_encrypt(data, expanded_key):
|
||||
@@ -189,15 +229,7 @@ def aes_decrypt_text(data, password, key_size_bytes):
|
||||
nonce = data[:NONCE_LENGTH_BYTES]
|
||||
cipher = data[NONCE_LENGTH_BYTES:]
|
||||
|
||||
class Counter(object):
|
||||
__value = nonce + [0] * (BLOCK_SIZE_BYTES - NONCE_LENGTH_BYTES)
|
||||
|
||||
def next_value(self):
|
||||
temp = self.__value
|
||||
self.__value = inc(self.__value)
|
||||
return temp
|
||||
|
||||
decrypted_data = aes_ctr_decrypt(cipher, key, Counter())
|
||||
decrypted_data = aes_ctr_decrypt(cipher, key, nonce + [0] * (BLOCK_SIZE_BYTES - NONCE_LENGTH_BYTES))
|
||||
plaintext = intlist_to_bytes(decrypted_data)
|
||||
|
||||
return plaintext
|
||||
@@ -278,6 +310,47 @@ RIJNDAEL_LOG_TABLE = (0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1a, 0xc6, 0x4b, 0xc7
|
||||
0x67, 0x4a, 0xed, 0xde, 0xc5, 0x31, 0xfe, 0x18, 0x0d, 0x63, 0x8c, 0x80, 0xc0, 0xf7, 0x70, 0x07)
|
||||
|
||||
|
||||
def key_expansion(data):
|
||||
"""
|
||||
Generate key schedule
|
||||
|
||||
@param {int[]} data 16/24/32-Byte cipher key
|
||||
@returns {int[]} 176/208/240-Byte expanded key
|
||||
"""
|
||||
data = data[:] # copy
|
||||
rcon_iteration = 1
|
||||
key_size_bytes = len(data)
|
||||
expanded_key_size_bytes = (key_size_bytes // 4 + 7) * BLOCK_SIZE_BYTES
|
||||
|
||||
while len(data) < expanded_key_size_bytes:
|
||||
temp = data[-4:]
|
||||
temp = key_schedule_core(temp, rcon_iteration)
|
||||
rcon_iteration += 1
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
|
||||
for _ in range(3):
|
||||
temp = data[-4:]
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
|
||||
if key_size_bytes == 32:
|
||||
temp = data[-4:]
|
||||
temp = sub_bytes(temp)
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
|
||||
for _ in range(3 if key_size_bytes == 32 else 2 if key_size_bytes == 24 else 0):
|
||||
temp = data[-4:]
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
data = data[:expanded_key_size_bytes]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def iter_vector(iv):
|
||||
while True:
|
||||
yield iv
|
||||
iv = inc(iv)
|
||||
|
||||
|
||||
def sub_bytes(data):
|
||||
return [SBOX[x] for x in data]
|
||||
|
||||
@@ -303,7 +376,7 @@ def xor(data1, data2):
|
||||
|
||||
|
||||
def rijndael_mul(a, b):
|
||||
if(a == 0 or b == 0):
|
||||
if a == 0 or b == 0:
|
||||
return 0
|
||||
return RIJNDAEL_EXP_TABLE[(RIJNDAEL_LOG_TABLE[a] + RIJNDAEL_LOG_TABLE[b]) % 0xFF]
|
||||
|
||||
@@ -347,6 +420,20 @@ def shift_rows_inv(data):
|
||||
return data_shifted
|
||||
|
||||
|
||||
def shift_block(data):
|
||||
data_shifted = []
|
||||
|
||||
bit = 0
|
||||
for n in data:
|
||||
if bit:
|
||||
n |= 0x100
|
||||
bit = n & 1
|
||||
n >>= 1
|
||||
data_shifted.append(n)
|
||||
|
||||
return data_shifted
|
||||
|
||||
|
||||
def inc(data):
|
||||
data = data[:] # copy
|
||||
for i in range(len(data) - 1, -1, -1):
|
||||
@@ -358,4 +445,50 @@ def inc(data):
|
||||
return data
|
||||
|
||||
|
||||
__all__ = ['aes_encrypt', 'key_expansion', 'aes_ctr_decrypt', 'aes_cbc_decrypt', 'aes_decrypt_text']
|
||||
def block_product(block_x, block_y):
|
||||
# NIST SP 800-38D, Algorithm 1
|
||||
|
||||
if len(block_x) != BLOCK_SIZE_BYTES or len(block_y) != BLOCK_SIZE_BYTES:
|
||||
raise ValueError("Length of blocks need to be %d bytes" % BLOCK_SIZE_BYTES)
|
||||
|
||||
block_r = [0xE1] + [0] * (BLOCK_SIZE_BYTES - 1)
|
||||
block_v = block_y[:]
|
||||
block_z = [0] * BLOCK_SIZE_BYTES
|
||||
|
||||
for i in block_x:
|
||||
for bit in range(7, -1, -1):
|
||||
if i & (1 << bit):
|
||||
block_z = xor(block_z, block_v)
|
||||
|
||||
do_xor = block_v[-1] & 1
|
||||
block_v = shift_block(block_v)
|
||||
if do_xor:
|
||||
block_v = xor(block_v, block_r)
|
||||
|
||||
return block_z
|
||||
|
||||
|
||||
def ghash(subkey, data):
|
||||
# NIST SP 800-38D, Algorithm 2
|
||||
|
||||
if len(data) % BLOCK_SIZE_BYTES:
|
||||
raise ValueError("Length of data should be %d bytes" % BLOCK_SIZE_BYTES)
|
||||
|
||||
last_y = [0] * BLOCK_SIZE_BYTES
|
||||
for i in range(0, len(data), BLOCK_SIZE_BYTES):
|
||||
block = data[i : i + BLOCK_SIZE_BYTES] # noqa: E203
|
||||
last_y = block_product(xor(last_y, block), subkey)
|
||||
|
||||
return last_y
|
||||
|
||||
|
||||
__all__ = [
|
||||
'aes_ctr_decrypt',
|
||||
'aes_cbc_decrypt',
|
||||
'aes_cbc_decrypt_bytes',
|
||||
'aes_decrypt_text',
|
||||
'aes_encrypt',
|
||||
'aes_gcm_decrypt_and_verify',
|
||||
'aes_gcm_decrypt_and_verify_bytes',
|
||||
'key_expansion'
|
||||
]
|
||||
|
||||
+28
-1
@@ -130,6 +130,33 @@ except AttributeError:
|
||||
asyncio.run = compat_asyncio_run
|
||||
|
||||
|
||||
# Python 3.8+ does not honor %HOME% on windows, but this breaks compatibility with youtube-dl
|
||||
# See https://github.com/yt-dlp/yt-dlp/issues/792
|
||||
# https://docs.python.org/3/library/os.path.html#os.path.expanduser
|
||||
if compat_os_name in ('nt', 'ce') and 'HOME' in os.environ:
|
||||
_userhome = os.environ['HOME']
|
||||
|
||||
def compat_expanduser(path):
|
||||
if not path.startswith('~'):
|
||||
return path
|
||||
i = path.replace('\\', '/', 1).find('/') # ~user
|
||||
if i < 0:
|
||||
i = len(path)
|
||||
userhome = os.path.join(os.path.dirname(_userhome), path[1:i]) if i > 1 else _userhome
|
||||
return userhome + path[i:]
|
||||
else:
|
||||
compat_expanduser = os.path.expanduser
|
||||
|
||||
|
||||
try:
|
||||
from Cryptodome.Cipher import AES as compat_pycrypto_AES
|
||||
except ImportError:
|
||||
try:
|
||||
from Crypto.Cipher import AES as compat_pycrypto_AES
|
||||
except ImportError:
|
||||
compat_pycrypto_AES = None
|
||||
|
||||
|
||||
# Deprecated
|
||||
|
||||
compat_basestring = str
|
||||
@@ -152,7 +179,6 @@ compat_cookies = http.cookies
|
||||
compat_cookies_SimpleCookie = compat_cookies.SimpleCookie
|
||||
compat_etree_Element = etree.Element
|
||||
compat_etree_register_namespace = etree.register_namespace
|
||||
compat_expanduser = os.path.expanduser
|
||||
compat_get_terminal_size = shutil.get_terminal_size
|
||||
compat_getenv = os.getenv
|
||||
compat_getpass = getpass.getpass
|
||||
@@ -224,6 +250,7 @@ __all__ = [
|
||||
'compat_os_name',
|
||||
'compat_parse_qs',
|
||||
'compat_print',
|
||||
'compat_pycrypto_AES',
|
||||
'compat_realpath',
|
||||
'compat_setenv',
|
||||
'compat_shlex_quote',
|
||||
|
||||
+14
-29
@@ -9,16 +9,14 @@ import tempfile
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from hashlib import pbkdf2_hmac
|
||||
|
||||
from yt_dlp.aes import aes_cbc_decrypt
|
||||
from yt_dlp.compat import (
|
||||
from .aes import aes_cbc_decrypt_bytes, aes_gcm_decrypt_and_verify_bytes
|
||||
from .compat import (
|
||||
compat_b64decode,
|
||||
compat_cookiejar_Cookie,
|
||||
)
|
||||
from yt_dlp.utils import (
|
||||
from .utils import (
|
||||
bug_reports_message,
|
||||
bytes_to_intlist,
|
||||
expand_path,
|
||||
intlist_to_bytes,
|
||||
process_communicate_or_kill,
|
||||
YoutubeDLCookieJar,
|
||||
)
|
||||
@@ -32,12 +30,6 @@ except ImportError:
|
||||
SQLITE_AVAILABLE = False
|
||||
|
||||
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
CRYPTO_AVAILABLE = True
|
||||
except ImportError:
|
||||
CRYPTO_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import keyring
|
||||
KEYRING_AVAILABLE = True
|
||||
@@ -123,7 +115,7 @@ def _extract_firefox_cookies(profile, logger):
|
||||
cookie_database_path = _find_most_recently_used_file(search_root, 'cookies.sqlite')
|
||||
if cookie_database_path is None:
|
||||
raise FileNotFoundError('could not find firefox cookies database in {}'.format(search_root))
|
||||
logger.debug('extracting from: "{}"'.format(cookie_database_path))
|
||||
logger.debug('Extracting cookies from: "{}"'.format(cookie_database_path))
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='youtube_dl') as tmpdir:
|
||||
cursor = None
|
||||
@@ -240,7 +232,7 @@ def _extract_chrome_cookies(browser_name, profile, logger):
|
||||
cookie_database_path = _find_most_recently_used_file(search_root, 'Cookies')
|
||||
if cookie_database_path is None:
|
||||
raise FileNotFoundError('could not find {} cookies database in "{}"'.format(browser_name, search_root))
|
||||
logger.debug('extracting from: "{}"'.format(cookie_database_path))
|
||||
logger.debug('Extracting cookies from: "{}"'.format(cookie_database_path))
|
||||
|
||||
decryptor = get_cookie_decryptor(config['browser_dir'], config['keyring_name'], logger)
|
||||
|
||||
@@ -400,11 +392,6 @@ class WindowsChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||
if self._v10_key is None:
|
||||
self._logger.warning('cannot decrypt v10 cookies: no key found', only_once=True)
|
||||
return None
|
||||
elif not CRYPTO_AVAILABLE:
|
||||
self._logger.warning('cannot decrypt cookie as the `pycryptodome` module is not installed. '
|
||||
'Please install by running `python3 -m pip install pycryptodome`',
|
||||
only_once=True)
|
||||
return None
|
||||
|
||||
# https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/os_crypt_win.cc
|
||||
# kNonceLength
|
||||
@@ -559,7 +546,7 @@ def _parse_safari_cookies_record(data, jar, logger):
|
||||
p.skip_to(value_offset)
|
||||
value = p.read_cstring()
|
||||
except UnicodeDecodeError:
|
||||
logger.warning('failed to parse cookie because UTF-8 decoding failed')
|
||||
logger.warning('failed to parse cookie because UTF-8 decoding failed', only_once=True)
|
||||
return record_size
|
||||
|
||||
p.skip_to(record_size, 'space at the end of the record')
|
||||
@@ -648,29 +635,26 @@ def pbkdf2_sha1(password, salt, iterations, key_length):
|
||||
|
||||
|
||||
def _decrypt_aes_cbc(ciphertext, key, logger, initialization_vector=b' ' * 16):
|
||||
plaintext = aes_cbc_decrypt(bytes_to_intlist(ciphertext),
|
||||
bytes_to_intlist(key),
|
||||
bytes_to_intlist(initialization_vector))
|
||||
plaintext = aes_cbc_decrypt_bytes(ciphertext, key, initialization_vector)
|
||||
padding_length = plaintext[-1]
|
||||
try:
|
||||
return intlist_to_bytes(plaintext[:-padding_length]).decode('utf-8')
|
||||
return plaintext[:-padding_length].decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
logger.warning('failed to decrypt cookie because UTF-8 decoding failed. Possibly the key is wrong?')
|
||||
logger.warning('failed to decrypt cookie because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
|
||||
return None
|
||||
|
||||
|
||||
def _decrypt_aes_gcm(ciphertext, key, nonce, authentication_tag, logger):
|
||||
cipher = AES.new(key, AES.MODE_GCM, nonce)
|
||||
try:
|
||||
plaintext = cipher.decrypt_and_verify(ciphertext, authentication_tag)
|
||||
plaintext = aes_gcm_decrypt_and_verify_bytes(ciphertext, key, authentication_tag, nonce)
|
||||
except ValueError:
|
||||
logger.warning('failed to decrypt cookie because the MAC check failed. Possibly the key is wrong?')
|
||||
logger.warning('failed to decrypt cookie because the MAC check failed. Possibly the key is wrong?', only_once=True)
|
||||
return None
|
||||
|
||||
try:
|
||||
return plaintext.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
logger.warning('failed to decrypt cookie because UTF-8 decoding failed. Possibly the key is wrong?')
|
||||
logger.warning('failed to decrypt cookie because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
|
||||
return None
|
||||
|
||||
|
||||
@@ -698,7 +682,7 @@ def _decrypt_windows_dpapi(ciphertext, logger):
|
||||
ctypes.byref(blob_out) # pDataOut
|
||||
)
|
||||
if not ret:
|
||||
logger.warning('failed to decrypt with DPAPI')
|
||||
logger.warning('failed to decrypt with DPAPI', only_once=True)
|
||||
return None
|
||||
|
||||
result = ctypes.string_at(blob_out.pbData, blob_out.cbData)
|
||||
@@ -748,6 +732,7 @@ def _is_path(value):
|
||||
|
||||
|
||||
def _parse_browser_specification(browser_name, profile=None):
|
||||
browser_name = browser_name.lower()
|
||||
if browser_name not in SUPPORTED_BROWSERS:
|
||||
raise ValueError(f'unsupported browser: "{browser_name}"')
|
||||
if profile is not None and _is_path(profile):
|
||||
|
||||
@@ -16,6 +16,11 @@ from ..utils import (
|
||||
shell_quote,
|
||||
timeconvert,
|
||||
)
|
||||
from ..minicurses import (
|
||||
MultilinePrinter,
|
||||
QuietMultilinePrinter,
|
||||
BreaklineStatusPrinter
|
||||
)
|
||||
|
||||
|
||||
class FileDownloader(object):
|
||||
@@ -68,6 +73,7 @@ class FileDownloader(object):
|
||||
self.ydl = ydl
|
||||
self._progress_hooks = []
|
||||
self.params = params
|
||||
self._multiline = None
|
||||
self.add_progress_hook(self.report_progress)
|
||||
|
||||
@staticmethod
|
||||
@@ -236,20 +242,35 @@ class FileDownloader(object):
|
||||
"""Report destination filename."""
|
||||
self.to_screen('[download] Destination: ' + filename)
|
||||
|
||||
def _report_progress_status(self, msg, is_last_line=False):
|
||||
def _prepare_multiline_status(self, lines):
|
||||
if self.params.get('quiet'):
|
||||
self._multiline = QuietMultilinePrinter()
|
||||
elif self.params.get('progress_with_newline', False):
|
||||
self._multiline = BreaklineStatusPrinter(sys.stderr, lines)
|
||||
elif self.params.get('noprogress', False):
|
||||
self._multiline = None
|
||||
else:
|
||||
self._multiline = MultilinePrinter(sys.stderr, lines)
|
||||
|
||||
def _finish_multiline_status(self):
|
||||
if self._multiline is not None:
|
||||
self._multiline.end()
|
||||
|
||||
def _report_progress_status(self, msg, is_last_line=False, progress_line=None):
|
||||
fullmsg = '[download] ' + msg
|
||||
if self.params.get('progress_with_newline', False):
|
||||
self.to_screen(fullmsg)
|
||||
elif progress_line is not None and self._multiline is not None:
|
||||
self._multiline.print_at_line(fullmsg, progress_line)
|
||||
else:
|
||||
if compat_os_name == 'nt':
|
||||
prev_len = getattr(self, '_report_progress_prev_line_length',
|
||||
0)
|
||||
if compat_os_name == 'nt' or not sys.stderr.isatty():
|
||||
prev_len = getattr(self, '_report_progress_prev_line_length', 0)
|
||||
if prev_len > len(fullmsg):
|
||||
fullmsg += ' ' * (prev_len - len(fullmsg))
|
||||
self._report_progress_prev_line_length = len(fullmsg)
|
||||
clear_line = '\r'
|
||||
else:
|
||||
clear_line = ('\r\x1b[K' if sys.stderr.isatty() else '\r')
|
||||
clear_line = '\r\x1b[K'
|
||||
self.to_screen(clear_line + fullmsg, skip_eol=not is_last_line)
|
||||
self.to_console_title('yt-dlp ' + msg)
|
||||
|
||||
@@ -266,7 +287,8 @@ class FileDownloader(object):
|
||||
s['_elapsed_str'] = self.format_seconds(s['elapsed'])
|
||||
msg_template += ' in %(_elapsed_str)s'
|
||||
self._report_progress_status(
|
||||
msg_template % s, is_last_line=True)
|
||||
msg_template % s, is_last_line=True, progress_line=s.get('progress_idx'))
|
||||
return
|
||||
|
||||
if self.params.get('noprogress'):
|
||||
return
|
||||
@@ -311,7 +333,7 @@ class FileDownloader(object):
|
||||
else:
|
||||
msg_template = '%(_percent_str)s % at %(_speed_str)s ETA %(_eta_str)s'
|
||||
|
||||
self._report_progress_status(msg_template % s)
|
||||
self._report_progress_status(msg_template % s, progress_line=s.get('progress_idx'))
|
||||
|
||||
def report_resuming_byte(self, resume_len):
|
||||
"""Report attempt to resume at given byte."""
|
||||
|
||||
@@ -6,13 +6,7 @@ import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
can_decrypt_frag = True
|
||||
except ImportError:
|
||||
can_decrypt_frag = False
|
||||
|
||||
from .common import FileDownloader
|
||||
from .fragment import FragmentFD
|
||||
from ..compat import (
|
||||
compat_setenv,
|
||||
compat_str,
|
||||
@@ -29,12 +23,11 @@ from ..utils import (
|
||||
check_executable,
|
||||
is_outdated_version,
|
||||
process_communicate_or_kill,
|
||||
sanitized_Request,
|
||||
sanitize_open,
|
||||
)
|
||||
|
||||
|
||||
class ExternalFD(FileDownloader):
|
||||
class ExternalFD(FragmentFD):
|
||||
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps')
|
||||
can_download_to_stdout = False
|
||||
|
||||
@@ -146,6 +139,7 @@ class ExternalFD(FileDownloader):
|
||||
self.report_error('Giving up after %s fragment retries' % fragment_retries)
|
||||
return -1
|
||||
|
||||
decrypt_fragment = self.decrypter(info_dict)
|
||||
dest, _ = sanitize_open(tmpfilename, 'wb')
|
||||
for frag_index, fragment in enumerate(info_dict['fragments']):
|
||||
fragment_filename = '%s-Frag%d' % (tmpfilename, frag_index)
|
||||
@@ -157,22 +151,7 @@ class ExternalFD(FileDownloader):
|
||||
continue
|
||||
self.report_error('Unable to open fragment %d' % frag_index)
|
||||
return -1
|
||||
decrypt_info = fragment.get('decrypt_info')
|
||||
if decrypt_info:
|
||||
if decrypt_info['METHOD'] == 'AES-128':
|
||||
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)
|
||||
dest.write(decrypt_fragment(fragment, src.read()))
|
||||
src.close()
|
||||
if not self.params.get('keep_fragments', False):
|
||||
os.remove(encodeFilename(fragment_filename))
|
||||
@@ -186,10 +165,6 @@ class ExternalFD(FileDownloader):
|
||||
self.to_stderr(stderr.decode('utf-8', 'replace'))
|
||||
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):
|
||||
AVAILABLE_OPT = '-V'
|
||||
@@ -357,7 +332,7 @@ class FFmpegFD(ExternalFD):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def can_merge_formats(cls, info_dict, params={}):
|
||||
def can_merge_formats(cls, info_dict, params):
|
||||
return (
|
||||
info_dict.get('requested_formats')
|
||||
and info_dict.get('protocol')
|
||||
@@ -523,7 +498,7 @@ class AVconvFD(FFmpegFD):
|
||||
_BY_NAME = dict(
|
||||
(klass.get_basename(), klass)
|
||||
for name, klass in globals().items()
|
||||
if name.endswith('FD') and name != 'ExternalFD'
|
||||
if name.endswith('FD') and name not in ('ExternalFD', 'FragmentFD')
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3,12 +3,7 @@ from __future__ import division, unicode_literals
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
can_decrypt_frag = True
|
||||
except ImportError:
|
||||
can_decrypt_frag = False
|
||||
from math import ceil
|
||||
|
||||
try:
|
||||
import concurrent.futures
|
||||
@@ -18,6 +13,7 @@ except ImportError:
|
||||
|
||||
from .common import FileDownloader
|
||||
from .http import HttpFD
|
||||
from ..aes import aes_cbc_decrypt_bytes
|
||||
from ..compat import (
|
||||
compat_urllib_error,
|
||||
compat_struct_pack,
|
||||
@@ -125,6 +121,7 @@ class FragmentFD(FileDownloader):
|
||||
'url': frag_url,
|
||||
'http_headers': headers or info_dict.get('http_headers'),
|
||||
'request_data': request_data,
|
||||
'ctx_id': ctx.get('ctx_id'),
|
||||
}
|
||||
success = ctx['dl'].download(fragment_filename, fragment_info_dict)
|
||||
if not success:
|
||||
@@ -224,6 +221,7 @@ class FragmentFD(FileDownloader):
|
||||
def _start_frag_download(self, ctx, info_dict):
|
||||
resume_len = ctx['complete_frags_downloaded_bytes']
|
||||
total_frags = ctx['total_frags']
|
||||
ctx_id = ctx.get('ctx_id')
|
||||
# This dict stores the download progress, it's updated by the progress
|
||||
# hook
|
||||
state = {
|
||||
@@ -247,6 +245,12 @@ class FragmentFD(FileDownloader):
|
||||
if s['status'] not in ('downloading', 'finished'):
|
||||
return
|
||||
|
||||
if ctx_id is not None and s.get('ctx_id') != ctx_id:
|
||||
return
|
||||
|
||||
state['max_progress'] = ctx.get('max_progress')
|
||||
state['progress_idx'] = ctx.get('progress_idx')
|
||||
|
||||
time_now = time.time()
|
||||
state['elapsed'] = time_now - start
|
||||
frag_total_bytes = s.get('total_bytes') or 0
|
||||
@@ -306,6 +310,9 @@ class FragmentFD(FileDownloader):
|
||||
'filename': ctx['filename'],
|
||||
'status': 'finished',
|
||||
'elapsed': elapsed,
|
||||
'ctx_id': ctx.get('ctx_id'),
|
||||
'max_progress': ctx.get('max_progress'),
|
||||
'progress_idx': ctx.get('progress_idx'),
|
||||
}, info_dict)
|
||||
|
||||
def _prepare_external_frag_download(self, ctx):
|
||||
@@ -329,7 +336,67 @@ class FragmentFD(FileDownloader):
|
||||
'fragment_index': 0,
|
||||
})
|
||||
|
||||
def download_and_append_fragments(self, ctx, fragments, info_dict, *, pack_func=None, finish_func=None):
|
||||
def decrypter(self, info_dict):
|
||||
_key_cache = {}
|
||||
|
||||
def _get_key(url):
|
||||
if url not in _key_cache:
|
||||
_key_cache[url] = self.ydl.urlopen(self._prepare_url(info_dict, url)).read()
|
||||
return _key_cache[url]
|
||||
|
||||
def decrypt_fragment(fragment, frag_content):
|
||||
decrypt_info = fragment.get('decrypt_info')
|
||||
if not decrypt_info or decrypt_info['METHOD'] != 'AES-128':
|
||||
return frag_content
|
||||
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', fragment['media_sequence'])
|
||||
decrypt_info['KEY'] = decrypt_info.get('KEY') or _get_key(info_dict.get('_decryption_key_url') or decrypt_info['URI'])
|
||||
# Don't decrypt the content in tests since the data is explicitly truncated and it's not to a valid block
|
||||
# size (see https://github.com/ytdl-org/youtube-dl/pull/27660). Tests only care that the correct data downloaded,
|
||||
# not what it decrypts to.
|
||||
if self.params.get('test', False):
|
||||
return frag_content
|
||||
return aes_cbc_decrypt_bytes(frag_content, decrypt_info['KEY'], iv)
|
||||
|
||||
return decrypt_fragment
|
||||
|
||||
def download_and_append_fragments_multiple(self, *args, pack_func=None, finish_func=None):
|
||||
'''
|
||||
@params (ctx1, fragments1, info_dict1), (ctx2, fragments2, info_dict2), ...
|
||||
all args must be either tuple or list
|
||||
'''
|
||||
max_progress = len(args)
|
||||
if max_progress == 1:
|
||||
return self.download_and_append_fragments(*args[0], pack_func=pack_func, finish_func=finish_func)
|
||||
max_workers = self.params.get('concurrent_fragment_downloads', max_progress)
|
||||
self._prepare_multiline_status(max_progress)
|
||||
|
||||
def thread_func(idx, ctx, fragments, info_dict, tpe):
|
||||
ctx['max_progress'] = max_progress
|
||||
ctx['progress_idx'] = idx
|
||||
return self.download_and_append_fragments(ctx, fragments, info_dict, pack_func=pack_func, finish_func=finish_func, tpe=tpe)
|
||||
|
||||
class FTPE(concurrent.futures.ThreadPoolExecutor):
|
||||
# has to stop this or it's going to wait on the worker thread itself
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
pass
|
||||
|
||||
spins = []
|
||||
for idx, (ctx, fragments, info_dict) in enumerate(args):
|
||||
tpe = FTPE(ceil(max_workers / max_progress))
|
||||
job = tpe.submit(thread_func, idx, ctx, fragments, info_dict, tpe)
|
||||
spins.append((tpe, job))
|
||||
|
||||
result = True
|
||||
for tpe, job in spins:
|
||||
try:
|
||||
result = result and job.result()
|
||||
finally:
|
||||
tpe.shutdown(wait=True)
|
||||
|
||||
self._finish_multiline_status()
|
||||
return True
|
||||
|
||||
def download_and_append_fragments(self, ctx, fragments, info_dict, *, pack_func=None, finish_func=None, tpe=None):
|
||||
fragment_retries = self.params.get('fragment_retries', 0)
|
||||
is_fatal = (lambda idx: idx == 0) if self.params.get('skip_unavailable_fragments', True) else (lambda _: True)
|
||||
if not pack_func:
|
||||
@@ -337,7 +404,7 @@ class FragmentFD(FileDownloader):
|
||||
|
||||
def download_fragment(fragment, ctx):
|
||||
frag_index = ctx['fragment_index'] = fragment['frag_index']
|
||||
headers = info_dict.get('http_headers', {})
|
||||
headers = info_dict.get('http_headers', {}).copy()
|
||||
byte_range = fragment.get('byte_range')
|
||||
if byte_range:
|
||||
headers['Range'] = 'bytes=%d-%d' % (byte_range['start'], byte_range['end'] - 1)
|
||||
@@ -374,20 +441,6 @@ class FragmentFD(FileDownloader):
|
||||
return False, frag_index
|
||||
return frag_content, frag_index
|
||||
|
||||
def decrypt_fragment(fragment, frag_content):
|
||||
decrypt_info = fragment.get('decrypt_info')
|
||||
if not decrypt_info or decrypt_info['METHOD'] != 'AES-128':
|
||||
return frag_content
|
||||
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', fragment['media_sequence'])
|
||||
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
|
||||
self._prepare_url(info_dict, info_dict.get('_decryption_key_url') or decrypt_info['URI'])).read()
|
||||
# Don't decrypt the content in tests since the data is explicitly truncated and it's not to a valid block
|
||||
# size (see https://github.com/ytdl-org/youtube-dl/pull/27660). Tests only care that the correct data downloaded,
|
||||
# not what it decrypts to.
|
||||
if self.params.get('test', False):
|
||||
return frag_content
|
||||
return AES.new(decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(frag_content)
|
||||
|
||||
def append_fragment(frag_content, frag_index, ctx):
|
||||
if not frag_content:
|
||||
if not is_fatal(frag_index - 1):
|
||||
@@ -401,6 +454,8 @@ class FragmentFD(FileDownloader):
|
||||
self._append_fragment(ctx, pack_func(frag_content, frag_index))
|
||||
return True
|
||||
|
||||
decrypt_fragment = self.decrypter(info_dict)
|
||||
|
||||
max_workers = self.params.get('concurrent_fragment_downloads', 1)
|
||||
if can_threaded_download and max_workers > 1:
|
||||
|
||||
@@ -410,7 +465,7 @@ class FragmentFD(FileDownloader):
|
||||
return fragment, frag_content, frag_index, ctx_copy.get('fragment_filename_sanitized')
|
||||
|
||||
self.report_warning('The download speed shown is only of one thread. This is a known issue and patches are welcome')
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
|
||||
with tpe or concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
|
||||
for fragment, frag_content, frag_index, frag_filename in pool.map(_download_fragment, fragments):
|
||||
ctx['fragment_filename_sanitized'] = frag_filename
|
||||
ctx['fragment_index'] = frag_index
|
||||
|
||||
@@ -5,7 +5,7 @@ import io
|
||||
import binascii
|
||||
|
||||
from ..downloader import get_suitable_downloader
|
||||
from .fragment import FragmentFD, can_decrypt_frag
|
||||
from .fragment import FragmentFD
|
||||
from .external import FFmpegFD
|
||||
|
||||
from ..compat import (
|
||||
@@ -29,7 +29,7 @@ class HlsFD(FragmentFD):
|
||||
FD_NAME = 'hlsnative'
|
||||
|
||||
@staticmethod
|
||||
def can_download(manifest, info_dict, allow_unplayable_formats=False, with_crypto=can_decrypt_frag):
|
||||
def can_download(manifest, info_dict, allow_unplayable_formats=False):
|
||||
UNSUPPORTED_FEATURES = [
|
||||
# r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [2]
|
||||
|
||||
@@ -56,9 +56,6 @@ class HlsFD(FragmentFD):
|
||||
|
||||
def check_results():
|
||||
yield not info_dict.get('is_live')
|
||||
is_aes128_enc = '#EXT-X-KEY:METHOD=AES-128' in manifest
|
||||
yield with_crypto or not is_aes128_enc
|
||||
yield not (is_aes128_enc and r'#EXT-X-BYTERANGE' in manifest)
|
||||
for feature in UNSUPPORTED_FEATURES:
|
||||
yield not re.search(feature, manifest)
|
||||
return all(check_results())
|
||||
@@ -75,8 +72,6 @@ class HlsFD(FragmentFD):
|
||||
if info_dict.get('extra_param_to_segment_url') or info_dict.get('_decryption_key_url'):
|
||||
self.report_error('pycryptodome not found. Please install')
|
||||
return False
|
||||
if self.can_download(s, info_dict, with_crypto=True):
|
||||
self.report_warning('pycryptodome is needed to download this file natively')
|
||||
fd = FFmpegFD(self.ydl, self.params)
|
||||
self.report_warning(
|
||||
'%s detected unsupported features; extraction will be delegated to %s' % (self.FD_NAME, fd.get_basename()))
|
||||
@@ -172,6 +167,7 @@ class HlsFD(FragmentFD):
|
||||
'byte_range': byte_range,
|
||||
'media_sequence': media_sequence,
|
||||
})
|
||||
media_sequence += 1
|
||||
|
||||
elif line.startswith('#EXT-X-MAP'):
|
||||
if format_index and discontinuity_count != format_index:
|
||||
@@ -196,6 +192,7 @@ class HlsFD(FragmentFD):
|
||||
'byte_range': byte_range,
|
||||
'media_sequence': media_sequence
|
||||
})
|
||||
media_sequence += 1
|
||||
|
||||
if map_info.get('BYTERANGE'):
|
||||
splitted_byte_range = map_info.get('BYTERANGE').split('@')
|
||||
|
||||
@@ -310,6 +310,7 @@ class HttpFD(FileDownloader):
|
||||
'eta': eta,
|
||||
'speed': speed,
|
||||
'elapsed': now - ctx.start_time,
|
||||
'ctx_id': info_dict.get('ctx_id'),
|
||||
}, info_dict)
|
||||
|
||||
if data_len is not None and byte_counter == data_len:
|
||||
@@ -357,6 +358,7 @@ class HttpFD(FileDownloader):
|
||||
'filename': ctx.filename,
|
||||
'status': 'finished',
|
||||
'elapsed': time.time() - ctx.start_time,
|
||||
'ctx_id': info_dict.get('ctx_id'),
|
||||
}, info_dict)
|
||||
|
||||
return True
|
||||
|
||||
@@ -6,7 +6,7 @@ import threading
|
||||
from .common import FileDownloader
|
||||
from ..downloader import get_suitable_downloader
|
||||
from ..extractor.niconico import NiconicoIE
|
||||
from ..compat import compat_urllib_request
|
||||
from ..utils import sanitized_Request
|
||||
|
||||
|
||||
class NiconicoDmcFD(FileDownloader):
|
||||
@@ -29,9 +29,11 @@ class NiconicoDmcFD(FileDownloader):
|
||||
heartbeat_data = heartbeat_info_dict['data'].encode()
|
||||
heartbeat_interval = heartbeat_info_dict.get('interval', 30)
|
||||
|
||||
request = sanitized_Request(heartbeat_url, heartbeat_data)
|
||||
|
||||
def heartbeat():
|
||||
try:
|
||||
compat_urllib_request.urlopen(url=heartbeat_url, data=heartbeat_data)
|
||||
self.ydl.urlopen(request).read()
|
||||
except Exception:
|
||||
self.to_screen('[%s] Heartbeat failed' % self.FD_NAME)
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ class YoutubeLiveChatFD(FragmentFD):
|
||||
request_data['currentPlayerState'] = {'playerOffsetMs': str(max(offset - 5000, 0))}
|
||||
if click_tracking_params:
|
||||
request_data['context']['clickTracking'] = {'clickTrackingParams': click_tracking_params}
|
||||
headers = ie.generate_api_headers(ytcfg, visitor_data=visitor_data)
|
||||
headers = ie.generate_api_headers(ytcfg=ytcfg, visitor_data=visitor_data)
|
||||
headers.update({'content-type': 'application/json'})
|
||||
fragment_request_data = json.dumps(request_data, ensure_ascii=False).encode('utf-8') + b'\n'
|
||||
success, continuation_id, offset, click_tracking_params = download_and_parse_fragment(
|
||||
|
||||
@@ -174,7 +174,7 @@ class ArteTVIE(ArteTVBaseIE):
|
||||
return {
|
||||
'id': player_info.get('VID') or video_id,
|
||||
'title': title,
|
||||
'description': player_info.get('VDE'),
|
||||
'description': player_info.get('VDE') or player_info.get('V7T'),
|
||||
'upload_date': unified_strdate(upload_date_str),
|
||||
'thumbnail': player_info.get('programImage') or player_info.get('VTU', {}).get('IUR'),
|
||||
'formats': formats,
|
||||
|
||||
+75
-43
@@ -1,74 +1,106 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
determine_ext,
|
||||
dict_get,
|
||||
int_or_none,
|
||||
unescapeHTML,
|
||||
float_or_none,
|
||||
jwt_encode_hs256,
|
||||
try_get,
|
||||
)
|
||||
|
||||
|
||||
class ATVAtIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?atv\.at/(?:[^/]+/){2}(?P<id>[dv]\d+)'
|
||||
_VALID_URL = r'https?://(?:www\.)?atv\.at/tv/(?:[^/]+/){2,3}(?P<id>.*)'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://www.atv.at/bauer-sucht-frau-die-zweite-chance/folge-1/d3390693/',
|
||||
'md5': 'c471605591009dfb6e6c54f7e62e2807',
|
||||
'url': 'https://www.atv.at/tv/bauer-sucht-frau/staffel-18/bauer-sucht-frau/bauer-sucht-frau-staffel-18-folge-3-die-hofwochen',
|
||||
'md5': '3c3b4aaca9f63e32b35e04a9c2515903',
|
||||
'info_dict': {
|
||||
'id': '3390684',
|
||||
'id': 'v-ce9cgn1e70n5-1',
|
||||
'ext': 'mp4',
|
||||
'title': 'Bauer sucht Frau - Die zweite Chance Folge 1',
|
||||
'title': 'Bauer sucht Frau - Staffel 18 Folge 3 - Die Hofwochen',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.atv.at/bauer-sucht-frau-staffel-17/fuenfte-eventfolge/d3339537/',
|
||||
'url': 'https://www.atv.at/tv/bauer-sucht-frau/staffel-18/episode-01/bauer-sucht-frau-staffel-18-vorstellungsfolge-1',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _process_source_entry(self, source, part_id):
|
||||
source_url = source.get('url')
|
||||
if not source_url:
|
||||
return
|
||||
if determine_ext(source_url) == 'm3u8':
|
||||
return self._extract_m3u8_formats(
|
||||
source_url, part_id, 'mp4', 'm3u8_native',
|
||||
m3u8_id='hls', fatal=False)
|
||||
else:
|
||||
return [{
|
||||
'url': source_url,
|
||||
}]
|
||||
# extracted from bootstrap.js function (search for e.encryption_key and use your browser's debugger)
|
||||
_ACCESS_ID = 'x_atv'
|
||||
_ENCRYPTION_KEY = 'Hohnaekeishoogh2omaeghooquooshia'
|
||||
|
||||
def _process_entry(self, entry):
|
||||
part_id = entry.get('id')
|
||||
if not part_id:
|
||||
return
|
||||
def _extract_video_info(self, url, content, video):
|
||||
clip_id = content.get('splitId', content['id'])
|
||||
formats = []
|
||||
for source in entry.get('sources', []):
|
||||
formats.extend(self._process_source_entry(source, part_id) or [])
|
||||
|
||||
clip_urls = video['urls']
|
||||
for protocol, variant in clip_urls.items():
|
||||
source_url = try_get(variant, lambda x: x['clear']['url'])
|
||||
if not source_url:
|
||||
continue
|
||||
if protocol == 'dash':
|
||||
formats.extend(self._extract_mpd_formats(
|
||||
source_url, clip_id, mpd_id=protocol, fatal=False))
|
||||
elif protocol == 'hls':
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
source_url, clip_id, 'mp4', 'm3u8_native',
|
||||
m3u8_id=protocol, fatal=False))
|
||||
else:
|
||||
formats.append({
|
||||
'url': source_url,
|
||||
'format_id': protocol,
|
||||
})
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': part_id,
|
||||
'title': entry.get('title'),
|
||||
'duration': int_or_none(entry.get('duration')),
|
||||
'formats': formats
|
||||
'id': clip_id,
|
||||
'title': content.get('title'),
|
||||
'duration': float_or_none(content.get('duration')),
|
||||
'series': content.get('tvShowTitle'),
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
video_data = self._parse_json(unescapeHTML(self._search_regex(
|
||||
r'var\splaylist\s*=\s*(?P<json>\[.*\]);',
|
||||
webpage, 'player data', group='json')),
|
||||
display_id)
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
json_data = self._parse_json(
|
||||
self._search_regex(r'<script id="state" type="text/plain">(.*)</script>', webpage, 'json_data'),
|
||||
video_id=video_id)
|
||||
|
||||
first_video = video_data[0]
|
||||
video_id = first_video['id']
|
||||
video_title = dict_get(first_video, ('tvShowTitle', 'title'))
|
||||
video_title = json_data['views']['default']['page']['title']
|
||||
contentResource = json_data['views']['default']['page']['contentResource']
|
||||
content_id = contentResource[0]['id']
|
||||
content_ids = [{'id': id, 'subclip_start': content['start'], 'subclip_end': content['end']}
|
||||
for id, content in enumerate(contentResource)]
|
||||
|
||||
time_of_request = datetime.datetime.now()
|
||||
not_before = time_of_request - datetime.timedelta(minutes=5)
|
||||
expire = time_of_request + datetime.timedelta(minutes=5)
|
||||
payload = {
|
||||
'content_ids': {
|
||||
content_id: content_ids,
|
||||
},
|
||||
'secure_delivery': True,
|
||||
'iat': int(time_of_request.timestamp()),
|
||||
'nbf': int(not_before.timestamp()),
|
||||
'exp': int(expire.timestamp()),
|
||||
}
|
||||
jwt_token = jwt_encode_hs256(payload, self._ENCRYPTION_KEY, headers={'kid': self._ACCESS_ID})
|
||||
videos = self._download_json(
|
||||
'https://vas-v4.p7s1video.net/4.0/getsources',
|
||||
content_id, 'Downloading videos JSON', query={
|
||||
'token': jwt_token.decode('utf-8')
|
||||
})
|
||||
|
||||
video_id, videos_data = list(videos['data'].items())[0]
|
||||
entries = [
|
||||
self._extract_video_info(url, contentResource[video['id']], video)
|
||||
for video in videos_data]
|
||||
|
||||
return {
|
||||
'_type': 'multi_video',
|
||||
'id': video_id,
|
||||
'title': video_title,
|
||||
'entries': (self._process_entry(entry) for entry in video_data),
|
||||
'entries': entries,
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ from ..utils import (
|
||||
try_get,
|
||||
smuggle_url,
|
||||
str_or_none,
|
||||
str_to_int,
|
||||
strip_jsonp,
|
||||
unified_timestamp,
|
||||
unsmuggle_url,
|
||||
@@ -774,3 +775,142 @@ class BiliBiliPlayerIE(InfoExtractor):
|
||||
return self.url_result(
|
||||
'http://www.bilibili.tv/video/av%s/' % video_id,
|
||||
ie=BiliBiliIE.ie_key(), video_id=video_id)
|
||||
|
||||
|
||||
class BiliIntlBaseIE(InfoExtractor):
|
||||
_API_URL = 'https://api.bili{}/intl/gateway{}'
|
||||
|
||||
def _call_api(self, type, endpoint, id):
|
||||
return self._download_json(self._API_URL.format(type, endpoint), id)['data']
|
||||
|
||||
def _get_subtitles(self, type, ep_id):
|
||||
sub_json = self._call_api(type, f'/m/subtitle?ep_id={ep_id}&platform=web', ep_id)
|
||||
subtitles = {}
|
||||
for sub in sub_json.get('subtitles', []):
|
||||
sub_url = sub.get('url')
|
||||
if not sub_url:
|
||||
continue
|
||||
subtitles.setdefault(sub.get('key', 'en'), []).append({
|
||||
'url': sub_url,
|
||||
})
|
||||
return subtitles
|
||||
|
||||
def _get_formats(self, type, ep_id):
|
||||
video_json = self._call_api(type, f'/web/playurl?ep_id={ep_id}&platform=web', ep_id)
|
||||
if not video_json:
|
||||
self.raise_login_required(method='cookies')
|
||||
video_json = video_json['playurl']
|
||||
formats = []
|
||||
for vid in video_json.get('video', []):
|
||||
video_res = vid.get('video_resource') or {}
|
||||
video_info = vid.get('stream_info') or {}
|
||||
if not video_res.get('url'):
|
||||
continue
|
||||
formats.append({
|
||||
'url': video_res['url'],
|
||||
'ext': 'mp4',
|
||||
'format_note': video_info.get('desc_words'),
|
||||
'width': video_res.get('width'),
|
||||
'height': video_res.get('height'),
|
||||
'vbr': video_res.get('bandwidth'),
|
||||
'acodec': 'none',
|
||||
'vcodec': video_res.get('codecs'),
|
||||
'filesize': video_res.get('size'),
|
||||
})
|
||||
for aud in video_json.get('audio_resource', []):
|
||||
if not aud.get('url'):
|
||||
continue
|
||||
formats.append({
|
||||
'url': aud['url'],
|
||||
'ext': 'mp4',
|
||||
'abr': aud.get('bandwidth'),
|
||||
'acodec': aud.get('codecs'),
|
||||
'vcodec': 'none',
|
||||
'filesize': aud.get('size'),
|
||||
})
|
||||
|
||||
self._sort_formats(formats)
|
||||
return formats
|
||||
|
||||
def _extract_ep_info(self, type, episode_data, ep_id):
|
||||
return {
|
||||
'id': ep_id,
|
||||
'title': episode_data.get('long_title') or episode_data['title'],
|
||||
'thumbnail': episode_data.get('cover'),
|
||||
'episode_number': str_to_int(episode_data.get('title')),
|
||||
'formats': self._get_formats(type, ep_id),
|
||||
'subtitles': self._get_subtitles(type, ep_id),
|
||||
'extractor_key': BiliIntlIE.ie_key(),
|
||||
}
|
||||
|
||||
|
||||
class BiliIntlIE(BiliIntlBaseIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?bili(?P<type>bili\.tv|intl.com)/(?:[a-z]{2}/)?play/(?P<season_id>\d+)/(?P<id>\d+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.bilibili.tv/en/play/34613/341736',
|
||||
'info_dict': {
|
||||
'id': '341736',
|
||||
'ext': 'mp4',
|
||||
'title': 'The First Night',
|
||||
'thumbnail': 'https://i0.hdslb.com/bfs/intl/management/91e30e5521235d9b163339a26a0b030ebda54310.png',
|
||||
'episode_number': 2,
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.biliintl.com/en/play/34613/341736',
|
||||
'info_dict': {
|
||||
'id': '341736',
|
||||
'ext': 'mp4',
|
||||
'title': 'The First Night',
|
||||
'thumbnail': 'https://i0.hdslb.com/bfs/intl/management/91e30e5521235d9b163339a26a0b030ebda54310.png',
|
||||
'episode_number': 2,
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
type, season_id, id = self._match_valid_url(url).groups()
|
||||
data_json = self._call_api(type, f'/web/view/ogv_collection?season_id={season_id}', id)
|
||||
episode_data = next(
|
||||
episode for episode in data_json.get('episodes', [])
|
||||
if str(episode.get('ep_id')) == id)
|
||||
return self._extract_ep_info(type, episode_data, id)
|
||||
|
||||
|
||||
class BiliIntlSeriesIE(BiliIntlBaseIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?bili(?P<type>bili\.tv|intl.com)/(?:[a-z]{2}/)?play/(?P<id>\d+)$'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.bilibili.tv/en/play/34613',
|
||||
'playlist_mincount': 15,
|
||||
'info_dict': {
|
||||
'id': '34613',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
'format': 'bv',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.biliintl.com/en/play/34613',
|
||||
'playlist_mincount': 15,
|
||||
'info_dict': {
|
||||
'id': '34613',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
'format': 'bv',
|
||||
},
|
||||
}]
|
||||
|
||||
def _entries(self, id, type):
|
||||
data_json = self._call_api(type, f'/web/view/ogv_collection?season_id={id}', id)
|
||||
for episode in data_json.get('episodes', []):
|
||||
episode_id = str(episode.get('ep_id'))
|
||||
yield self._extract_ep_info(type, episode, episode_id)
|
||||
|
||||
def _real_extract(self, url):
|
||||
type, id = self._match_valid_url(url).groups()
|
||||
return self.playlist_result(self._entries(id, type), playlist_id=id)
|
||||
|
||||
@@ -472,7 +472,7 @@ class BrightcoveNewIE(AdobePassIE):
|
||||
title = json_data['name'].strip()
|
||||
|
||||
num_drm_sources = 0
|
||||
formats = []
|
||||
formats, subtitles = [], {}
|
||||
sources = json_data.get('sources') or []
|
||||
for source in sources:
|
||||
container = source.get('container')
|
||||
@@ -488,12 +488,16 @@ class BrightcoveNewIE(AdobePassIE):
|
||||
elif ext == 'm3u8' or container == 'M2TS':
|
||||
if not src:
|
||||
continue
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
src, video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False))
|
||||
f, subs = self._extract_m3u8_formats_and_subtitles(
|
||||
src, video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False)
|
||||
formats.extend(f)
|
||||
subtitles = self._merge_subtitles(subtitles, subs)
|
||||
elif ext == 'mpd':
|
||||
if not src:
|
||||
continue
|
||||
formats.extend(self._extract_mpd_formats(src, video_id, 'dash', fatal=False))
|
||||
f, subs = self._extract_mpd_formats_and_subtitles(src, video_id, 'dash', fatal=False)
|
||||
formats.extend(f)
|
||||
subtitles = self._merge_subtitles(subtitles, subs)
|
||||
else:
|
||||
streaming_src = source.get('streaming_src')
|
||||
stream_name, app_name = source.get('stream_name'), source.get('app_name')
|
||||
@@ -556,7 +560,6 @@ class BrightcoveNewIE(AdobePassIE):
|
||||
for f in formats:
|
||||
f.setdefault('http_headers', {}).update(headers)
|
||||
|
||||
subtitles = {}
|
||||
for text_track in json_data.get('text_tracks', []):
|
||||
if text_track.get('kind') != 'captions':
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
|
||||
|
||||
class CAM4IE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:[^/]+\.)?cam4\.com/(?P<id>[a-z0-9_]+)'
|
||||
_TEST = {
|
||||
'url': 'https://www.cam4.com/foxynesss',
|
||||
'info_dict': {
|
||||
'id': 'foxynesss',
|
||||
'ext': 'mp4',
|
||||
'title': 're:^foxynesss [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
||||
'age_limit': 18,
|
||||
}
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
channel_id = self._match_id(url)
|
||||
m3u8_playlist = self._download_json('https://www.cam4.com/rest/v1.0/profile/{}/streamInfo'.format(channel_id), channel_id).get('cdnURL')
|
||||
|
||||
formats = self._extract_m3u8_formats(m3u8_playlist, channel_id, 'mp4', m3u8_id='hls', live=True)
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': channel_id,
|
||||
'title': self._live_title(channel_id),
|
||||
'is_live': True,
|
||||
'age_limit': 18,
|
||||
'formats': formats,
|
||||
}
|
||||
@@ -286,6 +286,9 @@ class VrtNUIE(GigyaBaseIE):
|
||||
'targetEnv': 'jssdk',
|
||||
}))
|
||||
|
||||
if auth_info.get('errorDetails'):
|
||||
raise ExtractorError('Unable to login: VrtNU said: ' + auth_info.get('errorDetails'), expected=True)
|
||||
|
||||
# Sometimes authentication fails for no good reason, retry
|
||||
login_attempt = 1
|
||||
while login_attempt <= 3:
|
||||
|
||||
+209
-277
@@ -1,30 +1,18 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import (
|
||||
compat_str,
|
||||
compat_HTTPError,
|
||||
)
|
||||
from ..utils import (
|
||||
js_to_json,
|
||||
smuggle_url,
|
||||
try_get,
|
||||
xpath_text,
|
||||
xpath_element,
|
||||
xpath_with_ns,
|
||||
find_xpath_attr,
|
||||
orderedSet,
|
||||
parse_duration,
|
||||
parse_iso8601,
|
||||
parse_age_limit,
|
||||
strip_or_none,
|
||||
int_or_none,
|
||||
ExtractorError,
|
||||
)
|
||||
|
||||
@@ -59,6 +47,7 @@ class CBCIE(InfoExtractor):
|
||||
'uploader': 'CBCC-NEW',
|
||||
'timestamp': 1382717907,
|
||||
},
|
||||
'skip': 'No longer available',
|
||||
}, {
|
||||
# with clipId, feed only available via tpfeed.cbc.ca
|
||||
'url': 'http://www.cbc.ca/archives/entry/1978-robin-williams-freestyles-on-90-minutes-live',
|
||||
@@ -209,289 +198,232 @@ class CBCPlayerIE(InfoExtractor):
|
||||
}
|
||||
|
||||
|
||||
class CBCWatchBaseIE(InfoExtractor):
|
||||
_device_id = None
|
||||
_device_token = None
|
||||
_API_BASE_URL = 'https://api-cbc.cloud.clearleap.com/cloffice/client/'
|
||||
_NS_MAP = {
|
||||
'media': 'http://search.yahoo.com/mrss/',
|
||||
'clearleap': 'http://www.clearleap.com/namespace/clearleap/1.0/',
|
||||
}
|
||||
_GEO_COUNTRIES = ['CA']
|
||||
_LOGIN_URL = 'https://api.loginradius.com/identity/v2/auth/login'
|
||||
_TOKEN_URL = 'https://cloud-api.loginradius.com/sso/jwt/api/token'
|
||||
_API_KEY = '3f4beddd-2061-49b0-ae80-6f1f2ed65b37'
|
||||
_NETRC_MACHINE = 'cbcwatch'
|
||||
|
||||
def _signature(self, email, password):
|
||||
data = json.dumps({
|
||||
'email': email,
|
||||
'password': password,
|
||||
}).encode()
|
||||
headers = {'content-type': 'application/json'}
|
||||
query = {'apikey': self._API_KEY}
|
||||
resp = self._download_json(self._LOGIN_URL, None, data=data, headers=headers, query=query)
|
||||
access_token = resp['access_token']
|
||||
|
||||
# token
|
||||
query = {
|
||||
'access_token': access_token,
|
||||
'apikey': self._API_KEY,
|
||||
'jwtapp': 'jwt',
|
||||
}
|
||||
resp = self._download_json(self._TOKEN_URL, None, headers=headers, query=query)
|
||||
return resp['signature']
|
||||
|
||||
def _call_api(self, path, video_id):
|
||||
url = path if path.startswith('http') else self._API_BASE_URL + path
|
||||
for _ in range(2):
|
||||
try:
|
||||
result = self._download_xml(url, video_id, headers={
|
||||
'X-Clearleap-DeviceId': self._device_id,
|
||||
'X-Clearleap-DeviceToken': self._device_token,
|
||||
})
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
|
||||
# Device token has expired, re-acquiring device token
|
||||
self._register_device()
|
||||
continue
|
||||
raise
|
||||
error_message = xpath_text(result, 'userMessage') or xpath_text(result, 'systemMessage')
|
||||
if error_message:
|
||||
raise ExtractorError('%s said: %s' % (self.IE_NAME, error_message))
|
||||
return result
|
||||
|
||||
def _real_initialize(self):
|
||||
if self._valid_device_token():
|
||||
return
|
||||
device = self._downloader.cache.load(
|
||||
'cbcwatch', self._cache_device_key()) or {}
|
||||
self._device_id, self._device_token = device.get('id'), device.get('token')
|
||||
if self._valid_device_token():
|
||||
return
|
||||
self._register_device()
|
||||
|
||||
def _valid_device_token(self):
|
||||
return self._device_id and self._device_token
|
||||
|
||||
def _cache_device_key(self):
|
||||
email, _ = self._get_login_info()
|
||||
return '%s_device' % hashlib.sha256(email.encode()).hexdigest() if email else 'device'
|
||||
|
||||
def _register_device(self):
|
||||
result = self._download_xml(
|
||||
self._API_BASE_URL + 'device/register',
|
||||
None, 'Acquiring device token',
|
||||
data=b'<device><type>web</type></device>')
|
||||
self._device_id = xpath_text(result, 'deviceId', fatal=True)
|
||||
email, password = self._get_login_info()
|
||||
if email and password:
|
||||
signature = self._signature(email, password)
|
||||
data = '<login><token>{0}</token><device><deviceId>{1}</deviceId><type>web</type></device></login>'.format(
|
||||
escape(signature), escape(self._device_id)).encode()
|
||||
url = self._API_BASE_URL + 'device/login'
|
||||
result = self._download_xml(
|
||||
url, None, data=data,
|
||||
headers={'content-type': 'application/xml'})
|
||||
self._device_token = xpath_text(result, 'token', fatal=True)
|
||||
else:
|
||||
self._device_token = xpath_text(result, 'deviceToken', fatal=True)
|
||||
self._downloader.cache.store(
|
||||
'cbcwatch', self._cache_device_key(), {
|
||||
'id': self._device_id,
|
||||
'token': self._device_token,
|
||||
})
|
||||
|
||||
def _parse_rss_feed(self, rss):
|
||||
channel = xpath_element(rss, 'channel', fatal=True)
|
||||
|
||||
def _add_ns(path):
|
||||
return xpath_with_ns(path, self._NS_MAP)
|
||||
|
||||
entries = []
|
||||
for item in channel.findall('item'):
|
||||
guid = xpath_text(item, 'guid', fatal=True)
|
||||
title = xpath_text(item, 'title', fatal=True)
|
||||
|
||||
media_group = xpath_element(item, _add_ns('media:group'), fatal=True)
|
||||
content = xpath_element(media_group, _add_ns('media:content'), fatal=True)
|
||||
content_url = content.attrib['url']
|
||||
|
||||
thumbnails = []
|
||||
for thumbnail in media_group.findall(_add_ns('media:thumbnail')):
|
||||
thumbnail_url = thumbnail.get('url')
|
||||
if not thumbnail_url:
|
||||
continue
|
||||
thumbnails.append({
|
||||
'id': thumbnail.get('profile'),
|
||||
'url': thumbnail_url,
|
||||
'width': int_or_none(thumbnail.get('width')),
|
||||
'height': int_or_none(thumbnail.get('height')),
|
||||
})
|
||||
|
||||
timestamp = None
|
||||
release_date = find_xpath_attr(
|
||||
item, _add_ns('media:credit'), 'role', 'releaseDate')
|
||||
if release_date is not None:
|
||||
timestamp = parse_iso8601(release_date.text)
|
||||
|
||||
entries.append({
|
||||
'_type': 'url_transparent',
|
||||
'url': content_url,
|
||||
'id': guid,
|
||||
'title': title,
|
||||
'description': xpath_text(item, 'description'),
|
||||
'timestamp': timestamp,
|
||||
'duration': int_or_none(content.get('duration')),
|
||||
'age_limit': parse_age_limit(xpath_text(item, _add_ns('media:rating'))),
|
||||
'episode': xpath_text(item, _add_ns('clearleap:episode')),
|
||||
'episode_number': int_or_none(xpath_text(item, _add_ns('clearleap:episodeInSeason'))),
|
||||
'series': xpath_text(item, _add_ns('clearleap:series')),
|
||||
'season_number': int_or_none(xpath_text(item, _add_ns('clearleap:season'))),
|
||||
'thumbnails': thumbnails,
|
||||
'ie_key': 'CBCWatchVideo',
|
||||
})
|
||||
|
||||
return self.playlist_result(
|
||||
entries, xpath_text(channel, 'guid'),
|
||||
xpath_text(channel, 'title'),
|
||||
xpath_text(channel, 'description'))
|
||||
|
||||
|
||||
class CBCWatchVideoIE(CBCWatchBaseIE):
|
||||
IE_NAME = 'cbc.ca:watch:video'
|
||||
_VALID_URL = r'https?://api-cbc\.cloud\.clearleap\.com/cloffice/client/web/play/?\?.*?\bcontentId=(?P<id>[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})'
|
||||
_TEST = {
|
||||
# geo-restricted to Canada, bypassable
|
||||
'url': 'https://api-cbc.cloud.clearleap.com/cloffice/client/web/play/?contentId=3c84472a-1eea-4dee-9267-2655d5055dcf&categoryId=ebc258f5-ee40-4cca-b66b-ba6bd55b7235',
|
||||
'only_matching': True,
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
result = self._call_api(url, video_id)
|
||||
|
||||
m3u8_url = xpath_text(result, 'url', fatal=True)
|
||||
formats = self._extract_m3u8_formats(re.sub(r'/([^/]+)/[^/?]+\.m3u8', r'/\1/\1.m3u8', m3u8_url), video_id, 'mp4', fatal=False)
|
||||
if len(formats) < 2:
|
||||
formats = self._extract_m3u8_formats(m3u8_url, video_id, 'mp4')
|
||||
for f in formats:
|
||||
format_id = f.get('format_id')
|
||||
if format_id.startswith('AAC'):
|
||||
f['acodec'] = 'aac'
|
||||
elif format_id.startswith('AC3'):
|
||||
f['acodec'] = 'ac-3'
|
||||
self._sort_formats(formats)
|
||||
|
||||
info = {
|
||||
'id': video_id,
|
||||
'title': video_id,
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
rss = xpath_element(result, 'rss')
|
||||
if rss:
|
||||
info.update(self._parse_rss_feed(rss)['entries'][0])
|
||||
del info['url']
|
||||
del info['_type']
|
||||
del info['ie_key']
|
||||
return info
|
||||
|
||||
|
||||
class CBCWatchIE(CBCWatchBaseIE):
|
||||
IE_NAME = 'cbc.ca:watch'
|
||||
_VALID_URL = r'https?://(?:gem|watch)\.cbc\.ca/(?:[^/]+/)+(?P<id>[0-9a-f-]+)'
|
||||
class CBCGemIE(InfoExtractor):
|
||||
IE_NAME = 'gem.cbc.ca'
|
||||
_VALID_URL = r'https?://gem\.cbc\.ca/media/(?P<id>[0-9a-z-]+/s[0-9]+[a-z][0-9]+)'
|
||||
_TESTS = [{
|
||||
# geo-restricted to Canada, bypassable
|
||||
'url': 'http://watch.cbc.ca/doc-zone/season-6/customer-disservice/38e815a-009e3ab12e4',
|
||||
# This is a normal, public, TV show video
|
||||
'url': 'https://gem.cbc.ca/media/schitts-creek/s06e01',
|
||||
'md5': '93dbb31c74a8e45b378cf13bd3f6f11e',
|
||||
'info_dict': {
|
||||
'id': '9673749a-5e77-484c-8b62-a1092a6b5168',
|
||||
'id': 'schitts-creek/s06e01',
|
||||
'ext': 'mp4',
|
||||
'title': 'Customer (Dis)Service',
|
||||
'description': 'md5:8bdd6913a0fe03d4b2a17ebe169c7c87',
|
||||
'upload_date': '20160219',
|
||||
'timestamp': 1455840000,
|
||||
},
|
||||
'params': {
|
||||
# m3u8 download
|
||||
'skip_download': True,
|
||||
'format': 'bestvideo',
|
||||
'title': 'Smoke Signals',
|
||||
'description': 'md5:929868d20021c924020641769eb3e7f1',
|
||||
'thumbnail': 'https://images.radio-canada.ca/v1/synps-cbc/episode/perso/cbc_schitts_creek_season_06e01_thumbnail_v01.jpg?im=Resize=(Size)',
|
||||
'duration': 1314,
|
||||
'categories': ['comedy'],
|
||||
'series': 'Schitt\'s Creek',
|
||||
'season': 'Season 6',
|
||||
'season_number': 6,
|
||||
'episode': 'Smoke Signals',
|
||||
'episode_number': 1,
|
||||
'episode_id': 'schitts-creek/s06e01',
|
||||
},
|
||||
'params': {'format': 'bv'},
|
||||
'skip': 'Geo-restricted to Canada',
|
||||
}, {
|
||||
# geo-restricted to Canada, bypassable
|
||||
'url': 'http://watch.cbc.ca/arthur/all/1ed4b385-cd84-49cf-95f0-80f004680057',
|
||||
# This video requires an account in the browser, but works fine in yt-dlp
|
||||
'url': 'https://gem.cbc.ca/media/schitts-creek/s01e01',
|
||||
'md5': '297a9600f554f2258aed01514226a697',
|
||||
'info_dict': {
|
||||
'id': '1ed4b385-cd84-49cf-95f0-80f004680057',
|
||||
'title': 'Arthur',
|
||||
'description': 'Arthur, the sweetest 8-year-old aardvark, and his pals solve all kinds of problems with humour, kindness and teamwork.',
|
||||
'id': 'schitts-creek/s01e01',
|
||||
'ext': 'mp4',
|
||||
'title': 'The Cup Runneth Over',
|
||||
'description': 'md5:9bca14ea49ab808097530eb05a29e797',
|
||||
'thumbnail': 'https://images.radio-canada.ca/v1/synps-cbc/episode/perso/cbc_schitts_creek_season_01e01_thumbnail_v01.jpg?im=Resize=(Size)',
|
||||
'series': 'Schitt\'s Creek',
|
||||
'season_number': 1,
|
||||
'season': 'Season 1',
|
||||
'episode_number': 1,
|
||||
'episode': 'The Cup Runneth Over',
|
||||
'episode_id': 'schitts-creek/s01e01',
|
||||
'duration': 1309,
|
||||
'categories': ['comedy'],
|
||||
},
|
||||
'playlist_mincount': 30,
|
||||
}, {
|
||||
'url': 'https://gem.cbc.ca/media/this-hour-has-22-minutes/season-26/episode-20/38e815a-0108c6c6a42',
|
||||
'only_matching': True,
|
||||
'params': {'format': 'bv'},
|
||||
'skip': 'Geo-restricted to Canada',
|
||||
}]
|
||||
_API_BASE = 'https://services.radio-canada.ca/ott/cbc-api/v2/assets/'
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
rss = self._call_api('web/browse/' + video_id, video_id)
|
||||
return self._parse_rss_feed(rss)
|
||||
video_info = self._download_json(self._API_BASE + video_id, video_id)
|
||||
|
||||
|
||||
class CBCOlympicsIE(InfoExtractor):
|
||||
IE_NAME = 'cbc.ca:olympics'
|
||||
_VALID_URL = r'https?://olympics\.cbc\.ca/video/[^/]+/(?P<id>[^/?#]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://olympics.cbc.ca/video/whats-on-tv/olympic-morning-featuring-the-opening-ceremony/',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
video_id = self._hidden_inputs(webpage)['videoId']
|
||||
video_doc = self._download_xml(
|
||||
'https://olympics.cbc.ca/videodata/%s.xml' % video_id, video_id)
|
||||
title = xpath_text(video_doc, 'title', fatal=True)
|
||||
is_live = xpath_text(video_doc, 'kind') == 'Live'
|
||||
if is_live:
|
||||
title = self._live_title(title)
|
||||
|
||||
formats = []
|
||||
for video_source in video_doc.findall('videoSources/videoSource'):
|
||||
uri = xpath_text(video_source, 'uri')
|
||||
if not uri:
|
||||
continue
|
||||
tokenize = self._download_json(
|
||||
'https://olympics.cbc.ca/api/api-akamai/tokenize',
|
||||
video_id, data=json.dumps({
|
||||
'VideoSource': uri,
|
||||
}).encode(), headers={
|
||||
'Content-Type': 'application/json',
|
||||
'Referer': url,
|
||||
# d3.VideoPlayer._init in https://olympics.cbc.ca/components/script/base.js
|
||||
'Cookie': '_dvp=TK:C0ObxjerU', # AKAMAI CDN cookie
|
||||
}, fatal=False)
|
||||
if not tokenize:
|
||||
continue
|
||||
content_url = tokenize['ContentUrl']
|
||||
video_source_format = video_source.get('format')
|
||||
if video_source_format == 'IIS':
|
||||
formats.extend(self._extract_ism_formats(
|
||||
content_url, video_id, ism_id=video_source_format, fatal=False))
|
||||
last_error = None
|
||||
attempt = -1
|
||||
retries = self.get_param('extractor_retries', 15)
|
||||
while attempt < retries:
|
||||
attempt += 1
|
||||
if last_error:
|
||||
self.report_warning('%s. Retrying ...' % last_error)
|
||||
m3u8_info = self._download_json(
|
||||
video_info['playSession']['url'], video_id,
|
||||
note='Downloading JSON metadata%s' % f' (attempt {attempt})')
|
||||
m3u8_url = m3u8_info.get('url')
|
||||
if m3u8_url:
|
||||
break
|
||||
elif m3u8_info.get('errorCode') == 1:
|
||||
self.raise_geo_restricted(countries=['CA'])
|
||||
else:
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
content_url, video_id, 'mp4',
|
||||
'm3u8' if is_live else 'm3u8_native',
|
||||
m3u8_id=video_source_format, fatal=False))
|
||||
last_error = f'{self.IE_NAME} said: {m3u8_info.get("errorCode")} - {m3u8_info.get("message")}'
|
||||
# 35 means media unavailable, but retries work
|
||||
if m3u8_info.get('errorCode') != 35 or attempt >= retries:
|
||||
raise ExtractorError(last_error)
|
||||
|
||||
formats = self._extract_m3u8_formats(m3u8_url, video_id, m3u8_id='hls')
|
||||
self._remove_duplicate_formats(formats)
|
||||
|
||||
for i, format in enumerate(formats):
|
||||
if format.get('vcodec') == 'none':
|
||||
if format.get('ext') is None:
|
||||
format['ext'] = 'm4a'
|
||||
if format.get('acodec') is None:
|
||||
format['acodec'] = 'mp4a.40.2'
|
||||
|
||||
# Put described audio at the beginning of the list, so that it
|
||||
# isn't chosen by default, as most people won't want it.
|
||||
if 'descriptive' in format['format_id'].lower():
|
||||
format['preference'] = -2
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'display_id': display_id,
|
||||
'title': title,
|
||||
'description': xpath_text(video_doc, 'description'),
|
||||
'thumbnail': xpath_text(video_doc, 'thumbnailUrl'),
|
||||
'duration': parse_duration(xpath_text(video_doc, 'duration')),
|
||||
'title': video_info['title'],
|
||||
'description': video_info.get('description'),
|
||||
'thumbnail': video_info.get('image'),
|
||||
'series': video_info.get('series'),
|
||||
'season_number': video_info.get('season'),
|
||||
'season': f'Season {video_info.get("season")}',
|
||||
'episode_number': video_info.get('episode'),
|
||||
'episode': video_info.get('title'),
|
||||
'episode_id': video_id,
|
||||
'duration': video_info.get('duration'),
|
||||
'categories': [video_info.get('category')],
|
||||
'formats': formats,
|
||||
'is_live': is_live,
|
||||
'release_timestamp': video_info.get('airDate'),
|
||||
'timestamp': video_info.get('availableDate'),
|
||||
}
|
||||
|
||||
|
||||
class CBCGemPlaylistIE(InfoExtractor):
|
||||
IE_NAME = 'gem.cbc.ca:playlist'
|
||||
_VALID_URL = r'https?://gem\.cbc\.ca/media/(?P<id>(?P<show>[0-9a-z-]+)/s(?P<season>[0-9]+))/?(?:[?#]|$)'
|
||||
_TESTS = [{
|
||||
# geo-restricted to Canada, bypassable
|
||||
# TV show playlist, all public videos
|
||||
'url': 'https://gem.cbc.ca/media/schitts-creek/s06',
|
||||
'playlist_count': 16,
|
||||
'info_dict': {
|
||||
'id': 'schitts-creek/s06',
|
||||
'title': 'Season 6',
|
||||
'description': 'md5:6a92104a56cbeb5818cc47884d4326a2',
|
||||
},
|
||||
'skip': 'Geo-restricted to Canada',
|
||||
}]
|
||||
_API_BASE = 'https://services.radio-canada.ca/ott/cbc-api/v2/shows/'
|
||||
|
||||
def _real_extract(self, url):
|
||||
match = self._match_valid_url(url)
|
||||
season_id = match.group('id')
|
||||
show = match.group('show')
|
||||
show_info = self._download_json(self._API_BASE + show, season_id)
|
||||
season = int(match.group('season'))
|
||||
season_info = try_get(show_info, lambda x: x['seasons'][season - 1])
|
||||
|
||||
if season_info is None:
|
||||
raise ExtractorError(f'Couldn\'t find season {season} of {show}')
|
||||
|
||||
episodes = []
|
||||
for episode in season_info['assets']:
|
||||
episodes.append({
|
||||
'_type': 'url_transparent',
|
||||
'ie_key': 'CBCGem',
|
||||
'url': 'https://gem.cbc.ca/media/' + episode['id'],
|
||||
'id': episode['id'],
|
||||
'title': episode.get('title'),
|
||||
'description': episode.get('description'),
|
||||
'thumbnail': episode.get('image'),
|
||||
'series': episode.get('series'),
|
||||
'season_number': episode.get('season'),
|
||||
'season': season_info['title'],
|
||||
'season_id': season_info.get('id'),
|
||||
'episode_number': episode.get('episode'),
|
||||
'episode': episode.get('title'),
|
||||
'episode_id': episode['id'],
|
||||
'duration': episode.get('duration'),
|
||||
'categories': [episode.get('category')],
|
||||
})
|
||||
|
||||
thumbnail = None
|
||||
tn_uri = season_info.get('image')
|
||||
# the-national was observed to use a "data:image/png;base64"
|
||||
# URI for their 'image' value. The image was 1x1, and is
|
||||
# probably just a placeholder, so it is ignored.
|
||||
if tn_uri is not None and not tn_uri.startswith('data:'):
|
||||
thumbnail = tn_uri
|
||||
|
||||
return {
|
||||
'_type': 'playlist',
|
||||
'entries': episodes,
|
||||
'id': season_id,
|
||||
'title': season_info['title'],
|
||||
'description': season_info.get('description'),
|
||||
'thumbnail': thumbnail,
|
||||
'series': show_info.get('title'),
|
||||
'season_number': season_info.get('season'),
|
||||
'season': season_info['title'],
|
||||
}
|
||||
|
||||
|
||||
class CBCGemLiveIE(InfoExtractor):
|
||||
IE_NAME = 'gem.cbc.ca:live'
|
||||
_VALID_URL = r'https?://gem\.cbc\.ca/live/(?P<id>[0-9]{12})'
|
||||
_TEST = {
|
||||
'url': 'https://gem.cbc.ca/live/920604739687',
|
||||
'info_dict': {
|
||||
'title': 'Ottawa',
|
||||
'description': 'The live TV channel and local programming from Ottawa',
|
||||
'thumbnail': 'https://thumbnails.cbc.ca/maven_legacy/thumbnails/CBC_OTT_VMS/Live_Channel_Static_Images/Ottawa_2880x1620.jpg',
|
||||
'is_live': True,
|
||||
'id': 'AyqZwxRqh8EH',
|
||||
'ext': 'mp4',
|
||||
'timestamp': 1492106160,
|
||||
'upload_date': '20170413',
|
||||
'uploader': 'CBCC-NEW',
|
||||
},
|
||||
'skip': 'Live might have ended',
|
||||
}
|
||||
|
||||
# It's unclear where the chars at the end come from, but they appear to be
|
||||
# constant. Might need updating in the future.
|
||||
_API = 'https://tpfeed.cbc.ca/f/ExhSPC/t_t3UKJR6MAT'
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
live_info = self._download_json(self._API, video_id)['entries']
|
||||
|
||||
video_info = None
|
||||
for stream in live_info:
|
||||
if stream.get('guid') == video_id:
|
||||
video_info = stream
|
||||
|
||||
if video_info is None:
|
||||
raise ExtractorError(
|
||||
'Couldn\'t find video metadata, maybe this livestream is now offline',
|
||||
expected=True)
|
||||
|
||||
return {
|
||||
'_type': 'url_transparent',
|
||||
'ie_key': 'ThePlatform',
|
||||
'url': video_info['content'][0]['url'],
|
||||
'id': video_id,
|
||||
'title': video_info.get('title'),
|
||||
'description': video_info.get('description'),
|
||||
'tags': try_get(video_info, lambda x: x['keywords'].split(', ')),
|
||||
'thumbnail': video_info.get('cbc$staticImage'),
|
||||
'is_live': True,
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@ class CBSIE(CBSBaseIE):
|
||||
title = xpath_text(video_data, 'videoTitle', 'title') or xpath_text(video_data, 'videotitle', 'title')
|
||||
|
||||
asset_types = {}
|
||||
has_drm = False
|
||||
for item in items_data.findall('.//item'):
|
||||
asset_type = xpath_text(item, 'assetType')
|
||||
query = {
|
||||
@@ -144,6 +145,8 @@ class CBSIE(CBSBaseIE):
|
||||
if asset_type in asset_types:
|
||||
continue
|
||||
elif any(excluded in asset_type for excluded in ('HLS_FPS', 'DASH_CENC', 'OnceURL')):
|
||||
if 'DASH_CENC' in asset_type:
|
||||
has_drm = True
|
||||
continue
|
||||
if asset_type.startswith('HLS') or 'StreamPack' in asset_type:
|
||||
query['formats'] = 'MPEG4,M3U'
|
||||
@@ -151,6 +154,9 @@ class CBSIE(CBSBaseIE):
|
||||
query['formats'] = 'MPEG4,FLV'
|
||||
asset_types[asset_type] = query
|
||||
|
||||
if not asset_types and has_drm:
|
||||
self.report_drm(content_id)
|
||||
|
||||
return self._extract_common_video_info(content_id, asset_types, mpx_acc, extra_info={
|
||||
'title': title,
|
||||
'series': xpath_text(video_data, 'seriesTitle'),
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
try_get,
|
||||
unified_timestamp,
|
||||
)
|
||||
|
||||
|
||||
class CGTNIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://news\.cgtn\.com/news/[0-9]{4}-[0-9]{2}-[0-9]{2}/[a-zA-Z0-9-]+-(?P<id>[a-zA-Z0-9-]+)/index\.html'
|
||||
_TESTS = [
|
||||
{
|
||||
'url': 'https://news.cgtn.com/news/2021-03-09/Up-and-Out-of-Poverty-Ep-1-A-solemn-promise-YuOUaOzGQU/index.html',
|
||||
'info_dict': {
|
||||
'id': 'YuOUaOzGQU',
|
||||
'ext': 'mp4',
|
||||
'title': 'Up and Out of Poverty Ep. 1: A solemn promise',
|
||||
'thumbnail': r're:^https?://.*\.jpg$',
|
||||
'timestamp': 1615295940,
|
||||
'upload_date': '20210309',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True
|
||||
}
|
||||
}, {
|
||||
'url': 'https://news.cgtn.com/news/2021-06-06/China-Indonesia-vow-to-further-deepen-maritime-cooperation-10REvJCewCY/index.html',
|
||||
'info_dict': {
|
||||
'id': '10REvJCewCY',
|
||||
'ext': 'mp4',
|
||||
'title': 'China, Indonesia vow to further deepen maritime cooperation',
|
||||
'thumbnail': r're:^https?://.*\.png$',
|
||||
'description': 'China and Indonesia vowed to upgrade their cooperation into the maritime sector and also for political security, economy, and cultural and people-to-people exchanges.',
|
||||
'author': 'CGTN',
|
||||
'category': 'China',
|
||||
'timestamp': 1622950200,
|
||||
'upload_date': '20210606',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': False
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
download_url = self._html_search_regex(r'data-video ="(?P<url>.+m3u8)"', webpage, 'download_url')
|
||||
datetime_str = self._html_search_regex(r'<span class="date">\s*(.+?)\s*</span>', webpage, 'datetime_str', fatal=False)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': self._og_search_title(webpage),
|
||||
'description': self._og_search_description(webpage, default=None),
|
||||
'thumbnail': self._og_search_thumbnail(webpage),
|
||||
'formats': self._extract_m3u8_formats(download_url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls'),
|
||||
'category': self._html_search_regex(r'<span class="section">\s*(.+?)\s*</span>',
|
||||
webpage, 'category', fatal=False),
|
||||
'author': self._html_search_regex(r'<div class="news-author-name">\s*(.+?)\s*</div>',
|
||||
webpage, 'author', default=None, fatal=False),
|
||||
'timestamp': try_get(unified_timestamp(datetime_str), lambda x: x - 8 * 3600),
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
import json
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_urllib_parse_unquote_plus
|
||||
from ..utils import (
|
||||
clean_html,
|
||||
ExtractorError,
|
||||
int_or_none,
|
||||
str_to_int,
|
||||
url_or_none,
|
||||
)
|
||||
|
||||
|
||||
class ChingariBaseIE(InfoExtractor):
|
||||
def _get_post(self, id, post_data):
|
||||
media_data = post_data['mediaLocation']
|
||||
base_url = media_data['base']
|
||||
author_data = post_data.get('authorData', {})
|
||||
song_data = post_data.get('song', {}) # revist this in future for differentiating b/w 'art' and 'author'
|
||||
|
||||
formats = [{
|
||||
'format_id': frmt,
|
||||
'width': str_to_int(frmt[1:]),
|
||||
'url': base_url + frmt_path,
|
||||
} for frmt, frmt_path in media_data.get('transcoded', {}).items()]
|
||||
|
||||
if media_data.get('path'):
|
||||
formats.append({
|
||||
'format_id': 'original',
|
||||
'format_note': 'Direct video.',
|
||||
'url': base_url + '/apipublic' + media_data['path'],
|
||||
'quality': 10,
|
||||
})
|
||||
self._sort_formats(formats)
|
||||
timestamp = str_to_int(post_data.get('created_at'))
|
||||
if timestamp:
|
||||
timestamp = int_or_none(timestamp, 1000)
|
||||
|
||||
thumbnail, uploader_url = None, None
|
||||
if media_data.get('thumbnail'):
|
||||
thumbnail = base_url + media_data.get('thumbnail')
|
||||
if author_data.get('username'):
|
||||
uploader_url = 'https://chingari.io/' + author_data.get('username')
|
||||
|
||||
return {
|
||||
'id': id,
|
||||
'title': compat_urllib_parse_unquote_plus(clean_html(post_data.get('caption'))),
|
||||
'description': compat_urllib_parse_unquote_plus(clean_html(post_data.get('caption'))),
|
||||
'duration': media_data.get('duration'),
|
||||
'thumbnail': url_or_none(thumbnail),
|
||||
'like_count': post_data.get('likeCount'),
|
||||
'view_count': post_data.get('viewsCount'),
|
||||
'comment_count': post_data.get('commentCount'),
|
||||
'repost_count': post_data.get('shareCount'),
|
||||
'timestamp': timestamp,
|
||||
'uploader_id': post_data.get('userId') or author_data.get('_id'),
|
||||
'uploader': author_data.get('name'),
|
||||
'uploader_url': url_or_none(uploader_url),
|
||||
'track': song_data.get('title'),
|
||||
'artist': song_data.get('author'),
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
|
||||
class ChingariIE(ChingariBaseIE):
|
||||
_VALID_URL = r'(?:https?://)(?:www\.)?chingari\.io/share/post\?id=(?P<id>[^&/#?]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://chingari.io/share/post?id=612f8f4ce1dc57090e8a7beb',
|
||||
'info_dict': {
|
||||
'id': '612f8f4ce1dc57090e8a7beb',
|
||||
'ext': 'mp4',
|
||||
'title': 'Happy birthday Srila Prabhupada',
|
||||
'description': 'md5:c7080ebfdfeb06016e638c286d6bc3fa',
|
||||
'duration': 0,
|
||||
'thumbnail': 'https://media.chingari.io/uploads/c41d30e2-06b6-4e3b-9b4b-edbb929cec06-1630506826911/thumbnail/198f993f-ce87-4623-82c6-cd071bd6d4f4-1630506828016.jpg',
|
||||
'like_count': int,
|
||||
'view_count': int,
|
||||
'comment_count': int,
|
||||
'repost_count': int,
|
||||
'timestamp': 1630506828,
|
||||
'upload_date': '20210901',
|
||||
'uploader_id': '5f0403982c8bd344f4813f8c',
|
||||
'uploader': 'ISKCON,Inc.',
|
||||
'uploader_url': 'https://chingari.io/iskcon,inc',
|
||||
'track': None,
|
||||
'artist': None,
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
id = self._match_id(url)
|
||||
post_json = self._download_json(f'https://api.chingari.io/post/post_details/{id}', id)
|
||||
if post_json['code'] != 200:
|
||||
raise ExtractorError(post_json['message'], expected=True)
|
||||
post_data = post_json['data']
|
||||
return self._get_post(id, post_data)
|
||||
|
||||
|
||||
class ChingariUserIE(ChingariBaseIE):
|
||||
_VALID_URL = r'(?:https?://)(?:www\.)?chingari\.io/(?!share/post)(?P<id>[^/?]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://chingari.io/dada1023',
|
||||
'playlist_mincount': 3,
|
||||
'info_dict': {
|
||||
'id': 'dada1023',
|
||||
},
|
||||
'entries': [{
|
||||
'url': 'https://chingari.io/share/post?id=614781f3ade60b3a0bfff42a',
|
||||
'info_dict': {
|
||||
'id': '614781f3ade60b3a0bfff42a',
|
||||
'ext': 'mp4',
|
||||
'title': '#chingaribappa ',
|
||||
'description': 'md5:d1df21d84088770468fa63afe3b17857',
|
||||
'duration': 7,
|
||||
'thumbnail': 'https://media.chingari.io/uploads/346d86d4-abb2-474e-a164-ffccf2bbcb72-1632076273717/thumbnail/b0b3aac2-2b86-4dd1-909d-9ed6e57cf77c-1632076275552.jpg',
|
||||
'like_count': int,
|
||||
'view_count': int,
|
||||
'comment_count': int,
|
||||
'repost_count': int,
|
||||
'timestamp': 1632076275,
|
||||
'upload_date': '20210919',
|
||||
'uploader_id': '5efc4b12cca35c3d1794c2d3',
|
||||
'uploader': 'dada (girish) dhawale',
|
||||
'uploader_url': 'https://chingari.io/dada1023',
|
||||
'track': None,
|
||||
'artist': None
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, {
|
||||
'url': 'https://chingari.io/share/post?id=6146b132bcbf860959e12cba',
|
||||
'info_dict': {
|
||||
'id': '6146b132bcbf860959e12cba',
|
||||
'ext': 'mp4',
|
||||
'title': 'Tactor harvesting',
|
||||
'description': 'md5:8403f12dce68828b77ecee7eb7e887b7',
|
||||
'duration': 59.3,
|
||||
'thumbnail': 'https://media.chingari.io/uploads/b353ca70-7a87-400d-93a6-fa561afaec86-1632022814584/thumbnail/c09302e3-2043-41b1-a2fe-77d97e5bd676-1632022834260.jpg',
|
||||
'like_count': int,
|
||||
'view_count': int,
|
||||
'comment_count': int,
|
||||
'repost_count': int,
|
||||
'timestamp': 1632022834,
|
||||
'upload_date': '20210919',
|
||||
'uploader_id': '5efc4b12cca35c3d1794c2d3',
|
||||
'uploader': 'dada (girish) dhawale',
|
||||
'uploader_url': 'https://chingari.io/dada1023',
|
||||
'track': None,
|
||||
'artist': None
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, {
|
||||
'url': 'https://chingari.io/share/post?id=6145651b74cb030a64c40b82',
|
||||
'info_dict': {
|
||||
'id': '6145651b74cb030a64c40b82',
|
||||
'ext': 'mp4',
|
||||
'title': '#odiabhajan ',
|
||||
'description': 'md5:687ea36835b9276cf2af90f25e7654cb',
|
||||
'duration': 56.67,
|
||||
'thumbnail': 'https://media.chingari.io/uploads/6cbf216b-babc-4cce-87fe-ceaac8d706ac-1631937782708/thumbnail/8855754f-6669-48ce-b269-8cc0699ed6da-1631937819522.jpg',
|
||||
'like_count': int,
|
||||
'view_count': int,
|
||||
'comment_count': int,
|
||||
'repost_count': int,
|
||||
'timestamp': 1631937819,
|
||||
'upload_date': '20210918',
|
||||
'uploader_id': '5efc4b12cca35c3d1794c2d3',
|
||||
'uploader': 'dada (girish) dhawale',
|
||||
'uploader_url': 'https://chingari.io/dada1023',
|
||||
'track': None,
|
||||
'artist': None
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}],
|
||||
}, {
|
||||
'url': 'https://chingari.io/iskcon%2Cinc',
|
||||
'playlist_mincount': 1025,
|
||||
'info_dict': {
|
||||
'id': 'iskcon%2Cinc',
|
||||
},
|
||||
}]
|
||||
|
||||
def _entries(self, id):
|
||||
skip = 0
|
||||
has_more = True
|
||||
for page in itertools.count():
|
||||
posts = self._download_json('https://api.chingari.io/users/getPosts', id,
|
||||
data=json.dumps({'userId': id, 'ownerId': id, 'skip': skip, 'limit': 20}).encode(),
|
||||
headers={'content-type': 'application/json;charset=UTF-8'},
|
||||
note='Downloading page %s' % page)
|
||||
for post in posts.get('data', []):
|
||||
post_data = post['post']
|
||||
yield self._get_post(post_data['_id'], post_data)
|
||||
skip += 20
|
||||
has_more = posts['hasMoreData']
|
||||
if not has_more:
|
||||
break
|
||||
|
||||
def _real_extract(self, url):
|
||||
alt_id = self._match_id(url)
|
||||
post_json = self._download_json(f'https://api.chingari.io/user/{alt_id}', alt_id)
|
||||
if post_json['code'] != 200:
|
||||
raise ExtractorError(post_json['message'], expected=True)
|
||||
id = post_json['data']['_id']
|
||||
return self.playlist_result(self._entries(id), playlist_id=alt_id)
|
||||
@@ -4,7 +4,7 @@ from .mtv import MTVServicesInfoExtractor
|
||||
|
||||
|
||||
class ComedyCentralIE(MTVServicesInfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?cc\.com/(?:episodes|video(?:-clips)?)/(?P<id>[0-9a-z]{6})'
|
||||
_VALID_URL = r'https?://(?:www\.)?cc\.com/(?:episodes|video(?:-clips)?|collection-playlist)/(?P<id>[0-9a-z]{6})'
|
||||
_FEED_URL = 'http://comedycentral.com/feeds/mrss/'
|
||||
|
||||
_TESTS = [{
|
||||
@@ -24,6 +24,9 @@ class ComedyCentralIE(MTVServicesInfoExtractor):
|
||||
}, {
|
||||
'url': 'https://www.cc.com/video/k3sdvm/the-daily-show-with-jon-stewart-exclusive-the-fourth-estate',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://www.cc.com/collection-playlist/cosnej/stand-up-specials/t6vtjb',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from ..compat import (
|
||||
compat_cookies_SimpleCookie,
|
||||
compat_etree_Element,
|
||||
compat_etree_fromstring,
|
||||
compat_expanduser,
|
||||
compat_getpass,
|
||||
compat_http_client,
|
||||
compat_os_name,
|
||||
@@ -788,9 +789,10 @@ class InfoExtractor(object):
|
||||
self._downloader.to_screen(dump)
|
||||
if self.get_param('write_pages', False):
|
||||
basen = '%s_%s' % (video_id, urlh.geturl())
|
||||
if len(basen) > 240:
|
||||
trim_length = self.get_param('trim_file_name') or 240
|
||||
if len(basen) > trim_length:
|
||||
h = '___' + hashlib.md5(basen.encode('utf-8')).hexdigest()
|
||||
basen = basen[:240 - len(h)] + h
|
||||
basen = basen[:trim_length - len(h)] + h
|
||||
raw_filename = basen + '.dump'
|
||||
filename = sanitize_filename(raw_filename, restricted=True)
|
||||
self.to_screen('Saving request to ' + filename)
|
||||
@@ -1166,7 +1168,10 @@ class InfoExtractor(object):
|
||||
|
||||
if self.get_param('usenetrc', False):
|
||||
try:
|
||||
info = netrc.netrc().authenticators(netrc_machine)
|
||||
netrc_file = compat_expanduser(self.get_param('netrc_location') or '~')
|
||||
if os.path.isdir(netrc_file):
|
||||
netrc_file = os.path.join(netrc_file, '.netrc')
|
||||
info = netrc.netrc(file=netrc_file).authenticators(netrc_machine)
|
||||
if info is not None:
|
||||
username = info[0]
|
||||
password = info[2]
|
||||
@@ -2618,8 +2623,10 @@ class InfoExtractor(object):
|
||||
base_url = base_url_e.text + base_url
|
||||
if re.match(r'^https?://', base_url):
|
||||
break
|
||||
if mpd_base_url and not re.match(r'^https?://', base_url):
|
||||
if not mpd_base_url.endswith('/') and not base_url.startswith('/'):
|
||||
if mpd_base_url and base_url.startswith('/'):
|
||||
base_url = compat_urlparse.urljoin(mpd_base_url, base_url)
|
||||
elif mpd_base_url and not re.match(r'^https?://', base_url):
|
||||
if not mpd_base_url.endswith('/'):
|
||||
mpd_base_url += '/'
|
||||
base_url = mpd_base_url + base_url
|
||||
representation_id = representation_attrib.get('id')
|
||||
@@ -3496,9 +3503,11 @@ class InfoExtractor(object):
|
||||
raise NotImplementedError('This method must be implemented by subclasses')
|
||||
|
||||
def mark_watched(self, *args, **kwargs):
|
||||
if (self.get_param('mark_watched', False)
|
||||
and (self._get_login_info()[0] is not None
|
||||
or self.get_param('cookiefile') is not None)):
|
||||
if not self.get_param('mark_watched', False):
|
||||
return
|
||||
if (self._get_login_info()[0] is not None
|
||||
or self.get_param('cookiefile')
|
||||
or self.get_param('cookiesfrombrowser')):
|
||||
self._mark_watched(*args, **kwargs)
|
||||
|
||||
def _mark_watched(self, *args, **kwargs):
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import ExtractorError, clean_html, int_or_none, try_get, unified_strdate
|
||||
from ..compat import compat_str
|
||||
|
||||
|
||||
class DamtomoBaseIE(InfoExtractor):
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage, handle = self._download_webpage_handle(self._WEBPAGE_URL_TMPL % video_id, video_id, encoding='sjis')
|
||||
|
||||
if handle.url == 'https://www.clubdam.com/sorry/':
|
||||
raise ExtractorError('You are rate-limited. Try again later.', expected=True)
|
||||
if '<h2>予期せぬエラーが発生しました。</h2>' in webpage:
|
||||
raise ExtractorError('There is an error on server-side. Try again later.', expected=True)
|
||||
|
||||
description = self._search_regex(r'(?m)<div id="public_comment">\s*<p>\s*([^<]*?)\s*</p>', webpage, 'description', default=None)
|
||||
uploader_id = self._search_regex(r'<a href="https://www\.clubdam\.com/app/damtomo/member/info/Profile\.do\?damtomoId=([^"]+)"', webpage, 'uploader_id', default=None)
|
||||
|
||||
data_dict = {
|
||||
mobj.group('class'): re.sub(r'\s+', ' ', clean_html(mobj.group('value')))
|
||||
for mobj in re.finditer(r'(?s)<(p|div)\s+class="(?P<class>[^" ]+?)">(?P<value>.+?)</\1>', webpage)}
|
||||
|
||||
# since videos do not have title, give the name of song instead
|
||||
data_dict['user_name'] = re.sub(r'\s*さん\s*$', '', data_dict['user_name'])
|
||||
title = data_dict.get('song_title')
|
||||
|
||||
stream_tree = self._download_xml(
|
||||
self._DKML_XML_URL % video_id, video_id, note='Requesting stream information', encoding='sjis',
|
||||
# doing this has no problem since there is no character outside ASCII,
|
||||
# and never likely to happen in the future
|
||||
transform_source=lambda x: re.sub(r'\s*encoding="[^"]+?"', '', x))
|
||||
m3u8_url = try_get(stream_tree, lambda x: x.find(
|
||||
'.//d:streamingUrl', {'d': self._DKML_XML_NS}).text.strip(), compat_str)
|
||||
if not m3u8_url:
|
||||
raise ExtractorError('Failed to obtain m3u8 URL')
|
||||
formats = self._extract_m3u8_formats(m3u8_url, video_id, ext='mp4')
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'uploader_id': uploader_id,
|
||||
'description': description,
|
||||
'uploader': data_dict.get('user_name'),
|
||||
'upload_date': unified_strdate(self._search_regex(r'(\d{4}/\d{2}/\d{2})', data_dict.get('date'), 'upload_date', default=None)),
|
||||
'view_count': int_or_none(self._search_regex(r'(\d+)', data_dict['audience'], 'view_count', default=None)),
|
||||
'like_count': int_or_none(self._search_regex(r'(\d+)', data_dict['nice'], 'like_count', default=None)),
|
||||
'track': title,
|
||||
'artist': data_dict.get('song_artist'),
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
|
||||
class DamtomoVideoIE(DamtomoBaseIE):
|
||||
IE_NAME = 'damtomo:video'
|
||||
_VALID_URL = r'https?://(?:www\.)?clubdam\.com/app/damtomo/(?:SP/)?karaokeMovie/StreamingDkm\.do\?karaokeMovieId=(?P<id>\d+)'
|
||||
_WEBPAGE_URL_TMPL = 'https://www.clubdam.com/app/damtomo/karaokeMovie/StreamingDkm.do?karaokeMovieId=%s'
|
||||
_DKML_XML_URL = 'https://www.clubdam.com/app/damtomo/karaokeMovie/GetStreamingDkmUrlXML.do?movieSelectFlg=2&karaokeMovieId=%s'
|
||||
_DKML_XML_NS = 'https://www.clubdam.com/app/damtomo/karaokeMovie/GetStreamingDkmUrlXML'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.clubdam.com/app/damtomo/karaokeMovie/StreamingDkm.do?karaokeMovieId=2414316',
|
||||
'info_dict': {
|
||||
'id': '2414316',
|
||||
'title': 'Get Wild',
|
||||
'uploader': 'Kドロン',
|
||||
'uploader_id': 'ODk5NTQwMzQ',
|
||||
'track': 'Get Wild',
|
||||
'artist': 'TM NETWORK(TMN)',
|
||||
'upload_date': '20201226',
|
||||
}
|
||||
}]
|
||||
|
||||
|
||||
class DamtomoRecordIE(DamtomoBaseIE):
|
||||
IE_NAME = 'damtomo:record'
|
||||
_VALID_URL = r'https?://(?:www\.)?clubdam\.com/app/damtomo/(?:SP/)?karaokePost/StreamingKrk\.do\?karaokeContributeId=(?P<id>\d+)'
|
||||
_WEBPAGE_URL_TMPL = 'https://www.clubdam.com/app/damtomo/karaokePost/StreamingKrk.do?karaokeContributeId=%s'
|
||||
_DKML_XML_URL = 'https://www.clubdam.com/app/damtomo/karaokePost/GetStreamingKrkUrlXML.do?karaokeContributeId=%s'
|
||||
_DKML_XML_NS = 'https://www.clubdam.com/app/damtomo/karaokePost/GetStreamingKrkUrlXML'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.clubdam.com/app/damtomo/karaokePost/StreamingKrk.do?karaokeContributeId=27376862',
|
||||
'info_dict': {
|
||||
'id': '27376862',
|
||||
'title': 'イカSUMMER [良音]',
|
||||
'description': None,
|
||||
'uploader': 'NANA',
|
||||
'uploader_id': 'MzAyMDExNTY',
|
||||
'upload_date': '20210721',
|
||||
'view_count': 4,
|
||||
'like_count': 1,
|
||||
'track': 'イカSUMMER [良音]',
|
||||
'artist': 'ORANGE RANGE',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.clubdam.com/app/damtomo/karaokePost/StreamingKrk.do?karaokeContributeId=27489418',
|
||||
'info_dict': {
|
||||
'id': '27489418',
|
||||
'title': '心みだれて〜say it with flowers〜(生音)',
|
||||
'uploader_id': 'NjI1MjI2MjU',
|
||||
'description': 'やっぱりキーを下げて正解だった感じ。リベンジ成功ということで。',
|
||||
'uploader': '箱の「中の人」',
|
||||
'upload_date': '20210815',
|
||||
'view_count': 5,
|
||||
'like_count': 3,
|
||||
'track': '心みだれて〜say it with flowers〜(生音)',
|
||||
'artist': '小林明子',
|
||||
}
|
||||
}]
|
||||
@@ -349,7 +349,7 @@ class DiscoveryPlusIE(DPlayIE):
|
||||
_API_URL = 'us1-prod-direct.discoveryplus.com'
|
||||
|
||||
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
||||
headers['x-disco-client'] = f'WEB:UNKNOWN:{self._PRODUCT}:15.0.0'
|
||||
headers['x-disco-client'] = f'WEB:UNKNOWN:{self._PRODUCT}:25.2.6'
|
||||
|
||||
def _download_video_playback_info(self, disco_base, video_id, headers):
|
||||
return self._download_json(
|
||||
@@ -389,3 +389,43 @@ class ScienceChannelIE(DiscoveryPlusIE):
|
||||
|
||||
_PRODUCT = 'sci'
|
||||
_API_URL = 'us1-prod-direct.sciencechannel.com'
|
||||
|
||||
|
||||
class DIYNetworkIE(DiscoveryPlusIE):
|
||||
_VALID_URL = r'https?://(?:watch\.)?diynetwork\.com/video' + DPlayIE._PATH_REGEX
|
||||
_TESTS = [{
|
||||
'url': 'https://watch.diynetwork.com/video/pool-kings-diy-network/bringing-beach-life-to-texas',
|
||||
'info_dict': {
|
||||
'id': '2309730',
|
||||
'display_id': 'pool-kings-diy-network/bringing-beach-life-to-texas',
|
||||
'ext': 'mp4',
|
||||
'title': 'Bringing Beach Life to Texas',
|
||||
'description': 'The Pool Kings give a family a day at the beach in their own backyard.',
|
||||
'season_number': 10,
|
||||
'episode_number': 2,
|
||||
},
|
||||
'skip': 'Available for Premium users',
|
||||
}]
|
||||
|
||||
_PRODUCT = 'diy'
|
||||
_API_URL = 'us1-prod-direct.watch.diynetwork.com'
|
||||
|
||||
|
||||
class AnimalPlanetIE(DiscoveryPlusIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?animalplanet\.com/video' + DPlayIE._PATH_REGEX
|
||||
_TESTS = [{
|
||||
'url': 'https://www.animalplanet.com/video/north-woods-law-animal-planet/squirrel-showdown',
|
||||
'info_dict': {
|
||||
'id': '3338923',
|
||||
'display_id': 'north-woods-law-animal-planet/squirrel-showdown',
|
||||
'ext': 'mp4',
|
||||
'title': 'Squirrel Showdown',
|
||||
'description': 'A woman is suspected of being in possession of flying squirrel kits.',
|
||||
'season_number': 16,
|
||||
'episode_number': 11,
|
||||
},
|
||||
'skip': 'Available for Premium users',
|
||||
}]
|
||||
|
||||
_PRODUCT = 'apl'
|
||||
_API_URL = 'us1-prod-direct.animalplanet.com'
|
||||
|
||||
@@ -5,6 +5,7 @@ from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
int_or_none,
|
||||
unified_strdate,
|
||||
url_or_none,
|
||||
)
|
||||
from ..compat import compat_urlparse
|
||||
|
||||
@@ -15,13 +16,13 @@ class DWIE(InfoExtractor):
|
||||
_TESTS = [{
|
||||
# video
|
||||
'url': 'http://www.dw.com/en/intelligent-light/av-19112290',
|
||||
'md5': '7372046e1815c5a534b43f3c3c36e6e9',
|
||||
'md5': 'fb9dfd9520811d3ece80f04befd73428',
|
||||
'info_dict': {
|
||||
'id': '19112290',
|
||||
'ext': 'mp4',
|
||||
'title': 'Intelligent light',
|
||||
'description': 'md5:90e00d5881719f2a6a5827cb74985af1',
|
||||
'upload_date': '20160311',
|
||||
'upload_date': '20160605',
|
||||
}
|
||||
}, {
|
||||
# audio
|
||||
@@ -55,15 +56,16 @@ class DWIE(InfoExtractor):
|
||||
title = hidden_inputs['media_title']
|
||||
media_id = hidden_inputs.get('media_id') or media_id
|
||||
|
||||
if hidden_inputs.get('player_type') == 'video' and hidden_inputs.get('stream_file') == '1':
|
||||
direct_url = url_or_none(hidden_inputs.get('file_name'))
|
||||
if direct_url:
|
||||
formats = [{'url': hidden_inputs['file_name']}]
|
||||
else:
|
||||
formats = self._extract_smil_formats(
|
||||
'http://www.dw.com/smil/v-%s' % media_id, media_id,
|
||||
transform_source=lambda s: s.replace(
|
||||
'rtmp://tv-od.dw.de/flash/',
|
||||
'http://tv-download.dw.de/dwtv_video/flv/'))
|
||||
self._sort_formats(formats)
|
||||
else:
|
||||
formats = [{'url': hidden_inputs['file_name']}]
|
||||
self._sort_formats(formats)
|
||||
|
||||
upload_date = hidden_inputs.get('display_date')
|
||||
if not upload_date:
|
||||
|
||||
@@ -147,6 +147,8 @@ from .bilibili import (
|
||||
BilibiliAudioAlbumIE,
|
||||
BiliBiliPlayerIE,
|
||||
BilibiliChannelIE,
|
||||
BiliIntlIE,
|
||||
BiliIntlSeriesIE,
|
||||
)
|
||||
from .biobiochiletv import BioBioChileTVIE
|
||||
from .bitchute import (
|
||||
@@ -183,6 +185,7 @@ from .businessinsider import BusinessInsiderIE
|
||||
from .buzzfeed import BuzzFeedIE
|
||||
from .byutv import BYUtvIE
|
||||
from .c56 import C56IE
|
||||
from .cam4 import CAM4IE
|
||||
from .camdemy import (
|
||||
CamdemyIE,
|
||||
CamdemyFolderIE
|
||||
@@ -205,9 +208,9 @@ from .cartoonnetwork import CartoonNetworkIE
|
||||
from .cbc import (
|
||||
CBCIE,
|
||||
CBCPlayerIE,
|
||||
CBCWatchVideoIE,
|
||||
CBCWatchIE,
|
||||
CBCOlympicsIE,
|
||||
CBCGemIE,
|
||||
CBCGemPlaylistIE,
|
||||
CBCGemLiveIE,
|
||||
)
|
||||
from .cbs import CBSIE
|
||||
from .cbslocal import (
|
||||
@@ -236,10 +239,15 @@ from .ceskatelevize import (
|
||||
CeskaTelevizeIE,
|
||||
CeskaTelevizePoradyIE,
|
||||
)
|
||||
from .cgtn import CGTNIE
|
||||
from .channel9 import Channel9IE
|
||||
from .charlierose import CharlieRoseIE
|
||||
from .chaturbate import ChaturbateIE
|
||||
from .chilloutzone import ChilloutzoneIE
|
||||
from .chingari import (
|
||||
ChingariIE,
|
||||
ChingariUserIE,
|
||||
)
|
||||
from .chirbit import (
|
||||
ChirbitIE,
|
||||
ChirbitProfileIE,
|
||||
@@ -307,6 +315,10 @@ from .dailymotion import (
|
||||
DailymotionPlaylistIE,
|
||||
DailymotionUserIE,
|
||||
)
|
||||
from .damtomo import (
|
||||
DamtomoRecordIE,
|
||||
DamtomoVideoIE,
|
||||
)
|
||||
from .daum import (
|
||||
DaumIE,
|
||||
DaumClipIE,
|
||||
@@ -337,7 +349,9 @@ from .dplay import (
|
||||
DPlayIE,
|
||||
DiscoveryPlusIE,
|
||||
HGTVDeIE,
|
||||
ScienceChannelIE
|
||||
ScienceChannelIE,
|
||||
DIYNetworkIE,
|
||||
AnimalPlanetIE
|
||||
)
|
||||
from .dreisat import DreiSatIE
|
||||
from .drbonanza import DRBonanzaIE
|
||||
@@ -511,6 +525,7 @@ from .googlepodcasts import (
|
||||
)
|
||||
from .googlesearch import GoogleSearchIE
|
||||
from .goshgay import GoshgayIE
|
||||
from .gotostage import GoToStageIE
|
||||
from .gputechconf import GPUTechConfIE
|
||||
from .groupon import GrouponIE
|
||||
from .hbo import HBOIE
|
||||
@@ -547,6 +562,10 @@ from .hungama import (
|
||||
HungamaAlbumPlaylistIE,
|
||||
)
|
||||
from .hypem import HypemIE
|
||||
from .ichinanalive import (
|
||||
IchinanaLiveIE,
|
||||
IchinanaLiveClipIE,
|
||||
)
|
||||
from .ign import (
|
||||
IGNIE,
|
||||
IGNVideoIE,
|
||||
@@ -613,6 +632,7 @@ from .kickstarter import KickStarterIE
|
||||
from .kinja import KinjaEmbedIE
|
||||
from .kinopoisk import KinoPoiskIE
|
||||
from .konserthusetplay import KonserthusetPlayIE
|
||||
from .koo import KooIE
|
||||
from .krasview import KrasViewIE
|
||||
from .ku6 import Ku6IE
|
||||
from .kusi import KUSIIE
|
||||
@@ -721,6 +741,8 @@ from .massengeschmacktv import MassengeschmackTVIE
|
||||
from .matchtv import MatchTVIE
|
||||
from .mdr import MDRIE
|
||||
from .medaltv import MedalTVIE
|
||||
from .mediaite import MediaiteIE
|
||||
from .mediaklikk import MediaKlikkIE
|
||||
from .mediaset import MediasetIE
|
||||
from .mediasite import (
|
||||
MediasiteIE,
|
||||
@@ -796,6 +818,7 @@ from .mtv import (
|
||||
MTVItaliaProgrammaIE,
|
||||
)
|
||||
from .muenchentv import MuenchenTVIE
|
||||
from .musescore import MuseScoreIE
|
||||
from .mwave import MwaveIE, MwaveMeetGreetIE
|
||||
from .mxplayer import (
|
||||
MxplayerIE,
|
||||
@@ -858,6 +881,7 @@ from .neteasemusic import (
|
||||
from .newgrounds import (
|
||||
NewgroundsIE,
|
||||
NewgroundsPlaylistIE,
|
||||
NewgroundsUserIE,
|
||||
)
|
||||
from .newstube import NewstubeIE
|
||||
from .nextmedia import (
|
||||
@@ -951,10 +975,12 @@ from .nytimes import (
|
||||
NYTimesCookingIE,
|
||||
)
|
||||
from .nuvid import NuvidIE
|
||||
from .nzherald import NZHeraldIE
|
||||
from .nzz import NZZIE
|
||||
from .odatv import OdaTVIE
|
||||
from .odnoklassniki import OdnoklassnikiIE
|
||||
from .oktoberfesttv import OktoberfestTVIE
|
||||
from .olympics import OlympicsReplayIE
|
||||
from .ondemandkorea import OnDemandKoreaIE
|
||||
from .onet import (
|
||||
OnetIE,
|
||||
@@ -1012,7 +1038,10 @@ from .patreon import (
|
||||
)
|
||||
from .pbs import PBSIE
|
||||
from .pearvideo import PearVideoIE
|
||||
from .peertube import PeerTubeIE
|
||||
from .peertube import (
|
||||
PeerTubeIE,
|
||||
PeerTubePlaylistIE,
|
||||
)
|
||||
from .peloton import (
|
||||
PelotonIE,
|
||||
PelotonLiveIE
|
||||
@@ -1107,6 +1136,11 @@ from .radiode import RadioDeIE
|
||||
from .radiojavan import RadioJavanIE
|
||||
from .radiobremen import RadioBremenIE
|
||||
from .radiofrance import RadioFranceIE
|
||||
from .radlive import (
|
||||
RadLiveIE,
|
||||
RadLiveChannelIE,
|
||||
RadLiveSeasonIE,
|
||||
)
|
||||
from .rai import (
|
||||
RaiPlayIE,
|
||||
RaiPlayLiveIE,
|
||||
@@ -1278,6 +1312,10 @@ from .southpark import (
|
||||
SouthParkEsIE,
|
||||
SouthParkNlIE
|
||||
)
|
||||
from .sovietscloset import (
|
||||
SovietsClosetIE,
|
||||
SovietsClosetPlaylistIE
|
||||
)
|
||||
from .spankbang import (
|
||||
SpankBangIE,
|
||||
SpankBangPlaylistIE,
|
||||
@@ -1321,6 +1359,7 @@ from .storyfire import (
|
||||
StoryFireSeriesIE,
|
||||
)
|
||||
from .streamable import StreamableIE
|
||||
from .streamanity import StreamanityIE
|
||||
from .streamcloud import StreamcloudIE
|
||||
from .streamcz import StreamCZIE
|
||||
from .streetvoice import StreetVoiceIE
|
||||
@@ -1389,6 +1428,7 @@ from .theplatform import (
|
||||
from .thescene import TheSceneIE
|
||||
from .thestar import TheStarIE
|
||||
from .thesun import TheSunIE
|
||||
from .theta import ThetaIE
|
||||
from .theweatherchannel import TheWeatherChannelIE
|
||||
from .thisamericanlife import ThisAmericanLifeIE
|
||||
from .thisav import ThisAVIE
|
||||
@@ -1755,7 +1795,11 @@ from .yandexmusic import (
|
||||
YandexMusicArtistTracksIE,
|
||||
YandexMusicArtistAlbumsIE,
|
||||
)
|
||||
from .yandexvideo import YandexVideoIE
|
||||
from .yandexvideo import (
|
||||
YandexVideoIE,
|
||||
ZenYandexIE,
|
||||
ZenYandexChannelIE,
|
||||
)
|
||||
from .yapfiles import YapFilesIE
|
||||
from .yesjapan import YesJapanIE
|
||||
from .yinyuetai import YinYueTaiIE
|
||||
@@ -1775,6 +1819,7 @@ from .yourporn import YourPornIE
|
||||
from .yourupload import YourUploadIE
|
||||
from .youtube import (
|
||||
YoutubeIE,
|
||||
YoutubeClipIE,
|
||||
YoutubeFavouritesIE,
|
||||
YoutubeHistoryIE,
|
||||
YoutubeTabIE,
|
||||
|
||||
@@ -173,7 +173,7 @@ class FancodeLiveIE(FancodeVodIE):
|
||||
|
||||
match_info = try_get(info_json, lambda x: x['data']['match'])
|
||||
|
||||
if match_info.get('status') != "LIVE":
|
||||
if match_info.get('streamingStatus') != "STARTED":
|
||||
raise ExtractorError('The stream can\'t be accessed', expected=True)
|
||||
self._check_login_required(match_info.get('isUserEntitled'), True) # all live streams are premium only
|
||||
|
||||
|
||||
@@ -302,12 +302,20 @@ class FunimationShowIE(FunimationIE):
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_initialize(self):
|
||||
region = self._get_cookies('https://www.funimation.com').get('region')
|
||||
self._region = region.value if region else try_get(
|
||||
self._download_json(
|
||||
'https://geo-service.prd.funimationsvc.com/geo/v1/region/check', None, fatal=False,
|
||||
note='Checking geo-location', errnote='Unable to fetch geo-location information'),
|
||||
lambda x: x['region']) or 'US'
|
||||
|
||||
def _real_extract(self, url):
|
||||
base_url, locale, display_id = self._match_valid_url(url).groups()
|
||||
|
||||
show_info = self._download_json(
|
||||
'https://title-api.prd.funimationsvc.com/v2/shows/%s?region=US&deviceType=web&locale=%s'
|
||||
% (display_id, locale or 'en'), display_id)
|
||||
'https://title-api.prd.funimationsvc.com/v2/shows/%s?region=%s&deviceType=web&locale=%s'
|
||||
% (display_id, self._region, locale or 'en'), display_id)
|
||||
items = self._download_json(
|
||||
'https://prod-api-funimationnow.dadcdigital.com/api/funimation/episodes/?limit=99999&title_id=%s'
|
||||
% show_info.get('id'), display_id).get('items')
|
||||
|
||||
@@ -1215,14 +1215,13 @@ class GenericIE(InfoExtractor):
|
||||
},
|
||||
{
|
||||
# JWPlatform iframe
|
||||
'url': 'https://www.mediaite.com/tv/dem-senator-claims-gary-cohn-faked-a-bad-connection-during-trump-call-to-get-him-off-the-phone/',
|
||||
'md5': 'ca00a040364b5b439230e7ebfd02c4e9',
|
||||
'url': 'https://www.covermagazine.co.uk/feature/2465255/business-protection-involved',
|
||||
'info_dict': {
|
||||
'id': 'O0c5JcKT',
|
||||
'id': 'AG26UQXM',
|
||||
'ext': 'mp4',
|
||||
'upload_date': '20171122',
|
||||
'timestamp': 1511366290,
|
||||
'title': 'Dem Senator Claims Gary Cohn Faked a Bad Connection During Trump Call to Get Him Off the Phone',
|
||||
'upload_date': '20160719',
|
||||
'timestamp': 468923808,
|
||||
'title': '2016_05_18 Cover L&G Business Protection V1 FINAL.mp4',
|
||||
},
|
||||
'add_ie': [JWPlatformIE.ie_key()],
|
||||
},
|
||||
|
||||
+63
-90
@@ -9,15 +9,14 @@ import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import (
|
||||
compat_HTTPError,
|
||||
compat_str,
|
||||
)
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
float_or_none,
|
||||
int_or_none,
|
||||
orderedSet,
|
||||
str_or_none,
|
||||
try_get,
|
||||
)
|
||||
|
||||
|
||||
@@ -26,18 +25,19 @@ class GloboIE(InfoExtractor):
|
||||
_NETRC_MACHINE = 'globo'
|
||||
_TESTS = [{
|
||||
'url': 'http://g1.globo.com/carros/autoesporte/videos/t/exclusivos-do-g1/v/mercedes-benz-gla-passa-por-teste-de-colisao-na-europa/3607726/',
|
||||
'md5': 'b3ccc801f75cd04a914d51dadb83a78d',
|
||||
'info_dict': {
|
||||
'id': '3607726',
|
||||
'ext': 'mp4',
|
||||
'title': 'Mercedes-Benz GLA passa por teste de colisão na Europa',
|
||||
'duration': 103.204,
|
||||
'uploader': 'Globo.com',
|
||||
'uploader_id': '265',
|
||||
'uploader': 'G1',
|
||||
'uploader_id': '2015',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
'url': 'http://globoplay.globo.com/v/4581987/',
|
||||
'md5': 'f36a1ecd6a50da1577eee6dd17f67eff',
|
||||
'info_dict': {
|
||||
'id': '4581987',
|
||||
'ext': 'mp4',
|
||||
@@ -46,6 +46,9 @@ class GloboIE(InfoExtractor):
|
||||
'uploader': 'Rede Globo',
|
||||
'uploader_id': '196',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
'url': 'http://canalbrasil.globo.com/programas/sangue-latino/videos/3928201.html',
|
||||
'only_matching': True,
|
||||
@@ -66,30 +69,6 @@ class GloboIE(InfoExtractor):
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_initialize(self):
|
||||
email, password = self._get_login_info()
|
||||
if email is None:
|
||||
return
|
||||
|
||||
try:
|
||||
glb_id = (self._download_json(
|
||||
'https://login.globo.com/api/authentication', None, data=json.dumps({
|
||||
'payload': {
|
||||
'email': email,
|
||||
'password': password,
|
||||
'serviceId': 4654,
|
||||
},
|
||||
}).encode(), headers={
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
}) or {}).get('glbId')
|
||||
if glb_id:
|
||||
self._set_cookie('.globo.com', 'GLBID', glb_id)
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
|
||||
resp = self._parse_json(e.cause.read(), None)
|
||||
raise ExtractorError(resp.get('userMessage') or resp['id'], expected=True)
|
||||
raise
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
|
||||
@@ -102,73 +81,67 @@ class GloboIE(InfoExtractor):
|
||||
title = video['title']
|
||||
|
||||
formats = []
|
||||
security = self._download_json(
|
||||
'https://playback.video.globo.com/v1/video-session', video_id, 'Downloading security hash for %s' % video_id,
|
||||
headers={'content-type': 'application/json'}, data=json.dumps({
|
||||
"player_type": "desktop",
|
||||
"video_id": video_id,
|
||||
"quality": "max",
|
||||
"content_protection": "widevine",
|
||||
"vsid": "581b986b-4c40-71f0-5a58-803e579d5fa2",
|
||||
"tz": "-3.0:00"
|
||||
}).encode())
|
||||
|
||||
security_hash = security['source']['token']
|
||||
if not security_hash:
|
||||
message = security.get('message')
|
||||
if message:
|
||||
raise ExtractorError(
|
||||
'%s returned error: %s' % (self.IE_NAME, message), expected=True)
|
||||
|
||||
hash_code = security_hash[:2]
|
||||
padding = '%010d' % random.randint(1, 10000000000)
|
||||
if hash_code in ('04', '14'):
|
||||
received_time = security_hash[3:13]
|
||||
received_md5 = security_hash[24:]
|
||||
hash_prefix = security_hash[:23]
|
||||
elif hash_code in ('02', '12', '03', '13'):
|
||||
received_time = security_hash[2:12]
|
||||
received_md5 = security_hash[22:]
|
||||
padding += '1'
|
||||
hash_prefix = '05' + security_hash[:22]
|
||||
|
||||
padded_sign_time = compat_str(int(received_time) + 86400) + padding
|
||||
md5_data = (received_md5 + padded_sign_time + '0xAC10FD').encode()
|
||||
signed_md5 = base64.urlsafe_b64encode(hashlib.md5(md5_data).digest()).decode().strip('=')
|
||||
signed_hash = hash_prefix + padded_sign_time + signed_md5
|
||||
source = security['source']['url_parts']
|
||||
resource_url = source['scheme'] + '://' + source['domain'] + source['path']
|
||||
signed_url = '%s?h=%s&k=html5&a=%s' % (resource_url, signed_hash, 'F' if video.get('subscriber_only') else 'A')
|
||||
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
signed_url, video_id, 'mp4', entry_protocol='m3u8_native', m3u8_id='hls', fatal=False))
|
||||
self._sort_formats(formats)
|
||||
|
||||
subtitles = {}
|
||||
for resource in video['resources']:
|
||||
resource_id = resource.get('_id')
|
||||
resource_url = resource.get('url')
|
||||
resource_type = resource.get('type')
|
||||
if not resource_url or (resource_type == 'media' and not resource_id) or resource_type not in ('subtitle', 'media'):
|
||||
continue
|
||||
|
||||
if resource_type == 'subtitle':
|
||||
if resource.get('type') == 'subtitle':
|
||||
subtitles.setdefault(resource.get('language') or 'por', []).append({
|
||||
'url': resource_url,
|
||||
'url': resource.get('url'),
|
||||
})
|
||||
continue
|
||||
|
||||
security = self._download_json(
|
||||
'http://security.video.globo.com/videos/%s/hash' % video_id,
|
||||
video_id, 'Downloading security hash for %s' % resource_id, query={
|
||||
'player': 'desktop',
|
||||
'version': '5.19.1',
|
||||
'resource_id': resource_id,
|
||||
subs = try_get(security, lambda x: x['source']['subtitles'], expected_type=dict) or {}
|
||||
for sub_lang, sub_url in subs.items():
|
||||
if sub_url:
|
||||
subtitles.setdefault(sub_lang or 'por', []).append({
|
||||
'url': sub_url,
|
||||
})
|
||||
|
||||
security_hash = security.get('hash')
|
||||
if not security_hash:
|
||||
message = security.get('message')
|
||||
if message:
|
||||
raise ExtractorError(
|
||||
'%s returned error: %s' % (self.IE_NAME, message), expected=True)
|
||||
continue
|
||||
|
||||
hash_code = security_hash[:2]
|
||||
padding = '%010d' % random.randint(1, 10000000000)
|
||||
if hash_code in ('04', '14'):
|
||||
received_time = security_hash[3:13]
|
||||
received_md5 = security_hash[24:]
|
||||
hash_prefix = security_hash[:23]
|
||||
elif hash_code in ('02', '12', '03', '13'):
|
||||
received_time = security_hash[2:12]
|
||||
received_md5 = security_hash[22:]
|
||||
padding += '1'
|
||||
hash_prefix = '05' + security_hash[:22]
|
||||
|
||||
padded_sign_time = compat_str(int(received_time) + 86400) + padding
|
||||
md5_data = (received_md5 + padded_sign_time + '0xAC10FD').encode()
|
||||
signed_md5 = base64.urlsafe_b64encode(hashlib.md5(md5_data).digest()).decode().strip('=')
|
||||
signed_hash = hash_prefix + padded_sign_time + signed_md5
|
||||
signed_url = '%s?h=%s&k=html5&a=%s&u=%s' % (resource_url, signed_hash, 'F' if video.get('subscriber_only') else 'A', security.get('user') or '')
|
||||
|
||||
if resource_id.endswith('m3u8') or resource_url.endswith('.m3u8'):
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
signed_url, resource_id, 'mp4', entry_protocol='m3u8_native',
|
||||
m3u8_id='hls', fatal=False))
|
||||
elif resource_id.endswith('mpd') or resource_url.endswith('.mpd'):
|
||||
formats.extend(self._extract_mpd_formats(
|
||||
signed_url, resource_id, mpd_id='dash', fatal=False))
|
||||
elif resource_id.endswith('manifest') or resource_url.endswith('/manifest'):
|
||||
formats.extend(self._extract_ism_formats(
|
||||
signed_url, resource_id, ism_id='mss', fatal=False))
|
||||
else:
|
||||
formats.append({
|
||||
'url': signed_url,
|
||||
'format_id': 'http-%s' % resource_id,
|
||||
'height': int_or_none(resource.get('height')),
|
||||
subs = try_get(security, lambda x: x['source']['subtitles_webvtt'], expected_type=dict) or {}
|
||||
for sub_lang, sub_url in subs.items():
|
||||
if sub_url:
|
||||
subtitles.setdefault(sub_lang or 'por', []).append({
|
||||
'url': sub_url,
|
||||
})
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
duration = float_or_none(video.get('duration'), 1000)
|
||||
uploader = video.get('channel')
|
||||
uploader_id = str_or_none(video.get('channel_id'))
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_str
|
||||
from ..utils import (
|
||||
try_get,
|
||||
url_or_none
|
||||
)
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class GoToStageIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?gotostage\.com/channel/[a-z0-9]+/recording/(?P<id>[a-z0-9]+)/watch'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.gotostage.com/channel/8901680603948959494/recording/60bb55548d434f21b9ce4f0e225c4895/watch',
|
||||
'md5': 'ca72ce990cdcd7a2bd152f7217e319a2',
|
||||
'info_dict': {
|
||||
'id': '60bb55548d434f21b9ce4f0e225c4895',
|
||||
'ext': 'mp4',
|
||||
'title': 'What is GoToStage?',
|
||||
'thumbnail': r're:^https?://.*\.jpg$',
|
||||
'duration': 93.924711
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.gotostage.com/channel/bacc3d3535b34bafacc3f4ef8d4df78a/recording/831e74cd3e0042be96defba627b6f676/watch?source=HOMEPAGE',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
metadata = self._download_json(
|
||||
'https://api.gotostage.com/contents?ids=%s' % video_id,
|
||||
video_id,
|
||||
note='Downloading video metadata',
|
||||
errnote='Unable to download video metadata')[0]
|
||||
|
||||
registration_data = {
|
||||
'product': metadata['product'],
|
||||
'resourceType': metadata['contentType'],
|
||||
'productReferenceKey': metadata['productRefKey'],
|
||||
'firstName': 'foo',
|
||||
'lastName': 'bar',
|
||||
'email': 'foobar@example.com'
|
||||
}
|
||||
|
||||
registration_response = self._download_json(
|
||||
'https://api-registrations.logmeininc.com/registrations',
|
||||
video_id,
|
||||
data=json.dumps(registration_data).encode(),
|
||||
expected_status=409,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
note='Register user',
|
||||
errnote='Unable to register user')
|
||||
|
||||
content_response = self._download_json(
|
||||
'https://api.gotostage.com/contents/%s/asset' % video_id,
|
||||
video_id,
|
||||
headers={'x-registrantkey': registration_response['registrationKey']},
|
||||
note='Get download url',
|
||||
errnote='Unable to get download url')
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': try_get(metadata, lambda x: x['title'], compat_str),
|
||||
'url': try_get(content_response, lambda x: x['cdnLocation'], compat_str),
|
||||
'ext': 'mp4',
|
||||
'thumbnail': url_or_none(try_get(metadata, lambda x: x['thumbnail']['location'])),
|
||||
'duration': try_get(metadata, lambda x: x['duration'], float),
|
||||
'categories': [try_get(metadata, lambda x: x['category'], compat_str)],
|
||||
'is_live': False
|
||||
}
|
||||
+45
-40
@@ -1,12 +1,13 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_str
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
int_or_none,
|
||||
try_get,
|
||||
url_or_none,
|
||||
urlencode_postdata,
|
||||
)
|
||||
@@ -57,48 +58,51 @@ class HiDiveIE(InfoExtractor):
|
||||
mobj = self._match_valid_url(url)
|
||||
title, key = mobj.group('title', 'key')
|
||||
video_id = '%s/%s' % (title, key)
|
||||
|
||||
settings = self._download_json(
|
||||
'https://www.hidive.com/play/settings', video_id,
|
||||
data=urlencode_postdata({
|
||||
'Title': title,
|
||||
'Key': key,
|
||||
'PlayerId': 'f4f895ce1ca713ba263b91caeb1daa2d08904783',
|
||||
}))
|
||||
|
||||
restriction = settings.get('restrictionReason')
|
||||
if restriction == 'RegionRestricted':
|
||||
self.raise_geo_restricted()
|
||||
|
||||
if restriction and restriction != 'None':
|
||||
raise ExtractorError(
|
||||
'%s said: %s' % (self.IE_NAME, restriction), expected=True)
|
||||
|
||||
webpage = self._download_webpage(url, video_id, fatal=False)
|
||||
data_videos = re.findall(r'data-video=\"([^\"]+)\"\s?data-captions=\"([^\"]+)\"', webpage)
|
||||
formats = []
|
||||
subtitles = {}
|
||||
for rendition_id, rendition in settings['renditions'].items():
|
||||
bitrates = rendition.get('bitrates')
|
||||
if not isinstance(bitrates, dict):
|
||||
continue
|
||||
m3u8_url = url_or_none(bitrates.get('hls'))
|
||||
if not m3u8_url:
|
||||
continue
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
m3u8_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||
m3u8_id='%s-hls' % rendition_id, fatal=False))
|
||||
cc_files = rendition.get('ccFiles')
|
||||
if not isinstance(cc_files, list):
|
||||
continue
|
||||
for cc_file in cc_files:
|
||||
if not isinstance(cc_file, list) or len(cc_file) < 3:
|
||||
for data_video in data_videos:
|
||||
_, _, _, version, audio, _, extra = data_video[0].split('_')
|
||||
caption = data_video[1]
|
||||
|
||||
settings = self._download_json(
|
||||
'https://www.hidive.com/play/settings', video_id,
|
||||
data=urlencode_postdata({
|
||||
'Title': title,
|
||||
'Key': key,
|
||||
'PlayerId': 'f4f895ce1ca713ba263b91caeb1daa2d08904783',
|
||||
'Version': version,
|
||||
'Audio': audio,
|
||||
'Captions': caption,
|
||||
'Extra': extra,
|
||||
}))
|
||||
|
||||
restriction = settings.get('restrictionReason')
|
||||
if restriction == 'RegionRestricted':
|
||||
self.raise_geo_restricted()
|
||||
|
||||
if restriction and restriction != 'None':
|
||||
raise ExtractorError(
|
||||
'%s said: %s' % (self.IE_NAME, restriction), expected=True)
|
||||
|
||||
for rendition_id, rendition in settings['renditions'].items():
|
||||
m3u8_url = url_or_none(try_get(rendition, lambda x: x['bitrates']['hls']))
|
||||
if not m3u8_url:
|
||||
continue
|
||||
cc_lang = cc_file[0]
|
||||
cc_url = url_or_none(cc_file[2])
|
||||
if not isinstance(cc_lang, compat_str) or not cc_url:
|
||||
continue
|
||||
subtitles.setdefault(cc_lang, []).append({
|
||||
'url': cc_url,
|
||||
})
|
||||
frmt = self._extract_m3u8_formats(
|
||||
m3u8_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||
m3u8_id='%s-%s-%s-%s' % (version, audio, extra, caption), fatal=False)
|
||||
for f in frmt:
|
||||
f['language'] = audio
|
||||
formats.extend(frmt)
|
||||
|
||||
for cc_file in rendition.get('ccFiles', []):
|
||||
cc_url = url_or_none(try_get(cc_file, lambda x: x[2]))
|
||||
# name is used since we cant distinguish subs with same language code
|
||||
cc_lang = try_get(cc_file, (lambda x: x[1].replace(' ', '-').lower(), lambda x: x[0]), str)
|
||||
if cc_url and cc_lang:
|
||||
subtitles.setdefault(cc_lang, []).append({'url': cc_url})
|
||||
self._sort_formats(formats)
|
||||
|
||||
season_number = int_or_none(self._search_regex(
|
||||
@@ -114,4 +118,5 @@ class HiDiveIE(InfoExtractor):
|
||||
'series': title,
|
||||
'season_number': season_number,
|
||||
'episode_number': episode_number,
|
||||
'http_headers': {'Referer': url}
|
||||
}
|
||||
|
||||
@@ -254,6 +254,9 @@ class HotStarIE(HotStarBaseIE):
|
||||
'season_id': video_data.get('seasonId'),
|
||||
'episode': title,
|
||||
'episode_number': int_or_none(video_data.get('episodeNo')),
|
||||
'http_headers': {
|
||||
'Referer': 'https://www.hotstar.com/in',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import ExtractorError, str_or_none, traverse_obj, unified_strdate
|
||||
from ..compat import compat_str
|
||||
|
||||
|
||||
class IchinanaLiveIE(InfoExtractor):
|
||||
IE_NAME = '17live'
|
||||
_VALID_URL = r'https?://(?:www\.)?17\.live/(?:[^/]+/)*(?:live|profile/r)/(?P<id>\d+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://17.live/live/3773096',
|
||||
'info_dict': {
|
||||
'id': '3773096',
|
||||
'title': '萠珈☕🤡🍫moka',
|
||||
'is_live': True,
|
||||
'uploader': '萠珈☕🤡🍫moka',
|
||||
'uploader_id': '3773096',
|
||||
'like_count': 366,
|
||||
'view_count': 18121,
|
||||
'timestamp': 1630569012,
|
||||
},
|
||||
'skip': 'running as of writing, but may be ended as of testing',
|
||||
}, {
|
||||
'note': 'nothing except language differs',
|
||||
'url': 'https://17.live/ja/live/3773096',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
@classmethod
|
||||
def suitable(cls, url):
|
||||
return not IchinanaLiveClipIE.suitable(url) and super(IchinanaLiveIE, cls).suitable(url)
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
url = 'https://17.live/live/%s' % video_id
|
||||
|
||||
enter = self._download_json(
|
||||
'https://api-dsa.17app.co/api/v1/lives/%s/enter' % video_id, video_id,
|
||||
headers={'Referer': url}, fatal=False, expected_status=420,
|
||||
data=b'\0')
|
||||
if enter and enter.get('message') == 'ended':
|
||||
raise ExtractorError('This live has ended.', expected=True)
|
||||
|
||||
view_data = self._download_json(
|
||||
'https://api-dsa.17app.co/api/v1/lives/%s' % video_id, video_id,
|
||||
headers={'Referer': url})
|
||||
|
||||
uploader = traverse_obj(
|
||||
view_data, ('userInfo', 'displayName'), ('userInfo', 'openID'))
|
||||
|
||||
video_urls = view_data.get('rtmpUrls')
|
||||
if not video_urls:
|
||||
raise ExtractorError('unable to extract live URL information')
|
||||
formats = []
|
||||
for (name, value) in video_urls[0].items():
|
||||
if not isinstance(value, compat_str):
|
||||
continue
|
||||
if not value.startswith('http'):
|
||||
continue
|
||||
quality = -1
|
||||
if 'web' in name:
|
||||
quality -= 1
|
||||
if 'High' in name:
|
||||
quality += 4
|
||||
if 'Low' in name:
|
||||
quality -= 2
|
||||
formats.append({
|
||||
'format_id': name,
|
||||
'url': value,
|
||||
'quality': quality,
|
||||
'http_headers': {'Referer': url},
|
||||
'ext': 'flv',
|
||||
'vcodec': 'h264',
|
||||
'acodec': 'aac',
|
||||
})
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': uploader or video_id,
|
||||
'formats': formats,
|
||||
'is_live': True,
|
||||
'uploader': uploader,
|
||||
'uploader_id': video_id,
|
||||
'like_count': view_data.get('receivedLikeCount'),
|
||||
'view_count': view_data.get('viewerCount'),
|
||||
'thumbnail': view_data.get('coverPhoto'),
|
||||
'description': view_data.get('caption'),
|
||||
'timestamp': view_data.get('beginTime'),
|
||||
}
|
||||
|
||||
|
||||
class IchinanaLiveClipIE(InfoExtractor):
|
||||
IE_NAME = '17live:clip'
|
||||
_VALID_URL = r'https?://(?:www\.)?17\.live/(?:[^/]+/)*profile/r/(?P<uploader_id>\d+)/clip/(?P<id>[^/]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://17.live/profile/r/1789280/clip/1bHQSK8KUieruFXaCH4A4upCzlN',
|
||||
'info_dict': {
|
||||
'id': '1bHQSK8KUieruFXaCH4A4upCzlN',
|
||||
'title': 'マチコ先生🦋Class💋',
|
||||
'description': 'マチ戦隊 第一次 バスターコール\n総額200万coin!\n動画制作@うぉーかー🌱Walker🎫',
|
||||
'uploader_id': '1789280',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://17.live/ja/profile/r/1789280/clip/1bHQSK8KUieruFXaCH4A4upCzlN',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
uploader_id, video_id = self._match_valid_url(url).groups()
|
||||
url = 'https://17.live/profile/r/%s/clip/%s' % (uploader_id, video_id)
|
||||
|
||||
view_data = self._download_json(
|
||||
'https://api-dsa.17app.co/api/v1/clips/%s' % video_id, video_id,
|
||||
headers={'Referer': url})
|
||||
|
||||
uploader = traverse_obj(
|
||||
view_data, ('userInfo', 'displayName'), ('userInfo', 'name'))
|
||||
|
||||
formats = []
|
||||
if view_data.get('videoURL'):
|
||||
formats.append({
|
||||
'id': 'video',
|
||||
'url': view_data['videoURL'],
|
||||
'quality': -1,
|
||||
})
|
||||
if view_data.get('transcodeURL'):
|
||||
formats.append({
|
||||
'id': 'transcode',
|
||||
'url': view_data['transcodeURL'],
|
||||
'quality': -1,
|
||||
})
|
||||
if view_data.get('srcVideoURL'):
|
||||
# highest quality
|
||||
formats.append({
|
||||
'id': 'srcVideo',
|
||||
'url': view_data['srcVideoURL'],
|
||||
'quality': 1,
|
||||
})
|
||||
|
||||
for fmt in formats:
|
||||
fmt.update({
|
||||
'ext': 'mp4',
|
||||
'protocol': 'https',
|
||||
'vcodec': 'h264',
|
||||
'acodec': 'aac',
|
||||
'http_headers': {'Referer': url},
|
||||
})
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': uploader or video_id,
|
||||
'formats': formats,
|
||||
'uploader': uploader,
|
||||
'uploader_id': uploader_id,
|
||||
'like_count': view_data.get('likeCount'),
|
||||
'view_count': view_data.get('viewCount'),
|
||||
'thumbnail': view_data.get('imageURL'),
|
||||
'duration': view_data.get('duration'),
|
||||
'description': view_data.get('caption'),
|
||||
'upload_date': unified_strdate(str_or_none(view_data.get('createdAt'))),
|
||||
}
|
||||
+110
-41
@@ -5,10 +5,14 @@ import json
|
||||
|
||||
from .common import InfoExtractor
|
||||
from .brightcove import BrightcoveNewIE
|
||||
|
||||
from ..compat import compat_str
|
||||
from ..utils import (
|
||||
base_url,
|
||||
clean_html,
|
||||
determine_ext,
|
||||
extract_attributes,
|
||||
ExtractorError,
|
||||
get_element_by_class,
|
||||
JSON_LD_RE,
|
||||
merge_dicts,
|
||||
@@ -16,6 +20,8 @@ from ..utils import (
|
||||
smuggle_url,
|
||||
try_get,
|
||||
url_or_none,
|
||||
url_basename,
|
||||
urljoin,
|
||||
)
|
||||
|
||||
|
||||
@@ -23,15 +29,32 @@ class ITVIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?itv\.com/hub/[^/]+/(?P<id>[0-9a-zA-Z]+)'
|
||||
_GEO_COUNTRIES = ['GB']
|
||||
_TESTS = [{
|
||||
'url': 'https://www.itv.com/hub/liar/2a4547a0012',
|
||||
'url': 'https://www.itv.com/hub/plebs/2a1873a0002',
|
||||
'info_dict': {
|
||||
'id': '2a4547a0012',
|
||||
'id': '2a1873a0002',
|
||||
'ext': 'mp4',
|
||||
'title': 'Liar - Series 2 - Episode 6',
|
||||
'description': 'md5:d0f91536569dec79ea184f0a44cca089',
|
||||
'series': 'Liar',
|
||||
'season_number': 2,
|
||||
'episode_number': 6,
|
||||
'title': 'Plebs - The Orgy',
|
||||
'description': 'md5:4d7159af53ebd5b36e8b3ec82a41fdb4',
|
||||
'series': 'Plebs',
|
||||
'season_number': 1,
|
||||
'episode_number': 1,
|
||||
'thumbnail': r're:https?://hubimages\.itv\.com/episode/2_1873_0002'
|
||||
},
|
||||
'params': {
|
||||
# m3u8 download
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.itv.com/hub/the-jonathan-ross-show/2a1166a0209',
|
||||
'info_dict': {
|
||||
'id': '2a1166a0209',
|
||||
'ext': 'mp4',
|
||||
'title': 'The Jonathan Ross Show - Series 17 - Episode 8',
|
||||
'description': 'md5:3023dcdd375db1bc9967186cdb3f1399',
|
||||
'series': 'The Jonathan Ross Show',
|
||||
'episode_number': 8,
|
||||
'season_number': 17,
|
||||
'thumbnail': r're:https?://hubimages\.itv\.com/episode/2_1873_0002'
|
||||
},
|
||||
'params': {
|
||||
# m3u8 download
|
||||
@@ -51,22 +74,16 @@ class ITVIE(InfoExtractor):
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
params = extract_attributes(self._search_regex(
|
||||
r'(?s)(<[^>]+id="video"[^>]*>)', webpage, 'params'))
|
||||
|
||||
ios_playlist_url = params.get('data-video-playlist') or params['data-video-id']
|
||||
hmac = params['data-video-hmac']
|
||||
headers = self.geo_verification_headers()
|
||||
headers.update({
|
||||
def _generate_api_headers(self, hmac):
|
||||
return merge_dicts({
|
||||
'Accept': 'application/vnd.itv.vod.playlist.v2+json',
|
||||
'Content-Type': 'application/json',
|
||||
'hmac': hmac.upper(),
|
||||
})
|
||||
ios_playlist = self._download_json(
|
||||
ios_playlist_url, video_id, data=json.dumps({
|
||||
}, self.geo_verification_headers())
|
||||
|
||||
def _call_api(self, video_id, playlist_url, headers, platform_tag, featureset, fatal=True):
|
||||
return self._download_json(
|
||||
playlist_url, video_id, data=json.dumps({
|
||||
'user': {
|
||||
'itvUserId': '',
|
||||
'entitlements': [],
|
||||
@@ -87,15 +104,61 @@ class ITVIE(InfoExtractor):
|
||||
},
|
||||
'variantAvailability': {
|
||||
'featureset': {
|
||||
'min': ['hls', 'aes', 'outband-webvtt'],
|
||||
'max': ['hls', 'aes', 'outband-webvtt']
|
||||
'min': featureset,
|
||||
'max': featureset
|
||||
},
|
||||
'platformTag': 'dotcom'
|
||||
'platformTag': platform_tag
|
||||
}
|
||||
}).encode(), headers=headers)
|
||||
video_data = ios_playlist['Playlist']['Video']
|
||||
ios_base_url = video_data.get('Base')
|
||||
}).encode(), headers=headers, fatal=fatal)
|
||||
|
||||
def _get_subtitles(self, video_id, variants, ios_playlist_url, headers, *args, **kwargs):
|
||||
subtitles = {}
|
||||
# Prefer last matching featureset
|
||||
# See: https://github.com/yt-dlp/yt-dlp/issues/986
|
||||
platform_tag_subs, featureset_subs = next(
|
||||
((platform_tag, featureset)
|
||||
for platform_tag, featuresets in reversed(variants.items()) for featureset in featuresets
|
||||
if try_get(featureset, lambda x: x[2]) == 'outband-webvtt'),
|
||||
(None, None))
|
||||
|
||||
if platform_tag_subs and featureset_subs:
|
||||
subs_playlist = self._call_api(
|
||||
video_id, ios_playlist_url, headers, platform_tag_subs, featureset_subs, fatal=False)
|
||||
subs = try_get(subs_playlist, lambda x: x['Playlist']['Video']['Subtitles'], list) or []
|
||||
for sub in subs:
|
||||
if not isinstance(sub, dict):
|
||||
continue
|
||||
href = url_or_none(sub.get('Href'))
|
||||
if not href:
|
||||
continue
|
||||
subtitles.setdefault('en', []).append({'url': href})
|
||||
return subtitles
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
params = extract_attributes(self._search_regex(
|
||||
r'(?s)(<[^>]+id="video"[^>]*>)', webpage, 'params'))
|
||||
variants = self._parse_json(
|
||||
try_get(params, lambda x: x['data-video-variants'], compat_str) or '{}',
|
||||
video_id, fatal=False)
|
||||
# Prefer last matching featureset
|
||||
# See: https://github.com/yt-dlp/yt-dlp/issues/986
|
||||
platform_tag_video, featureset_video = next(
|
||||
((platform_tag, featureset)
|
||||
for platform_tag, featuresets in reversed(variants.items()) for featureset in featuresets
|
||||
if try_get(featureset, lambda x: x[:2]) == ['hls', 'aes']),
|
||||
(None, None))
|
||||
if not platform_tag_video or not featureset_video:
|
||||
raise ExtractorError('No downloads available', expected=True, video_id=video_id)
|
||||
|
||||
ios_playlist_url = params.get('data-video-playlist') or params['data-video-id']
|
||||
headers = self._generate_api_headers(params['data-video-hmac'])
|
||||
ios_playlist = self._call_api(
|
||||
video_id, ios_playlist_url, headers, platform_tag_video, featureset_video)
|
||||
|
||||
video_data = try_get(ios_playlist, lambda x: x['Playlist']['Video'], dict) or {}
|
||||
ios_base_url = video_data.get('Base')
|
||||
formats = []
|
||||
for media_file in (video_data.get('MediaFiles') or []):
|
||||
href = media_file.get('Href')
|
||||
@@ -113,20 +176,6 @@ class ITVIE(InfoExtractor):
|
||||
'url': href,
|
||||
})
|
||||
self._sort_formats(formats)
|
||||
|
||||
subtitles = {}
|
||||
subs = video_data.get('Subtitles') or []
|
||||
for sub in subs:
|
||||
if not isinstance(sub, dict):
|
||||
continue
|
||||
href = url_or_none(sub.get('Href'))
|
||||
if not href:
|
||||
continue
|
||||
subtitles.setdefault('en', []).append({
|
||||
'url': href,
|
||||
'ext': determine_ext(href, 'vtt'),
|
||||
})
|
||||
|
||||
info = self._search_json_ld(webpage, video_id, default={})
|
||||
if not info:
|
||||
json_ld = self._parse_json(self._search_regex(
|
||||
@@ -140,13 +189,33 @@ class ITVIE(InfoExtractor):
|
||||
info = self._json_ld(item, video_id, fatal=False) or {}
|
||||
break
|
||||
|
||||
thumbnails = []
|
||||
thumbnail_url = try_get(params, lambda x: x['data-video-posterframe'], compat_str)
|
||||
if thumbnail_url:
|
||||
thumbnails.extend([{
|
||||
'url': thumbnail_url.format(width=1920, height=1080, quality=100, blur=0, bg='false'),
|
||||
'width': 1920,
|
||||
'height': 1080,
|
||||
}, {
|
||||
'url': urljoin(base_url(thumbnail_url), url_basename(thumbnail_url)),
|
||||
'preference': -2
|
||||
}])
|
||||
|
||||
thumbnail_url = self._html_search_meta(['og:image', 'twitter:image'], webpage, default=None)
|
||||
if thumbnail_url:
|
||||
thumbnails.append({
|
||||
'url': thumbnail_url,
|
||||
})
|
||||
self._remove_duplicate_formats(thumbnails)
|
||||
|
||||
return merge_dicts({
|
||||
'id': video_id,
|
||||
'title': self._html_search_meta(['og:title', 'twitter:title'], webpage),
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
'subtitles': self.extract_subtitles(video_id, variants, ios_playlist_url, headers),
|
||||
'duration': parse_duration(video_data.get('Duration')),
|
||||
'description': clean_html(get_element_by_class('episode-info__synopsis', webpage)),
|
||||
'thumbnails': thumbnails
|
||||
}, info)
|
||||
|
||||
|
||||
|
||||
+10
-16
@@ -3,7 +3,6 @@ from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
@@ -94,20 +93,21 @@ class IviIE(InfoExtractor):
|
||||
]
|
||||
})
|
||||
|
||||
bundled = hasattr(sys, 'frozen')
|
||||
|
||||
for site in (353, 183):
|
||||
content_data = (data % site).encode()
|
||||
if site == 353:
|
||||
if bundled:
|
||||
continue
|
||||
try:
|
||||
from Cryptodome.Cipher import Blowfish
|
||||
from Cryptodome.Hash import CMAC
|
||||
pycryptodomex_found = True
|
||||
pycryptodome_found = True
|
||||
except ImportError:
|
||||
pycryptodomex_found = False
|
||||
continue
|
||||
try:
|
||||
from Crypto.Cipher import Blowfish
|
||||
from Crypto.Hash import CMAC
|
||||
pycryptodome_found = True
|
||||
except ImportError:
|
||||
pycryptodome_found = False
|
||||
continue
|
||||
|
||||
timestamp = (self._download_json(
|
||||
self._LIGHT_URL, video_id,
|
||||
@@ -140,14 +140,8 @@ class IviIE(InfoExtractor):
|
||||
extractor_msg = 'Video %s does not exist'
|
||||
elif site == 353:
|
||||
continue
|
||||
elif bundled:
|
||||
raise ExtractorError(
|
||||
'This feature does not work from bundled exe. Run yt-dlp from sources.',
|
||||
expected=True)
|
||||
elif not pycryptodomex_found:
|
||||
raise ExtractorError(
|
||||
'pycryptodomex not found. Please install',
|
||||
expected=True)
|
||||
elif not pycryptodome_found:
|
||||
raise ExtractorError('pycryptodome not found. Please install', expected=True)
|
||||
elif message:
|
||||
extractor_msg += ': ' + message
|
||||
raise ExtractorError(extractor_msg % video_id, expected=True)
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
clean_html,
|
||||
try_get,
|
||||
)
|
||||
|
||||
|
||||
class KooIE(InfoExtractor):
|
||||
_VALID_URL = r'(?:https?://)(?:www\.)?kooapp\.com/koo/[^/]+/(?P<id>[^/&#$?]+)'
|
||||
_TESTS = [{ # Test for video in the comments
|
||||
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/946c4189-bc2d-4524-b95b-43f641e2adde',
|
||||
'info_dict': {
|
||||
'id': '946c4189-bc2d-4524-b95b-43f641e2adde',
|
||||
'ext': 'mp4',
|
||||
'title': 'test for video in comment',
|
||||
'description': 'md5:daa77dc214add4da8b6ea7d2226776e7',
|
||||
'timestamp': 1632215195,
|
||||
'uploader_id': 'ytdlpTestAccount',
|
||||
'uploader': 'yt-dlpTestAccount',
|
||||
'duration': 7000,
|
||||
'upload_date': '20210921'
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, { # Test for koo with long title
|
||||
'url': 'https://www.kooapp.com/koo/laxman_kumarDBFEC/33decbf7-5e1e-4bb8-bfd7-04744a064361',
|
||||
'info_dict': {
|
||||
'id': '33decbf7-5e1e-4bb8-bfd7-04744a064361',
|
||||
'ext': 'mp4',
|
||||
'title': 'md5:47a71c2337295330c5a19a8af1bbf450',
|
||||
'description': 'md5:06a6a84e9321499486dab541693d8425',
|
||||
'timestamp': 1632106884,
|
||||
'uploader_id': 'laxman_kumarDBFEC',
|
||||
'uploader': 'Laxman Kumar 🇮🇳',
|
||||
'duration': 46000,
|
||||
'upload_date': '20210920'
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, { # Test for audio
|
||||
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/a2a9c88e-ce4b-4d2d-952f-d06361c5b602',
|
||||
'info_dict': {
|
||||
'id': 'a2a9c88e-ce4b-4d2d-952f-d06361c5b602',
|
||||
'ext': 'mp4',
|
||||
'title': 'Test for audio',
|
||||
'description': 'md5:ecb9a2b6a5d34b736cecb53788cb11e8',
|
||||
'timestamp': 1632211634,
|
||||
'uploader_id': 'ytdlpTestAccount',
|
||||
'uploader': 'yt-dlpTestAccount',
|
||||
'duration': 214000,
|
||||
'upload_date': '20210921'
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, { # Test for video
|
||||
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/a3e56c53-c1ed-4ac9-ac02-ed1630e6b1d1',
|
||||
'info_dict': {
|
||||
'id': 'a3e56c53-c1ed-4ac9-ac02-ed1630e6b1d1',
|
||||
'ext': 'mp4',
|
||||
'title': 'Test for video',
|
||||
'description': 'md5:7afc4eb839074ddeb2beea5dd6fe9500',
|
||||
'timestamp': 1632211468,
|
||||
'uploader_id': 'ytdlpTestAccount',
|
||||
'uploader': 'yt-dlpTestAccount',
|
||||
'duration': 14000,
|
||||
'upload_date': '20210921'
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, { # Test for link
|
||||
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/01bf5b94-81a5-4d8e-a387-5f732022e15a',
|
||||
'skip': 'No video/audio found at the provided url.',
|
||||
'info_dict': {
|
||||
'id': '01bf5b94-81a5-4d8e-a387-5f732022e15a',
|
||||
'title': 'Test for link',
|
||||
'ext': 'none',
|
||||
},
|
||||
}, { # Test for images
|
||||
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/dc05d9cd-a61d-45fd-bb07-e8019d8ca8cb',
|
||||
'skip': 'No video/audio found at the provided url.',
|
||||
'info_dict': {
|
||||
'id': 'dc05d9cd-a61d-45fd-bb07-e8019d8ca8cb',
|
||||
'title': 'Test for images',
|
||||
'ext': 'none',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
id = self._match_id(url)
|
||||
data_json = self._download_json(f'https://www.kooapp.com/apiV1/ku/{id}?limit=20&offset=0&showSimilarKoos=true', id)['parentContent']
|
||||
item_json = next(content['items'][0] for content in data_json
|
||||
if try_get(content, lambda x: x['items'][0]['id']) == id)
|
||||
media_json = item_json['mediaMap']
|
||||
formats = []
|
||||
|
||||
mp4_url = media_json.get('videoMp4')
|
||||
video_m3u8_url = media_json.get('videoHls')
|
||||
if mp4_url:
|
||||
formats.append({
|
||||
'url': mp4_url,
|
||||
'ext': 'mp4',
|
||||
})
|
||||
if video_m3u8_url:
|
||||
formats.extend(self._extract_m3u8_formats(video_m3u8_url, id, fatal=False, ext='mp4'))
|
||||
if not formats:
|
||||
self.raise_no_formats('No video/audio found at the provided url.', expected=True)
|
||||
|
||||
self._sort_formats(formats)
|
||||
return {
|
||||
'id': id,
|
||||
'title': clean_html(item_json.get('title')),
|
||||
'description': f'{clean_html(item_json.get("title"))}\n\n{clean_html(item_json.get("enTransliteration"))}',
|
||||
'timestamp': item_json.get('createdAt'),
|
||||
'uploader_id': item_json.get('handle'),
|
||||
'uploader': item_json.get('name'),
|
||||
'duration': media_json.get('duration'),
|
||||
'formats': formats,
|
||||
}
|
||||
@@ -28,14 +28,19 @@ class LBRYBaseIE(InfoExtractor):
|
||||
_SUPPORTED_STREAM_TYPES = ['video', 'audio']
|
||||
|
||||
def _call_api_proxy(self, method, display_id, params, resource):
|
||||
return self._download_json(
|
||||
response = self._download_json(
|
||||
'https://api.lbry.tv/api/v1/proxy',
|
||||
display_id, 'Downloading %s JSON metadata' % resource,
|
||||
headers={'Content-Type': 'application/json-rpc'},
|
||||
data=json.dumps({
|
||||
'method': method,
|
||||
'params': params,
|
||||
}).encode())['result']
|
||||
}).encode())
|
||||
err = response.get('error')
|
||||
if err:
|
||||
raise ExtractorError(
|
||||
f'{self.IE_NAME} said: {err.get("code")} - {err.get("message")}', expected=True)
|
||||
return response['result']
|
||||
|
||||
def _resolve_url(self, url, display_id, resource):
|
||||
return self._call_api_proxy(
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from .common import InfoExtractor
|
||||
|
||||
|
||||
class MediaiteIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?mediaite.com/(?:tv|sports|politics|podcasts|opinion)/[\w-]+/'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.mediaite.com/sports/bill-burr-roasts-nfl-for-promoting-black-lives-matter-while-scheduling-more-games-after-all-the-sht-they-know-about-cte/',
|
||||
'info_dict': {
|
||||
'id': 'vPHKITzy',
|
||||
'ext': 'm4a',
|
||||
'title': 'Bill Burr On NFL And Black Lives Matter',
|
||||
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
|
||||
'thumbnail': 'https://cdn.jwplayer.com/v2/media/vPHKITzy/poster.jpg?width=720',
|
||||
'duration': 55,
|
||||
'timestamp': 1631630185,
|
||||
'upload_date': '20210914',
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, {
|
||||
'url': 'https://www.mediaite.com/tv/joe-scarborough-goes-off-on-tax-breaks-for-super-wealthy-largest-income-redistribution-scam-in-american-history/',
|
||||
'info_dict': {
|
||||
'id': 'eeFcK4Xm',
|
||||
'ext': 'mp4',
|
||||
'title': 'Morning Joe-6_16_52 am - 6_21_10 am-2021-09-14.mp4',
|
||||
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
|
||||
'thumbnail': 'https://cdn.jwplayer.com/v2/media/eeFcK4Xm/poster.jpg?width=720',
|
||||
'duration': 258,
|
||||
'timestamp': 1631618057,
|
||||
'upload_date': '20210914',
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, {
|
||||
'url': 'https://www.mediaite.com/politics/watch-rudy-giuliani-impersonates-queen-elizabeth-calls-mark-milley-an-asshle-in-bizarre-9-11-speech/',
|
||||
'info_dict': {
|
||||
'id': 'EiyiXKcr',
|
||||
'ext': 'mp4',
|
||||
'title': 'Giuliani 1',
|
||||
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
|
||||
'thumbnail': 'https://cdn.jwplayer.com/v2/media/EiyiXKcr/poster.jpg?width=720',
|
||||
'duration': 39,
|
||||
'timestamp': 1631536476,
|
||||
'upload_date': '20210913',
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, {
|
||||
'url': 'https://www.mediaite.com/podcasts/clarissa-ward-says-she-decided-to-become-a-journalist-on-9-11/',
|
||||
'info_dict': {
|
||||
'id': 'TxavoRTx',
|
||||
'ext': 'mp4',
|
||||
'title': 'clarissa-ward-3.mp4',
|
||||
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
|
||||
'thumbnail': 'https://cdn.jwplayer.com/v2/media/TxavoRTx/poster.jpg?width=720',
|
||||
'duration': 83,
|
||||
'timestamp': 1631311188,
|
||||
'upload_date': '20210910',
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, {
|
||||
'url': 'https://www.mediaite.com/opinion/mainstream-media-ignores-rose-mcgowans-bombshell-allegation-that-newsoms-wife-tried-to-silence-her-on-weinstein/',
|
||||
'info_dict': {
|
||||
'id': 'sEIWvKR7',
|
||||
'ext': 'mp4',
|
||||
'title': 'KTTV_09-13-2021_05.34.21',
|
||||
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
|
||||
'thumbnail': 'https://cdn.jwplayer.com/v2/media/sEIWvKR7/poster.jpg?width=720',
|
||||
'duration': 52,
|
||||
'timestamp': 1631553328,
|
||||
'upload_date': '20210913',
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
webpage = self._download_webpage(url, None)
|
||||
id = self._search_regex(r'data-video-id\s?=\s?\"([^\"]+)\"', webpage, 'id')
|
||||
data_json = self._download_json(f'https://cdn.jwplayer.com/v2/media/{id}', id)
|
||||
return self._parse_jwplayer_data(data_json)
|
||||
@@ -0,0 +1,104 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from ..utils import (
|
||||
unified_strdate
|
||||
)
|
||||
from .common import InfoExtractor
|
||||
from ..compat import (
|
||||
compat_urllib_parse_unquote,
|
||||
compat_str
|
||||
)
|
||||
|
||||
|
||||
class MediaKlikkIE(InfoExtractor):
|
||||
_VALID_URL = r'''(?x)^https?:\/\/(?:www\.)?
|
||||
(?:mediaklikk|m4sport|hirado|petofilive)\.hu\/.*?videok?\/
|
||||
(?:(?P<year>[0-9]{4})/(?P<month>[0-9]{1,2})/(?P<day>[0-9]{1,2})/)?
|
||||
(?P<id>[^/#?_]+)'''
|
||||
|
||||
_TESTS = [{
|
||||
# mediaklikk. date in html.
|
||||
'url': 'https://mediaklikk.hu/video/hazajaro-delnyugat-bacska-a-duna-menten-palankatol-doroszloig/',
|
||||
'info_dict': {
|
||||
'id': '4754129',
|
||||
'title': 'Hazajáró, DÉLNYUGAT-BÁCSKA – A Duna mentén Palánkától Doroszlóig',
|
||||
'ext': 'mp4',
|
||||
'upload_date': '20210901',
|
||||
'thumbnail': 'http://mediaklikk.hu/wp-content/uploads/sites/4/2014/02/hazajarouj_JO.jpg'
|
||||
}
|
||||
}, {
|
||||
# m4sport
|
||||
'url': 'https://m4sport.hu/video/2021/08/30/gyemant-liga-parizs/',
|
||||
'info_dict': {
|
||||
'id': '4754999',
|
||||
'title': 'Gyémánt Liga, Párizs',
|
||||
'ext': 'mp4',
|
||||
'upload_date': '20210830',
|
||||
'thumbnail': 'http://m4sport.hu/wp-content/uploads/sites/4/2021/08/vlcsnap-2021-08-30-18h21m20s10-1024x576.jpg'
|
||||
}
|
||||
}, {
|
||||
# m4sport with *video/ url and no date
|
||||
'url': 'https://m4sport.hu/bl-video/real-madrid-chelsea-1-1/',
|
||||
'info_dict': {
|
||||
'id': '4492099',
|
||||
'title': 'Real Madrid - Chelsea 1-1',
|
||||
'ext': 'mp4',
|
||||
'thumbnail': 'http://m4sport.hu/wp-content/uploads/sites/4/2021/04/Sequence-01.Still001-1024x576.png'
|
||||
}
|
||||
}, {
|
||||
# hirado
|
||||
'url': 'https://hirado.hu/videok/felteteleket-szabott-a-fovaros/',
|
||||
'info_dict': {
|
||||
'id': '4760120',
|
||||
'title': 'Feltételeket szabott a főváros',
|
||||
'ext': 'mp4',
|
||||
'thumbnail': 'http://hirado.hu/wp-content/uploads/sites/4/2021/09/vlcsnap-2021-09-01-20h20m37s165.jpg'
|
||||
}
|
||||
}, {
|
||||
# petofilive
|
||||
'url': 'https://petofilive.hu/video/2021/06/07/tha-shudras-az-akusztikban/',
|
||||
'info_dict': {
|
||||
'id': '4571948',
|
||||
'title': 'Tha Shudras az Akusztikban',
|
||||
'ext': 'mp4',
|
||||
'upload_date': '20210607',
|
||||
'thumbnail': 'http://petofilive.hu/wp-content/uploads/sites/4/2021/06/vlcsnap-2021-06-07-22h14m23s915-1024x576.jpg'
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = self._match_valid_url(url)
|
||||
display_id = mobj.group('id')
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
|
||||
player_data_str = self._html_search_regex(
|
||||
r'mtva_player_manager\.player\(document.getElementById\(.*\),\s?(\{.*\}).*\);', webpage, 'player data')
|
||||
player_data = self._parse_json(player_data_str, display_id, compat_urllib_parse_unquote)
|
||||
video_id = compat_str(player_data['contentId'])
|
||||
title = player_data.get('title') or self._og_search_title(webpage, fatal=False) or \
|
||||
self._html_search_regex(r'<h\d+\b[^>]+\bclass="article_title">([^<]+)<', webpage, 'title')
|
||||
|
||||
upload_date = unified_strdate(
|
||||
'%s-%s-%s' % (mobj.group('year'), mobj.group('month'), mobj.group('day')))
|
||||
if not upload_date:
|
||||
upload_date = unified_strdate(self._html_search_regex(
|
||||
r'<p+\b[^>]+\bclass="article_date">([^<]+)<', webpage, 'upload date', default=None))
|
||||
|
||||
player_data['video'] = player_data.pop('token')
|
||||
player_page = self._download_webpage('https://player.mediaklikk.hu/playernew/player.php', video_id, query=player_data)
|
||||
playlist_url = self._proto_relative_url(compat_urllib_parse_unquote(
|
||||
self._html_search_regex(r'\"file\":\s*\"(\\?/\\?/.*playlist\.m3u8)\"', player_page, 'playlist_url')).replace('\\/', '/'))
|
||||
|
||||
formats = self._extract_wowza_formats(
|
||||
playlist_url, video_id, skip_protocols=['f4m', 'smil', 'dash'])
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'display_id': display_id,
|
||||
'formats': formats,
|
||||
'upload_date': upload_date,
|
||||
'thumbnail': player_data.get('bgImage') or self._og_search_thumbnail(webpage)
|
||||
}
|
||||
@@ -44,7 +44,7 @@ class MTVServicesInfoExtractor(InfoExtractor):
|
||||
# Remove the templates, like &device={device}
|
||||
return re.sub(r'&[^=]*?={.*?}(?=(&|$))', '', url)
|
||||
|
||||
def _get_feed_url(self, uri):
|
||||
def _get_feed_url(self, uri, url=None):
|
||||
return self._FEED_URL
|
||||
|
||||
def _get_thumbnail_url(self, uri, itemdoc):
|
||||
@@ -229,9 +229,9 @@ class MTVServicesInfoExtractor(InfoExtractor):
|
||||
data['lang'] = self._LANG
|
||||
return data
|
||||
|
||||
def _get_videos_info(self, uri, use_hls=True):
|
||||
def _get_videos_info(self, uri, use_hls=True, url=None):
|
||||
video_id = self._id_from_uri(uri)
|
||||
feed_url = self._get_feed_url(uri)
|
||||
feed_url = self._get_feed_url(uri, url)
|
||||
info_url = update_url_query(feed_url, self._get_feed_query(uri))
|
||||
return self._get_videos_info_from_url(info_url, video_id, use_hls)
|
||||
|
||||
@@ -323,7 +323,7 @@ class MTVServicesInfoExtractor(InfoExtractor):
|
||||
title = url_basename(url)
|
||||
webpage = self._download_webpage(url, title)
|
||||
mgid = self._extract_mgid(webpage)
|
||||
videos_info = self._get_videos_info(mgid)
|
||||
videos_info = self._get_videos_info(mgid, url=url)
|
||||
return videos_info
|
||||
|
||||
|
||||
@@ -352,7 +352,7 @@ class MTVServicesEmbeddedIE(MTVServicesInfoExtractor):
|
||||
if mobj:
|
||||
return mobj.group('url')
|
||||
|
||||
def _get_feed_url(self, uri):
|
||||
def _get_feed_url(self, uri, url=None):
|
||||
video_id = self._id_from_uri(uri)
|
||||
config = self._download_json(
|
||||
'http://media.mtvnservices.com/pmt/e1/access/index.html?uri=%s&configtype=edge' % uri, video_id)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
|
||||
|
||||
class MuseScoreIE(InfoExtractor):
|
||||
_VALID_URL = r'(?:https?://)(?:www\.)?musescore\.com/(?:user/\d+|[^/]+)(?:/scores)?/(?P<id>[^#&?]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://musescore.com/user/73797/scores/142975',
|
||||
'info_dict': {
|
||||
'id': '142975',
|
||||
'ext': 'mp3',
|
||||
'title': 'WA Mozart Marche Turque (Turkish March fingered)',
|
||||
'description': 'md5:7ede08230e4eaabd67a4a98bb54d07be',
|
||||
'thumbnail': r're:(?:https?://)(?:www\.)?musescore\.com/.*\.png[^$]+',
|
||||
'uploader': 'PapyPiano',
|
||||
'creator': 'Wolfgang Amadeus Mozart',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://musescore.com/user/36164500/scores/6837638',
|
||||
'info_dict': {
|
||||
'id': '6837638',
|
||||
'ext': 'mp3',
|
||||
'title': 'Sweet Child O\' Mine – Guns N\' Roses sweet child',
|
||||
'description': 'md5:4dca71191c14abc312a0a4192492eace',
|
||||
'thumbnail': r're:(?:https?://)(?:www\.)?musescore\.com/.*\.png[^$]+',
|
||||
'uploader': 'roxbelviolin',
|
||||
'creator': 'Guns N´Roses Arr. Roxbel Violin',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://musescore.com/classicman/fur-elise',
|
||||
'info_dict': {
|
||||
'id': '33816',
|
||||
'ext': 'mp3',
|
||||
'title': 'Für Elise – Beethoven',
|
||||
'description': 'md5:49515a3556d5ecaf9fa4b2514064ac34',
|
||||
'thumbnail': r're:(?:https?://)(?:www\.)?musescore\.com/.*\.png[^$]+',
|
||||
'uploader': 'ClassicMan',
|
||||
'creator': 'Ludwig van Beethoven (1770–1827)',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://musescore.com/minh_cuteee/scores/6555384',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
webpage = self._download_webpage(url, None)
|
||||
url = self._og_search_url(webpage) or url
|
||||
id = self._match_id(url)
|
||||
mp3_url = self._download_json(f'https://musescore.com/api/jmuse?id={id}&index=0&type=mp3&v2=1', id,
|
||||
headers={'authorization': '63794e5461e4cfa046edfbdddfccc1ac16daffd2'})['info']['url']
|
||||
formats = [{
|
||||
'url': mp3_url,
|
||||
'ext': 'mp3',
|
||||
'vcodec': 'none',
|
||||
}]
|
||||
|
||||
return {
|
||||
'id': id,
|
||||
'formats': formats,
|
||||
'title': self._og_search_title(webpage),
|
||||
'description': self._og_search_description(webpage),
|
||||
'thumbnail': self._og_search_thumbnail(webpage),
|
||||
'uploader': self._html_search_meta('musescore:author', webpage, 'uploader'),
|
||||
'creator': self._html_search_meta('musescore:composer', webpage, 'composer'),
|
||||
}
|
||||
+126
-79
@@ -3,43 +3,68 @@ from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_str
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
js_to_json,
|
||||
qualities,
|
||||
try_get,
|
||||
url_or_none,
|
||||
urljoin,
|
||||
)
|
||||
from ..utils import try_get
|
||||
|
||||
|
||||
class MxplayerIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?mxplayer\.in/(?:movie|show/[-\w]+/[-\w]+)/(?P<display_id>[-\w]+)-(?P<id>\w+)'
|
||||
_VALID_URL = r'https?://(?:www\.)?mxplayer\.in/(?P<type>movie|show/[-\w]+/[-\w]+)/(?P<display_id>[-\w]+)-(?P<id>\w+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.mxplayer.in/show/watch-my-girlfriend-is-an-alien-hindi-dubbed/season-1/episode-1-online-9d2013d31d5835bb8400e3b3c5e7bb72',
|
||||
'info_dict': {
|
||||
'id': '9d2013d31d5835bb8400e3b3c5e7bb72',
|
||||
'ext': 'mp4',
|
||||
'title': 'Episode 1',
|
||||
'description': 'md5:62ed43eb9fec5efde5cf3bd1040b7670',
|
||||
'season_number': 1,
|
||||
'episode_number': 1,
|
||||
'duration': 2451,
|
||||
'season': 'Season 1',
|
||||
'series': 'My Girlfriend Is An Alien (Hindi Dubbed)',
|
||||
'thumbnail': 'https://qqcdnpictest.mxplay.com/pic/9d2013d31d5835bb8400e3b3c5e7bb72/en/16x9/320x180/9562f5f8df42cad09c9a9c4e69eb1567_1920x1080.webp',
|
||||
'episode': 'Episode 1'
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.mxplayer.in/movie/watch-knock-knock-hindi-dubbed-movie-online-b9fa28df3bfb8758874735bbd7d2655a?watch=true',
|
||||
'info_dict': {
|
||||
'id': 'b9fa28df3bfb8758874735bbd7d2655a',
|
||||
'ext': 'mp4',
|
||||
'title': 'Knock Knock (Hindi Dubbed)',
|
||||
'description': 'md5:b195ba93ff1987309cfa58e2839d2a5b'
|
||||
'description': 'md5:b195ba93ff1987309cfa58e2839d2a5b',
|
||||
'season_number': 0,
|
||||
'episode_number': 0,
|
||||
'duration': 5970,
|
||||
'season': 'Season 0',
|
||||
'series': None,
|
||||
'thumbnail': 'https://qqcdnpictest.mxplay.com/pic/b9fa28df3bfb8758874735bbd7d2655a/en/16x9/320x180/test_pic1588676032011.webp',
|
||||
'episode': 'Episode 0'
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
'skip_download': True,
|
||||
'format': 'bestvideo'
|
||||
}
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.mxplayer.in/show/watch-shaitaan/season-1/the-infamous-taxi-gang-of-meerut-online-45055d5bcff169ad48f2ad7552a83d6c',
|
||||
'info_dict': {
|
||||
'id': '45055d5bcff169ad48f2ad7552a83d6c',
|
||||
'ext': 'm3u8',
|
||||
'ext': 'mp4',
|
||||
'title': 'The infamous taxi gang of Meerut',
|
||||
'description': 'md5:033a0a7e3fd147be4fb7e07a01a3dc28',
|
||||
'season_number': 1,
|
||||
'episode_number': 1,
|
||||
'duration': 2332,
|
||||
'season': 'Season 1',
|
||||
'series': 'Shaitaan'
|
||||
'series': 'Shaitaan',
|
||||
'thumbnail': 'https://qqcdnpictest.mxplay.com/pic/45055d5bcff169ad48f2ad7552a83d6c/en/16x9/320x180/voot_8e7d5f8d8183340869279c732c1e3a43.webp',
|
||||
'episode': 'Episode 1'
|
||||
},
|
||||
'params': {
|
||||
'format': 'best',
|
||||
'skip_download': True,
|
||||
}
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.mxplayer.in/show/watch-aashram/chapter-1/duh-swapna-online-d445579792b0135598ba1bc9088a84cb',
|
||||
'info_dict': {
|
||||
@@ -47,88 +72,110 @@ class MxplayerIE(InfoExtractor):
|
||||
'ext': 'mp4',
|
||||
'title': 'Duh Swapna',
|
||||
'description': 'md5:35ff39c4bdac403c53be1e16a04192d8',
|
||||
'season_number': 1,
|
||||
'episode_number': 3,
|
||||
'duration': 2568,
|
||||
'season': 'Chapter 1',
|
||||
'series': 'Aashram'
|
||||
'series': 'Aashram',
|
||||
'thumbnail': 'https://qqcdnpictest.mxplay.com/pic/d445579792b0135598ba1bc9088a84cb/en/4x3/1600x1200/test_pic1624819307993.webp',
|
||||
'episode': 'Episode 3'
|
||||
},
|
||||
'expected_warnings': ['Unknown MIME type application/mp4 in DASH manifest'],
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
'skip_download': True,
|
||||
'format': 'bestvideo'
|
||||
}
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.mxplayer.in/show/watch-dangerous/season-1/chapter-1-online-5a351b4f9fb69436f6bd6ae3a1a75292',
|
||||
'info_dict': {
|
||||
'id': '5a351b4f9fb69436f6bd6ae3a1a75292',
|
||||
'ext': 'mp4',
|
||||
'title': 'Chapter 1',
|
||||
'description': 'md5:233886b8598bc91648ac098abe1d288f',
|
||||
'season_number': 1,
|
||||
'episode_number': 1,
|
||||
'duration': 1305,
|
||||
'season': 'Season 1',
|
||||
'series': 'Dangerous',
|
||||
'thumbnail': 'https://qqcdnpictest.mxplay.com/pic/5a351b4f9fb69436f6bd6ae3a1a75292/en/4x3/1600x1200/test_pic1624706302350.webp',
|
||||
'episode': 'Episode 1'
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.mxplayer.in/movie/watch-the-attacks-of-2611-movie-online-0452f0d80226c398d63ce7e3ea40fa2d',
|
||||
'info_dict': {
|
||||
'id': '0452f0d80226c398d63ce7e3ea40fa2d',
|
||||
'ext': 'mp4',
|
||||
'title': 'The Attacks of 26/11',
|
||||
'description': 'md5:689bacd29e97b3f31eaf519eb14127e5',
|
||||
'season_number': 0,
|
||||
'episode_number': 0,
|
||||
'duration': 6085,
|
||||
'season': 'Season 0',
|
||||
'series': None,
|
||||
'thumbnail': 'https://qqcdnpictest.mxplay.com/pic/0452f0d80226c398d63ce7e3ea40fa2d/en/16x9/320x180/00c8955dab5e5d340dbde643f9b1f6fd_1920x1080.webp',
|
||||
'episode': 'Episode 0'
|
||||
},
|
||||
'params': {
|
||||
'format': 'best',
|
||||
'skip_download': True,
|
||||
},
|
||||
}]
|
||||
|
||||
def _get_stream_urls(self, video_dict):
|
||||
stream_provider_dict = try_get(
|
||||
video_dict,
|
||||
lambda x: x['stream'][x['stream']['provider']])
|
||||
if not stream_provider_dict:
|
||||
raise ExtractorError('No stream provider found', expected=True)
|
||||
|
||||
for stream_name, stream in stream_provider_dict.items():
|
||||
if stream_name in ('hls', 'dash', 'hlsUrl', 'dashUrl'):
|
||||
stream_type = stream_name.replace('Url', '')
|
||||
if isinstance(stream, dict):
|
||||
for quality, stream_url in stream.items():
|
||||
if stream_url:
|
||||
yield stream_type, quality, stream_url
|
||||
else:
|
||||
yield stream_type, 'base', stream
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id, video_id = self._match_valid_url(url).groups()
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
source = self._parse_json(
|
||||
js_to_json(self._html_search_regex(
|
||||
r'(?s)<script>window\.state\s*[:=]\s(\{.+\})\n(\w+).*(</script>).*',
|
||||
webpage, 'WindowState')),
|
||||
video_id)
|
||||
if not source:
|
||||
raise ExtractorError('Cannot find source', expected=True)
|
||||
|
||||
config_dict = source['config']
|
||||
video_dict = source['entities'][video_id]
|
||||
type, display_id, video_id = self._match_valid_url(url).groups()
|
||||
type = 'movie_film' if type == 'movie' else 'tvshow_episode'
|
||||
API_URL = 'https://androidapi.mxplay.com/v1/detail/'
|
||||
headers = {
|
||||
'X-Av-Code': '23',
|
||||
'X-Country': 'IN',
|
||||
'X-Platform': 'android',
|
||||
'X-App-Version': '1370001318',
|
||||
'X-Resolution': '3840x2160',
|
||||
}
|
||||
data_json = self._download_json(f'{API_URL}{type}/{video_id}', display_id, headers=headers)['profile']
|
||||
|
||||
season, series = None, None
|
||||
for dct in data_json.get('levelInfos', []):
|
||||
if dct.get('type') == 'tvshow_season':
|
||||
season = dct.get('name')
|
||||
elif dct.get('type') == 'tvshow_show':
|
||||
series = dct.get('name')
|
||||
thumbnails = []
|
||||
for i in video_dict.get('imageInfo') or []:
|
||||
for thumb in data_json.get('poster', []):
|
||||
thumbnails.append({
|
||||
'url': urljoin(config_dict['imageBaseUrl'], i['url']),
|
||||
'width': i['width'],
|
||||
'height': i['height'],
|
||||
'url': thumb.get('url'),
|
||||
'width': thumb.get('width'),
|
||||
'height': thumb.get('height'),
|
||||
})
|
||||
|
||||
formats = []
|
||||
get_quality = qualities(['main', 'base', 'high'])
|
||||
for stream_type, quality, stream_url in self._get_stream_urls(video_dict):
|
||||
format_url = url_or_none(urljoin(config_dict['videoCdnBaseUrl'], stream_url))
|
||||
if not format_url:
|
||||
continue
|
||||
if stream_type == 'dash':
|
||||
dash_formats = self._extract_mpd_formats(
|
||||
format_url, video_id, mpd_id='dash-%s' % quality, headers={'Referer': url})
|
||||
for frmt in dash_formats:
|
||||
frmt['quality'] = get_quality(quality)
|
||||
formats.extend(dash_formats)
|
||||
dash_formats_h265 = self._extract_mpd_formats(
|
||||
format_url.replace('h264_high', 'h265_main'), video_id, mpd_id='dash-%s' % quality, headers={'Referer': url}, fatal=False)
|
||||
for frmt in dash_formats_h265:
|
||||
frmt['quality'] = get_quality(quality)
|
||||
formats.extend(dash_formats_h265)
|
||||
elif stream_type == 'hls':
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
format_url, video_id, fatal=False,
|
||||
m3u8_id='hls-%s' % quality, quality=get_quality(quality), ext='mp4'))
|
||||
|
||||
subtitles = {}
|
||||
for dct in data_json.get('playInfo', []):
|
||||
if dct.get('extension') == 'mpd':
|
||||
frmt, subs = self._extract_mpd_formats_and_subtitles(dct.get('playUrl'), display_id, fatal=False)
|
||||
formats.extend(frmt)
|
||||
subtitles = self._merge_subtitles(subtitles, subs)
|
||||
elif dct.get('extension') == 'm3u8':
|
||||
frmt, subs = self._extract_m3u8_formats_and_subtitles(dct.get('playUrl'), display_id, fatal=False)
|
||||
formats.extend(frmt)
|
||||
subtitles = self._merge_subtitles(subtitles, subs)
|
||||
self._sort_formats(formats)
|
||||
return {
|
||||
'id': video_id,
|
||||
'display_id': display_id,
|
||||
'title': video_dict['title'] or self._og_search_title(webpage),
|
||||
'formats': formats,
|
||||
'description': video_dict.get('description'),
|
||||
'season': try_get(video_dict, lambda x: x['container']['title']),
|
||||
'series': try_get(video_dict, lambda x: x['container']['container']['title']),
|
||||
'title': data_json.get('name') or display_id,
|
||||
'description': data_json.get('description'),
|
||||
'season_number': data_json.get('seasonNum'),
|
||||
'episode_number': data_json.get('episodeNum'),
|
||||
'duration': data_json.get('duration'),
|
||||
'season': season,
|
||||
'series': series,
|
||||
'thumbnails': thumbnails,
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
}
|
||||
|
||||
|
||||
|
||||
+84
-85
@@ -1,15 +1,14 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
determine_ext,
|
||||
int_or_none,
|
||||
merge_dicts,
|
||||
parse_iso8601,
|
||||
parse_duration,
|
||||
qualities,
|
||||
try_get,
|
||||
unified_strdate,
|
||||
urljoin,
|
||||
)
|
||||
|
||||
@@ -28,110 +27,110 @@ class NDRIE(NDRBaseIE):
|
||||
IE_DESC = 'NDR.de - Norddeutscher Rundfunk'
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:daserste\.)?ndr\.de/(?:[^/]+/)*(?P<display_id>[^/?#]+),(?P<id>[\da-z]+)\.html'
|
||||
_TESTS = [{
|
||||
# httpVideo, same content id
|
||||
'url': 'http://www.ndr.de/fernsehen/Party-Poette-und-Parade,hafengeburtstag988.html',
|
||||
'md5': '6515bc255dc5c5f8c85bbc38e035a659',
|
||||
'info_dict': {
|
||||
'id': 'hafengeburtstag988',
|
||||
'display_id': 'Party-Poette-und-Parade',
|
||||
'ext': 'mp4',
|
||||
'title': 'Party, Pötte und Parade',
|
||||
'thumbnail': 'https://www.ndr.de/fernsehen/hafengeburtstag990_v-contentxl.jpg',
|
||||
'description': 'md5:ad14f9d2f91d3040b6930c697e5f6b4c',
|
||||
'uploader': 'ndrtv',
|
||||
'timestamp': 1431108900,
|
||||
'upload_date': '20150510',
|
||||
'series': None,
|
||||
'channel': 'NDR Fernsehen',
|
||||
'upload_date': '20150508',
|
||||
'duration': 3498,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
# httpVideo, different content id
|
||||
'url': 'http://www.ndr.de/sport/fussball/40-Osnabrueck-spielt-sich-in-einen-Rausch,osna270.html',
|
||||
'md5': '1043ff203eab307f0c51702ec49e9a71',
|
||||
'url': 'https://www.ndr.de/sport/fussball/Rostocks-Matchwinner-Froede-Ein-Hansa-Debuet-wie-im-Maerchen,hansa10312.html',
|
||||
'only_matching': True
|
||||
}, {
|
||||
'url': 'https://www.ndr.de/nachrichten/niedersachsen/kommunalwahl_niedersachsen_2021/Grosse-Parteien-zufrieden-mit-Ergebnissen-der-Kommunalwahl,kommunalwahl1296.html',
|
||||
'info_dict': {
|
||||
'id': 'osna272',
|
||||
'display_id': '40-Osnabrueck-spielt-sich-in-einen-Rausch',
|
||||
'id': 'kommunalwahl1296',
|
||||
'ext': 'mp4',
|
||||
'title': 'Osnabrück - Wehen Wiesbaden: Die Highlights',
|
||||
'description': 'md5:32e9b800b3d2d4008103752682d5dc01',
|
||||
'uploader': 'ndrtv',
|
||||
'timestamp': 1442059200,
|
||||
'upload_date': '20150912',
|
||||
'duration': 510,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
'title': 'Die Spitzenrunde: Die Wahl aus Sicht der Landespolitik',
|
||||
'thumbnail': 'https://www.ndr.de/fernsehen/screenshot1194912_v-contentxl.jpg',
|
||||
'description': 'md5:5c6e2ad744cef499135735a1036d7aa7',
|
||||
'series': 'Hallo Niedersachsen',
|
||||
'channel': 'NDR Fernsehen',
|
||||
'upload_date': '20210913',
|
||||
'duration': 438,
|
||||
},
|
||||
}, {
|
||||
# httpAudio, same content id
|
||||
'url': 'http://www.ndr.de/info/La-Valette-entgeht-der-Hinrichtung,audio51535.html',
|
||||
'md5': 'bb3cd38e24fbcc866d13b50ca59307b8',
|
||||
'info_dict': {
|
||||
'id': 'audio51535',
|
||||
'display_id': 'La-Valette-entgeht-der-Hinrichtung',
|
||||
'ext': 'mp3',
|
||||
'title': 'La Valette entgeht der Hinrichtung',
|
||||
'description': 'md5:22f9541913a40fe50091d5cdd7c9f536',
|
||||
'uploader': 'ndrinfo',
|
||||
'timestamp': 1290626100,
|
||||
'upload_date': '20140729',
|
||||
'duration': 884,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
# with subtitles
|
||||
'url': 'https://www.ndr.de/fernsehen/sendungen/extra_3/extra-3-Satiremagazin-mit-Christian-Ehring,sendung1091858.html',
|
||||
'info_dict': {
|
||||
'id': 'extra18674',
|
||||
'display_id': 'extra-3-Satiremagazin-mit-Christian-Ehring',
|
||||
'id': 'sendung1091858',
|
||||
'ext': 'mp4',
|
||||
'title': 'Extra 3 vom 11.11.2020 mit Christian Ehring',
|
||||
'description': 'md5:42ee53990a715eaaf4dc7f13a3bd56c6',
|
||||
'uploader': 'ndrtv',
|
||||
'upload_date': '20201113',
|
||||
'thumbnail': 'https://www.ndr.de/fernsehen/screenshot983938_v-contentxl.jpg',
|
||||
'description': 'md5:700f6de264010585012a72f97b0ac0c9',
|
||||
'series': 'extra 3',
|
||||
'channel': 'NDR Fernsehen',
|
||||
'upload_date': '20201111',
|
||||
'duration': 1749,
|
||||
'subtitles': {
|
||||
'de': [{
|
||||
'ext': 'ttml',
|
||||
'url': r're:^https://www\.ndr\.de.+',
|
||||
}],
|
||||
},
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
'expected_warnings': ['Unable to download f4m manifest'],
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.ndr.de/Fettes-Brot-Ferris-MC-und-Thees-Uhlmann-live-on-stage,festivalsommer116.html',
|
||||
'only_matching': True,
|
||||
'url': 'http://www.ndr.de/info/La-Valette-entgeht-der-Hinrichtung,audio51535.html',
|
||||
'info_dict': {
|
||||
'id': 'audio51535',
|
||||
'ext': 'mp3',
|
||||
'title': 'La Valette entgeht der Hinrichtung',
|
||||
'thumbnail': 'https://www.ndr.de/mediathek/mediathekbild140_v-podcast.jpg',
|
||||
'description': 'md5:22f9541913a40fe50091d5cdd7c9f536',
|
||||
'upload_date': '20140729',
|
||||
'duration': 884.0,
|
||||
},
|
||||
'expected_warnings': ['unable to extract json url'],
|
||||
}]
|
||||
|
||||
def _extract_embed(self, webpage, display_id, id):
|
||||
embed_url = self._html_search_meta(
|
||||
'embedURL', webpage, 'embed URL',
|
||||
default=None) or self._search_regex(
|
||||
r'\bembedUrl["\']\s*:\s*(["\'])(?P<url>(?:(?!\1).)+)\1', webpage,
|
||||
'embed URL', fatal=False, group='url')
|
||||
if embed_url is None:
|
||||
return self.url_result('ndr:%s' % id, ie=NDREmbedBaseIE.ie_key())
|
||||
description = self._search_regex(
|
||||
r'<p[^>]+itemprop="description">([^<]+)</p>',
|
||||
webpage, 'description', default=None) or self._og_search_description(webpage)
|
||||
timestamp = parse_iso8601(
|
||||
self._search_regex(
|
||||
r'<span[^>]+itemprop="(?:datePublished|uploadDate)"[^>]+content="([^"]+)"',
|
||||
webpage, 'upload date', default=None))
|
||||
info = self._search_json_ld(webpage, display_id, default={})
|
||||
return merge_dicts({
|
||||
'_type': 'url_transparent',
|
||||
'url': embed_url,
|
||||
'display_id': display_id,
|
||||
'description': description,
|
||||
'timestamp': timestamp,
|
||||
}, info)
|
||||
formats = []
|
||||
base_url = 'https://www.ndr.de'
|
||||
json_url = self._search_regex(r'<iframe[^>]+src=\"([^\"]+)_theme-ndrde[^\.]*\.html\"', webpage,
|
||||
'json url', fatal=False)
|
||||
if json_url:
|
||||
data_json = self._download_json(base_url + json_url.replace('ardplayer_image', 'ardjson_image') + '.json',
|
||||
id, fatal=False)
|
||||
info_json = data_json.get('_info', {})
|
||||
media_json = try_get(data_json, lambda x: x['_mediaArray'][0]['_mediaStreamArray'])
|
||||
for media in media_json:
|
||||
if media.get('_quality') == 'auto':
|
||||
formats.extend(self._extract_m3u8_formats(media['_stream'], id))
|
||||
subtitles = {}
|
||||
sub_url = data_json.get('_subtitleUrl')
|
||||
if sub_url:
|
||||
subtitles.setdefault('de', []).append({
|
||||
'url': base_url + sub_url,
|
||||
})
|
||||
self._sort_formats(formats)
|
||||
return {
|
||||
'id': id,
|
||||
'title': info_json.get('clipTitle'),
|
||||
'thumbnail': base_url + data_json.get('_previewImage'),
|
||||
'description': info_json.get('clipDescription'),
|
||||
'series': info_json.get('seriesTitle') or None,
|
||||
'channel': info_json.get('channelTitle'),
|
||||
'upload_date': unified_strdate(info_json.get('clipDate')),
|
||||
'duration': data_json.get('_duration'),
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
}
|
||||
else:
|
||||
json_url = base_url + self._search_regex(r'apiUrl\s?=\s?\'([^\']+)\'', webpage, 'json url').replace(
|
||||
'_belongsToPodcast-', '')
|
||||
data_json = self._download_json(json_url, id, fatal=False)
|
||||
return {
|
||||
'id': id,
|
||||
'title': data_json.get('title'),
|
||||
'thumbnail': base_url + data_json.get('poster'),
|
||||
'description': data_json.get('summary'),
|
||||
'upload_date': unified_strdate(data_json.get('publicationDate')),
|
||||
'duration': parse_duration(data_json.get('duration')),
|
||||
'formats': [{
|
||||
'url': try_get(data_json, (lambda x: x['audio'][0]['url'], lambda x: x['files'][0]['url'])),
|
||||
'vcodec': 'none',
|
||||
'ext': 'mp3',
|
||||
}],
|
||||
}
|
||||
|
||||
|
||||
class NJoyIE(NDRBaseIE):
|
||||
|
||||
+238
-238
@@ -1,238 +1,238 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
from urllib.error import HTTPError
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_str, compat_urllib_parse_unquote, compat_urllib_parse_quote
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
parse_iso8601,
|
||||
try_get,
|
||||
urljoin,
|
||||
)
|
||||
|
||||
|
||||
class NebulaIE(InfoExtractor):
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:watchnebula\.com|nebula\.app)/videos/(?P<id>[-\w]+)'
|
||||
_TESTS = [
|
||||
{
|
||||
'url': 'https://nebula.app/videos/that-time-disney-remade-beauty-and-the-beast',
|
||||
'md5': 'fe79c4df8b3aa2fea98a93d027465c7e',
|
||||
'info_dict': {
|
||||
'id': '5c271b40b13fd613090034fd',
|
||||
'ext': 'mp4',
|
||||
'title': 'That Time Disney Remade Beauty and the Beast',
|
||||
'description': 'Note: this video was originally posted on YouTube with the sponsor read included. We weren’t able to remove it without reducing video quality, so it’s presented here in its original context.',
|
||||
'upload_date': '20180731',
|
||||
'timestamp': 1533009600,
|
||||
'channel': 'Lindsay Ellis',
|
||||
'uploader': 'Lindsay Ellis',
|
||||
},
|
||||
'params': {
|
||||
'usenetrc': True,
|
||||
},
|
||||
'skip': 'All Nebula content requires authentication',
|
||||
},
|
||||
{
|
||||
'url': 'https://nebula.app/videos/the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore',
|
||||
'md5': '6d4edd14ce65720fa63aba5c583fb328',
|
||||
'info_dict': {
|
||||
'id': '5e7e78171aaf320001fbd6be',
|
||||
'ext': 'mp4',
|
||||
'title': 'Landing Craft - How The Allies Got Ashore',
|
||||
'description': r're:^In this episode we explore the unsung heroes of D-Day, the landing craft.',
|
||||
'upload_date': '20200327',
|
||||
'timestamp': 1585348140,
|
||||
'channel': 'The Logistics of D-Day',
|
||||
'uploader': 'The Logistics of D-Day',
|
||||
},
|
||||
'params': {
|
||||
'usenetrc': True,
|
||||
},
|
||||
'skip': 'All Nebula content requires authentication',
|
||||
},
|
||||
{
|
||||
'url': 'https://nebula.app/videos/money-episode-1-the-draw',
|
||||
'md5': '8c7d272910eea320f6f8e6d3084eecf5',
|
||||
'info_dict': {
|
||||
'id': '5e779ebdd157bc0001d1c75a',
|
||||
'ext': 'mp4',
|
||||
'title': 'Episode 1: The Draw',
|
||||
'description': r'contains:There’s free money on offer… if the players can all work together.',
|
||||
'upload_date': '20200323',
|
||||
'timestamp': 1584980400,
|
||||
'channel': 'Tom Scott Presents: Money',
|
||||
'uploader': 'Tom Scott Presents: Money',
|
||||
},
|
||||
'params': {
|
||||
'usenetrc': True,
|
||||
},
|
||||
'skip': 'All Nebula content requires authentication',
|
||||
},
|
||||
{
|
||||
'url': 'https://watchnebula.com/videos/money-episode-1-the-draw',
|
||||
'only_matching': True,
|
||||
},
|
||||
]
|
||||
_NETRC_MACHINE = 'watchnebula'
|
||||
|
||||
_nebula_token = None
|
||||
|
||||
def _retrieve_nebula_auth(self):
|
||||
"""
|
||||
Log in to Nebula, and returns a Nebula API token
|
||||
"""
|
||||
|
||||
username, password = self._get_login_info()
|
||||
if not (username and password):
|
||||
self.raise_login_required()
|
||||
|
||||
self.report_login()
|
||||
data = json.dumps({'email': username, 'password': password}).encode('utf8')
|
||||
response = self._download_json(
|
||||
'https://api.watchnebula.com/api/v1/auth/login/',
|
||||
data=data, fatal=False, video_id=None,
|
||||
headers={
|
||||
'content-type': 'application/json',
|
||||
# Submitting the 'sessionid' cookie always causes a 403 on auth endpoint
|
||||
'cookie': ''
|
||||
},
|
||||
note='Authenticating to Nebula with supplied credentials',
|
||||
errnote='Authentication failed or rejected')
|
||||
if not response or not response.get('key'):
|
||||
self.raise_login_required()
|
||||
|
||||
# save nebula token as cookie
|
||||
self._set_cookie(
|
||||
'nebula.app', 'nebula-auth',
|
||||
compat_urllib_parse_quote(
|
||||
json.dumps({
|
||||
"apiToken": response["key"],
|
||||
"isLoggingIn": False,
|
||||
"isLoggingOut": False,
|
||||
}, separators=(",", ":"))),
|
||||
expire_time=int(time.time()) + 86400 * 365,
|
||||
)
|
||||
|
||||
return response['key']
|
||||
|
||||
def _retrieve_zype_api_key(self, page_url, display_id):
|
||||
"""
|
||||
Retrieves the Zype API key
|
||||
"""
|
||||
|
||||
# Find the js that has the API key from the webpage and download it
|
||||
webpage = self._download_webpage(page_url, video_id=display_id)
|
||||
main_script_relpath = self._search_regex(
|
||||
r'<script[^>]*src="(?P<script_relpath>[^"]*main.[0-9a-f]*.chunk.js)"[^>]*>', webpage,
|
||||
group='script_relpath', name='script relative path', fatal=True)
|
||||
main_script_abspath = urljoin(page_url, main_script_relpath)
|
||||
main_script = self._download_webpage(main_script_abspath, video_id=display_id,
|
||||
note='Retrieving Zype API key')
|
||||
|
||||
api_key = self._search_regex(
|
||||
r'REACT_APP_ZYPE_API_KEY\s*:\s*"(?P<api_key>[\w-]*)"', main_script,
|
||||
group='api_key', name='API key', fatal=True)
|
||||
|
||||
return api_key
|
||||
|
||||
def _call_zype_api(self, path, params, video_id, api_key, note):
|
||||
"""
|
||||
A helper for making calls to the Zype API.
|
||||
"""
|
||||
query = {'api_key': api_key, 'per_page': 1}
|
||||
query.update(params)
|
||||
return self._download_json('https://api.zype.com' + path, video_id, query=query, note=note)
|
||||
|
||||
def _call_nebula_api(self, path, video_id, access_token, note):
|
||||
"""
|
||||
A helper for making calls to the Nebula API.
|
||||
"""
|
||||
return self._download_json('https://api.watchnebula.com/api/v1' + path, video_id, headers={
|
||||
'Authorization': 'Token {access_token}'.format(access_token=access_token)
|
||||
}, note=note)
|
||||
|
||||
def _fetch_zype_access_token(self, video_id):
|
||||
try:
|
||||
user_object = self._call_nebula_api('/auth/user/', video_id, self._nebula_token, note='Retrieving Zype access token')
|
||||
except ExtractorError as exc:
|
||||
# if 401, attempt credential auth and retry
|
||||
if exc.cause and isinstance(exc.cause, HTTPError) and exc.cause.code == 401:
|
||||
self._nebula_token = self._retrieve_nebula_auth()
|
||||
user_object = self._call_nebula_api('/auth/user/', video_id, self._nebula_token, note='Retrieving Zype access token')
|
||||
else:
|
||||
raise
|
||||
|
||||
access_token = try_get(user_object, lambda x: x['zype_auth_info']['access_token'], compat_str)
|
||||
if not access_token:
|
||||
if try_get(user_object, lambda x: x['is_subscribed'], bool):
|
||||
# TODO: Reimplement the same Zype token polling the Nebula frontend implements
|
||||
# see https://github.com/ytdl-org/youtube-dl/pull/24805#issuecomment-749231532
|
||||
raise ExtractorError(
|
||||
'Unable to extract Zype access token from Nebula API authentication endpoint. '
|
||||
'Open an arbitrary video in a browser with this account to generate a token',
|
||||
expected=True)
|
||||
raise ExtractorError('Unable to extract Zype access token from Nebula API authentication endpoint')
|
||||
return access_token
|
||||
|
||||
def _extract_channel_title(self, video_meta):
|
||||
# TODO: Implement the API calls giving us the channel list,
|
||||
# so that we can do the title lookup and then figure out the channel URL
|
||||
categories = video_meta.get('categories', []) if video_meta else []
|
||||
# the channel name is the value of the first category
|
||||
for category in categories:
|
||||
if category.get('value'):
|
||||
return category['value'][0]
|
||||
|
||||
def _real_initialize(self):
|
||||
# check cookie jar for valid token
|
||||
nebula_cookies = self._get_cookies('https://nebula.app')
|
||||
nebula_cookie = nebula_cookies.get('nebula-auth')
|
||||
if nebula_cookie:
|
||||
self.to_screen('Authenticating to Nebula with token from cookie jar')
|
||||
nebula_cookie_value = compat_urllib_parse_unquote(nebula_cookie.value)
|
||||
self._nebula_token = self._parse_json(nebula_cookie_value, None).get('apiToken')
|
||||
|
||||
# try to authenticate using credentials if no valid token has been found
|
||||
if not self._nebula_token:
|
||||
self._nebula_token = self._retrieve_nebula_auth()
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
api_key = self._retrieve_zype_api_key(url, display_id)
|
||||
|
||||
response = self._call_zype_api('/videos', {'friendly_title': display_id},
|
||||
display_id, api_key, note='Retrieving metadata from Zype')
|
||||
if len(response.get('response') or []) != 1:
|
||||
raise ExtractorError('Unable to find video on Zype API')
|
||||
video_meta = response['response'][0]
|
||||
|
||||
video_id = video_meta['_id']
|
||||
zype_access_token = self._fetch_zype_access_token(display_id)
|
||||
|
||||
channel_title = self._extract_channel_title(video_meta)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'display_id': display_id,
|
||||
'_type': 'url_transparent',
|
||||
'ie_key': 'Zype',
|
||||
'url': 'https://player.zype.com/embed/%s.html?access_token=%s' % (video_id, zype_access_token),
|
||||
'title': video_meta.get('title'),
|
||||
'description': video_meta.get('description'),
|
||||
'timestamp': parse_iso8601(video_meta.get('published_at')),
|
||||
'thumbnails': [{
|
||||
'id': tn.get('name'), # this appears to be null
|
||||
'url': tn['url'],
|
||||
'width': tn.get('width'),
|
||||
'height': tn.get('height'),
|
||||
} for tn in video_meta.get('thumbnails', [])],
|
||||
'duration': video_meta.get('duration'),
|
||||
'channel': channel_title,
|
||||
'uploader': channel_title, # we chose uploader = channel name
|
||||
# TODO: uploader_url, channel_id, channel_url
|
||||
}
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
from urllib.error import HTTPError
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_str, compat_urllib_parse_unquote, compat_urllib_parse_quote
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
parse_iso8601,
|
||||
try_get,
|
||||
urljoin,
|
||||
)
|
||||
|
||||
|
||||
class NebulaIE(InfoExtractor):
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:watchnebula\.com|nebula\.app)/videos/(?P<id>[-\w]+)'
|
||||
_TESTS = [
|
||||
{
|
||||
'url': 'https://nebula.app/videos/that-time-disney-remade-beauty-and-the-beast',
|
||||
'md5': 'fe79c4df8b3aa2fea98a93d027465c7e',
|
||||
'info_dict': {
|
||||
'id': '5c271b40b13fd613090034fd',
|
||||
'ext': 'mp4',
|
||||
'title': 'That Time Disney Remade Beauty and the Beast',
|
||||
'description': 'Note: this video was originally posted on YouTube with the sponsor read included. We weren’t able to remove it without reducing video quality, so it’s presented here in its original context.',
|
||||
'upload_date': '20180731',
|
||||
'timestamp': 1533009600,
|
||||
'channel': 'Lindsay Ellis',
|
||||
'uploader': 'Lindsay Ellis',
|
||||
},
|
||||
'params': {
|
||||
'usenetrc': True,
|
||||
},
|
||||
'skip': 'All Nebula content requires authentication',
|
||||
},
|
||||
{
|
||||
'url': 'https://nebula.app/videos/the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore',
|
||||
'md5': '6d4edd14ce65720fa63aba5c583fb328',
|
||||
'info_dict': {
|
||||
'id': '5e7e78171aaf320001fbd6be',
|
||||
'ext': 'mp4',
|
||||
'title': 'Landing Craft - How The Allies Got Ashore',
|
||||
'description': r're:^In this episode we explore the unsung heroes of D-Day, the landing craft.',
|
||||
'upload_date': '20200327',
|
||||
'timestamp': 1585348140,
|
||||
'channel': 'The Logistics of D-Day',
|
||||
'uploader': 'The Logistics of D-Day',
|
||||
},
|
||||
'params': {
|
||||
'usenetrc': True,
|
||||
},
|
||||
'skip': 'All Nebula content requires authentication',
|
||||
},
|
||||
{
|
||||
'url': 'https://nebula.app/videos/money-episode-1-the-draw',
|
||||
'md5': '8c7d272910eea320f6f8e6d3084eecf5',
|
||||
'info_dict': {
|
||||
'id': '5e779ebdd157bc0001d1c75a',
|
||||
'ext': 'mp4',
|
||||
'title': 'Episode 1: The Draw',
|
||||
'description': r'contains:There’s free money on offer… if the players can all work together.',
|
||||
'upload_date': '20200323',
|
||||
'timestamp': 1584980400,
|
||||
'channel': 'Tom Scott Presents: Money',
|
||||
'uploader': 'Tom Scott Presents: Money',
|
||||
},
|
||||
'params': {
|
||||
'usenetrc': True,
|
||||
},
|
||||
'skip': 'All Nebula content requires authentication',
|
||||
},
|
||||
{
|
||||
'url': 'https://watchnebula.com/videos/money-episode-1-the-draw',
|
||||
'only_matching': True,
|
||||
},
|
||||
]
|
||||
_NETRC_MACHINE = 'watchnebula'
|
||||
|
||||
_nebula_token = None
|
||||
|
||||
def _retrieve_nebula_auth(self):
|
||||
"""
|
||||
Log in to Nebula, and returns a Nebula API token
|
||||
"""
|
||||
|
||||
username, password = self._get_login_info()
|
||||
if not (username and password):
|
||||
self.raise_login_required()
|
||||
|
||||
self.report_login()
|
||||
data = json.dumps({'email': username, 'password': password}).encode('utf8')
|
||||
response = self._download_json(
|
||||
'https://api.watchnebula.com/api/v1/auth/login/',
|
||||
data=data, fatal=False, video_id=None,
|
||||
headers={
|
||||
'content-type': 'application/json',
|
||||
# Submitting the 'sessionid' cookie always causes a 403 on auth endpoint
|
||||
'cookie': ''
|
||||
},
|
||||
note='Authenticating to Nebula with supplied credentials',
|
||||
errnote='Authentication failed or rejected')
|
||||
if not response or not response.get('key'):
|
||||
self.raise_login_required()
|
||||
|
||||
# save nebula token as cookie
|
||||
self._set_cookie(
|
||||
'nebula.app', 'nebula-auth',
|
||||
compat_urllib_parse_quote(
|
||||
json.dumps({
|
||||
"apiToken": response["key"],
|
||||
"isLoggingIn": False,
|
||||
"isLoggingOut": False,
|
||||
}, separators=(",", ":"))),
|
||||
expire_time=int(time.time()) + 86400 * 365,
|
||||
)
|
||||
|
||||
return response['key']
|
||||
|
||||
def _retrieve_zype_api_key(self, page_url, display_id):
|
||||
"""
|
||||
Retrieves the Zype API key
|
||||
"""
|
||||
|
||||
# Find the js that has the API key from the webpage and download it
|
||||
webpage = self._download_webpage(page_url, video_id=display_id)
|
||||
main_script_relpath = self._search_regex(
|
||||
r'<script[^>]*src="(?P<script_relpath>[^"]*main.[0-9a-f]*.chunk.js)"[^>]*>', webpage,
|
||||
group='script_relpath', name='script relative path', fatal=True)
|
||||
main_script_abspath = urljoin(page_url, main_script_relpath)
|
||||
main_script = self._download_webpage(main_script_abspath, video_id=display_id,
|
||||
note='Retrieving Zype API key')
|
||||
|
||||
api_key = self._search_regex(
|
||||
r'REACT_APP_ZYPE_API_KEY\s*:\s*"(?P<api_key>[\w-]*)"', main_script,
|
||||
group='api_key', name='API key', fatal=True)
|
||||
|
||||
return api_key
|
||||
|
||||
def _call_zype_api(self, path, params, video_id, api_key, note):
|
||||
"""
|
||||
A helper for making calls to the Zype API.
|
||||
"""
|
||||
query = {'api_key': api_key, 'per_page': 1}
|
||||
query.update(params)
|
||||
return self._download_json('https://api.zype.com' + path, video_id, query=query, note=note)
|
||||
|
||||
def _call_nebula_api(self, path, video_id, access_token, note):
|
||||
"""
|
||||
A helper for making calls to the Nebula API.
|
||||
"""
|
||||
return self._download_json('https://api.watchnebula.com/api/v1' + path, video_id, headers={
|
||||
'Authorization': 'Token {access_token}'.format(access_token=access_token)
|
||||
}, note=note)
|
||||
|
||||
def _fetch_zype_access_token(self, video_id):
|
||||
try:
|
||||
user_object = self._call_nebula_api('/auth/user/', video_id, self._nebula_token, note='Retrieving Zype access token')
|
||||
except ExtractorError as exc:
|
||||
# if 401, attempt credential auth and retry
|
||||
if exc.cause and isinstance(exc.cause, HTTPError) and exc.cause.code == 401:
|
||||
self._nebula_token = self._retrieve_nebula_auth()
|
||||
user_object = self._call_nebula_api('/auth/user/', video_id, self._nebula_token, note='Retrieving Zype access token')
|
||||
else:
|
||||
raise
|
||||
|
||||
access_token = try_get(user_object, lambda x: x['zype_auth_info']['access_token'], compat_str)
|
||||
if not access_token:
|
||||
if try_get(user_object, lambda x: x['is_subscribed'], bool):
|
||||
# TODO: Reimplement the same Zype token polling the Nebula frontend implements
|
||||
# see https://github.com/ytdl-org/youtube-dl/pull/24805#issuecomment-749231532
|
||||
raise ExtractorError(
|
||||
'Unable to extract Zype access token from Nebula API authentication endpoint. '
|
||||
'Open an arbitrary video in a browser with this account to generate a token',
|
||||
expected=True)
|
||||
raise ExtractorError('Unable to extract Zype access token from Nebula API authentication endpoint')
|
||||
return access_token
|
||||
|
||||
def _extract_channel_title(self, video_meta):
|
||||
# TODO: Implement the API calls giving us the channel list,
|
||||
# so that we can do the title lookup and then figure out the channel URL
|
||||
categories = video_meta.get('categories', []) if video_meta else []
|
||||
# the channel name is the value of the first category
|
||||
for category in categories:
|
||||
if category.get('value'):
|
||||
return category['value'][0]
|
||||
|
||||
def _real_initialize(self):
|
||||
# check cookie jar for valid token
|
||||
nebula_cookies = self._get_cookies('https://nebula.app')
|
||||
nebula_cookie = nebula_cookies.get('nebula-auth')
|
||||
if nebula_cookie:
|
||||
self.to_screen('Authenticating to Nebula with token from cookie jar')
|
||||
nebula_cookie_value = compat_urllib_parse_unquote(nebula_cookie.value)
|
||||
self._nebula_token = self._parse_json(nebula_cookie_value, None).get('apiToken')
|
||||
|
||||
# try to authenticate using credentials if no valid token has been found
|
||||
if not self._nebula_token:
|
||||
self._nebula_token = self._retrieve_nebula_auth()
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
api_key = self._retrieve_zype_api_key(url, display_id)
|
||||
|
||||
response = self._call_zype_api('/videos', {'friendly_title': display_id},
|
||||
display_id, api_key, note='Retrieving metadata from Zype')
|
||||
if len(response.get('response') or []) != 1:
|
||||
raise ExtractorError('Unable to find video on Zype API')
|
||||
video_meta = response['response'][0]
|
||||
|
||||
video_id = video_meta['_id']
|
||||
zype_access_token = self._fetch_zype_access_token(display_id)
|
||||
|
||||
channel_title = self._extract_channel_title(video_meta)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'display_id': display_id,
|
||||
'_type': 'url_transparent',
|
||||
'ie_key': 'Zype',
|
||||
'url': 'https://player.zype.com/embed/%s.html?access_token=%s' % (video_id, zype_access_token),
|
||||
'title': video_meta.get('title'),
|
||||
'description': video_meta.get('description'),
|
||||
'timestamp': parse_iso8601(video_meta.get('published_at')),
|
||||
'thumbnails': [{
|
||||
'id': tn.get('name'), # this appears to be null
|
||||
'url': tn['url'],
|
||||
'width': tn.get('width'),
|
||||
'height': tn.get('height'),
|
||||
} for tn in video_meta.get('thumbnails', [])],
|
||||
'duration': video_meta.get('duration'),
|
||||
'channel': channel_title,
|
||||
'uploader': channel_title, # we chose uploader = channel name
|
||||
# TODO: uploader_url, channel_id, channel_url
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import functools
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
@@ -8,8 +10,9 @@ from ..utils import (
|
||||
int_or_none,
|
||||
parse_count,
|
||||
parse_duration,
|
||||
parse_filesize,
|
||||
unified_timestamp,
|
||||
OnDemandPagedList,
|
||||
try_get,
|
||||
)
|
||||
|
||||
|
||||
@@ -88,10 +91,10 @@ class NewgroundsIE(InfoExtractor):
|
||||
webpage = self._download_webpage(url, media_id)
|
||||
|
||||
title = self._html_search_regex(
|
||||
r'<title>([^>]+)</title>', webpage, 'title')
|
||||
r'<title>(.+?)</title>', webpage, 'title')
|
||||
|
||||
media_url_string = self._search_regex(
|
||||
r'"url"\s*:\s*("[^"]+"),', webpage, 'media url', default=None, fatal=False)
|
||||
r'"url"\s*:\s*("[^"]+"),', webpage, 'media url', default=None)
|
||||
|
||||
if media_url_string:
|
||||
media_url = self._parse_json(media_url_string, media_id)
|
||||
@@ -128,20 +131,26 @@ class NewgroundsIE(InfoExtractor):
|
||||
(r'<dt>\s*Uploaded\s*</dt>\s*<dd>([^<]+</dd>\s*<dd>[^<]+)',
|
||||
r'<dt>\s*Uploaded\s*</dt>\s*<dd>([^<]+)'), webpage, 'timestamp',
|
||||
default=None))
|
||||
duration = parse_duration(self._search_regex(
|
||||
r'(?s)<dd>\s*Song\s*</dd>\s*<dd>.+?</dd>\s*<dd>([^<]+)', webpage,
|
||||
duration = parse_duration(self._html_search_regex(
|
||||
r'"duration"\s*:\s*["\']?([\d]+)["\']?,', webpage,
|
||||
'duration', default=None))
|
||||
|
||||
view_count = parse_count(self._html_search_regex(r'(?s)<dt>\s*Views\s*</dt>\s*<dd>([\d\.,]+)</dd>', webpage,
|
||||
'view_count', fatal=False, default=None))
|
||||
view_count = parse_count(self._html_search_regex(
|
||||
r'(?s)<dt>\s*Views\s*</dt>\s*<dd>([\d\.,]+)</dd>', webpage,
|
||||
'view count', default=None))
|
||||
|
||||
filesize_approx = parse_filesize(self._html_search_regex(
|
||||
r'(?s)<dd>\s*Song\s*</dd>\s*<dd>(.+?)</dd>', webpage, 'filesize',
|
||||
filesize = int_or_none(self._html_search_regex(
|
||||
r'"filesize"\s*:\s*["\']?([\d]+)["\']?,', webpage, 'filesize',
|
||||
default=None))
|
||||
if len(formats) == 1:
|
||||
formats[0]['filesize_approx'] = filesize_approx
|
||||
|
||||
if '<dd>Song' in webpage:
|
||||
video_type_description = self._html_search_regex(
|
||||
r'"description"\s*:\s*["\']?([^"\']+)["\']?,', webpage, 'filesize',
|
||||
default=None)
|
||||
|
||||
if len(formats) == 1:
|
||||
formats[0]['filesize'] = filesize
|
||||
|
||||
if video_type_description == 'Audio File':
|
||||
formats[0]['vcodec'] = 'none'
|
||||
self._check_formats(formats, media_id)
|
||||
self._sort_formats(formats)
|
||||
@@ -160,6 +169,7 @@ class NewgroundsIE(InfoExtractor):
|
||||
|
||||
|
||||
class NewgroundsPlaylistIE(InfoExtractor):
|
||||
IE_NAME = 'Newgrounds:playlist'
|
||||
_VALID_URL = r'https?://(?:www\.)?newgrounds\.com/(?:collection|[^/]+/search/[^/]+)/(?P<id>[^/?#&]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.newgrounds.com/collection/cats',
|
||||
@@ -202,7 +212,57 @@ class NewgroundsPlaylistIE(InfoExtractor):
|
||||
continue
|
||||
entries.append(
|
||||
self.url_result(
|
||||
'https://www.newgrounds.com/%s' % path,
|
||||
f'https://www.newgrounds.com/{path}',
|
||||
ie=NewgroundsIE.ie_key(), video_id=media_id))
|
||||
|
||||
return self.playlist_result(entries, playlist_id, title)
|
||||
|
||||
|
||||
class NewgroundsUserIE(InfoExtractor):
|
||||
IE_NAME = 'Newgrounds:user'
|
||||
_VALID_URL = r'https?://(?P<id>[^\.]+)\.newgrounds\.com/(?:movies|audio)/?(?:[#?]|$)'
|
||||
_TESTS = [{
|
||||
'url': 'https://burn7.newgrounds.com/audio',
|
||||
'info_dict': {
|
||||
'id': 'burn7',
|
||||
},
|
||||
'playlist_mincount': 150,
|
||||
}, {
|
||||
'url': 'https://burn7.newgrounds.com/movies',
|
||||
'info_dict': {
|
||||
'id': 'burn7',
|
||||
},
|
||||
'playlist_mincount': 2,
|
||||
}, {
|
||||
'url': 'https://brian-beaton.newgrounds.com/movies',
|
||||
'info_dict': {
|
||||
'id': 'brian-beaton',
|
||||
},
|
||||
'playlist_mincount': 10,
|
||||
}]
|
||||
_PAGE_SIZE = 30
|
||||
|
||||
def _fetch_page(self, channel_id, url, page):
|
||||
page += 1
|
||||
posts_info = self._download_json(
|
||||
f'{url}/page/{page}', channel_id,
|
||||
note=f'Downloading page {page}', headers={
|
||||
'Accept': 'application/json, text/javascript, */*; q = 0.01',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
})
|
||||
sequence = posts_info.get('sequence', [])
|
||||
for year in sequence:
|
||||
posts = try_get(posts_info, lambda x: x['years'][str(year)]['items'])
|
||||
for post in posts:
|
||||
path, media_id = self._search_regex(
|
||||
r'<a[^>]+\bhref=["\'][^"\']+((?:portal/view|audio/listen)/(\d+))[^>]+>',
|
||||
post, 'url', group=(1, 2))
|
||||
yield self.url_result(f'https://www.newgrounds.com/{path}', NewgroundsIE.ie_key(), media_id)
|
||||
|
||||
def _real_extract(self, url):
|
||||
channel_id = self._match_id(url)
|
||||
|
||||
entries = OnDemandPagedList(functools.partial(
|
||||
self._fetch_page, channel_id, url), self._PAGE_SIZE)
|
||||
|
||||
return self.playlist_result(entries, channel_id)
|
||||
|
||||
+45
-14
@@ -8,6 +8,10 @@ from ..utils import (
|
||||
int_or_none,
|
||||
float_or_none,
|
||||
smuggle_url,
|
||||
str_or_none,
|
||||
try_get,
|
||||
unified_strdate,
|
||||
unified_timestamp,
|
||||
)
|
||||
|
||||
|
||||
@@ -37,6 +41,24 @@ class NineNowIE(InfoExtractor):
|
||||
# DRM protected
|
||||
'url': 'https://www.9now.com.au/andrew-marrs-history-of-the-world/season-1/episode-1',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
# episode of series
|
||||
'url': 'https://www.9now.com.au/lego-masters/season-3/episode-3',
|
||||
'info_dict': {
|
||||
'id': '6249614030001',
|
||||
'title': 'Episode 3',
|
||||
'ext': 'mp4',
|
||||
'season_number': 3,
|
||||
'episode_number': 3,
|
||||
'description': 'In the first elimination of the competition, teams will have 10 hours to build a world inside a snow globe.',
|
||||
'uploader_id': '4460760524001',
|
||||
'timestamp': 1619002200,
|
||||
'upload_date': '20210421',
|
||||
},
|
||||
'expected_warnings': ['Ignoring subtitle tracks'],
|
||||
'params':{
|
||||
'skip_download': True,
|
||||
}
|
||||
}]
|
||||
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/4460760524001/default_default/index.html?videoId=%s'
|
||||
|
||||
@@ -59,26 +81,31 @@ class NineNowIE(InfoExtractor):
|
||||
cache = page_data.get(kind, {}).get('%sCache' % kind, {})
|
||||
if not cache:
|
||||
continue
|
||||
common_data = (cache.get(current_key) or list(cache.values())[0])[kind]
|
||||
common_data = {
|
||||
'episode': (cache.get(current_key) or list(cache.values())[0])[kind],
|
||||
'season': (cache.get(current_key) or list(cache.values())[0]).get('season', None)
|
||||
}
|
||||
break
|
||||
else:
|
||||
raise ExtractorError('Unable to find video data')
|
||||
|
||||
video_data = common_data['video']
|
||||
|
||||
brightcove_id = video_data.get('brightcoveId') or 'ref:' + video_data['referenceId']
|
||||
video_id = compat_str(video_data.get('id') or brightcove_id)
|
||||
|
||||
if not self.get_param('allow_unplayable_formats') and video_data.get('drm'):
|
||||
self.report_drm(video_id)
|
||||
|
||||
title = common_data['name']
|
||||
if not self.get_param('allow_unplayable_formats') and try_get(common_data, lambda x: x['episode']['video']['drm'], bool):
|
||||
self.report_drm(display_id)
|
||||
brightcove_id = try_get(
|
||||
common_data, lambda x: x['episode']['video']['brightcoveId'], compat_str) or 'ref:%s' % common_data['episode']['video']['referenceId']
|
||||
video_id = str_or_none(try_get(common_data, lambda x: x['episode']['video']['id'])) or brightcove_id
|
||||
|
||||
title = try_get(common_data, lambda x: x['episode']['name'], compat_str)
|
||||
season_number = try_get(common_data, lambda x: x['season']['seasonNumber'], int)
|
||||
episode_number = try_get(common_data, lambda x: x['episode']['episodeNumber'], int)
|
||||
timestamp = unified_timestamp(try_get(common_data, lambda x: x['episode']['airDate'], compat_str))
|
||||
release_date = unified_strdate(try_get(common_data, lambda x: x['episode']['availability'], compat_str))
|
||||
thumbnails_data = try_get(common_data, lambda x: x['episode']['image']['sizes'], dict) or {}
|
||||
thumbnails = [{
|
||||
'id': thumbnail_id,
|
||||
'url': thumbnail_url,
|
||||
'width': int_or_none(thumbnail_id[1:])
|
||||
} for thumbnail_id, thumbnail_url in common_data.get('image', {}).get('sizes', {}).items()]
|
||||
'width': int_or_none(thumbnail_id[1:]),
|
||||
} for thumbnail_id, thumbnail_url in thumbnails_data.items()]
|
||||
|
||||
return {
|
||||
'_type': 'url_transparent',
|
||||
@@ -87,8 +114,12 @@ class NineNowIE(InfoExtractor):
|
||||
{'geo_countries': self._GEO_COUNTRIES}),
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'description': common_data.get('description'),
|
||||
'duration': float_or_none(video_data.get('duration'), 1000),
|
||||
'description': try_get(common_data, lambda x: x['episode']['description'], compat_str),
|
||||
'duration': float_or_none(try_get(common_data, lambda x: x['episode']['video']['duration'], float), 1000),
|
||||
'thumbnails': thumbnails,
|
||||
'ie_key': 'BrightcoveNew',
|
||||
'season_number': season_number,
|
||||
'episode_number': episode_number,
|
||||
'timestamp': timestamp,
|
||||
'release_date': release_date,
|
||||
}
|
||||
|
||||
+44
-42
@@ -1,71 +1,73 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
parse_duration,
|
||||
int_or_none,
|
||||
try_get,
|
||||
)
|
||||
|
||||
|
||||
class NuvidIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www|m)\.nuvid\.com/video/(?P<id>[0-9]+)'
|
||||
_TEST = {
|
||||
'url': 'http://m.nuvid.com/video/1310741/',
|
||||
'md5': 'eab207b7ac4fccfb4e23c86201f11277',
|
||||
_TESTS = [{
|
||||
'url': 'https://www.nuvid.com/video/6513023/italian-babe',
|
||||
'md5': '772d2f8288f3d3c5c45f7a41761c7844',
|
||||
'info_dict': {
|
||||
'id': '1310741',
|
||||
'id': '6513023',
|
||||
'ext': 'mp4',
|
||||
'title': 'Horny babes show their awesome bodeis and',
|
||||
'duration': 129,
|
||||
'title': 'italian babe',
|
||||
'duration': 321.0,
|
||||
'age_limit': 18,
|
||||
}
|
||||
}
|
||||
}, {
|
||||
'url': 'https://m.nuvid.com/video/6523263',
|
||||
'info_dict': {
|
||||
'id': '6523263',
|
||||
'ext': 'mp4',
|
||||
'age_limit': 18,
|
||||
'title': 'Slut brunette college student anal dorm',
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
|
||||
page_url = 'http://m.nuvid.com/video/%s' % video_id
|
||||
webpage = self._download_webpage(
|
||||
page_url, video_id, 'Downloading video page')
|
||||
# When dwnld_speed exists and has a value larger than the MP4 file's
|
||||
# bitrate, Nuvid returns the MP4 URL
|
||||
# It's unit is 100bytes/millisecond, see mobile-nuvid-min.js for the algorithm
|
||||
self._set_cookie('nuvid.com', 'dwnld_speed', '10.0')
|
||||
mp4_webpage = self._download_webpage(
|
||||
page_url, video_id, 'Downloading video page for MP4 format')
|
||||
qualities = {
|
||||
'lq': '360p',
|
||||
'hq': '720p',
|
||||
}
|
||||
|
||||
html5_video_re = r'(?s)<(?:video|audio)[^<]*(?:>.*?<source[^>]*)?\s+src=["\'](.*?)["\']',
|
||||
video_url = self._html_search_regex(html5_video_re, webpage, video_id)
|
||||
mp4_video_url = self._html_search_regex(html5_video_re, mp4_webpage, video_id)
|
||||
formats = [{
|
||||
'url': video_url,
|
||||
}]
|
||||
if mp4_video_url != video_url:
|
||||
formats.append({
|
||||
'url': mp4_video_url,
|
||||
json_url = f'https://www.nuvid.com/player_config_json/?vid={video_id}&aid=0&domain_id=0&embed=0&check_speed=0'
|
||||
video_data = self._download_json(
|
||||
json_url, video_id, headers={
|
||||
'Accept': 'application/json, text/javascript, */*; q = 0.01',
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||
})
|
||||
|
||||
title = self._html_search_regex(
|
||||
[r'<span title="([^"]+)">',
|
||||
r'<div class="thumb-holder video">\s*<h5[^>]*>([^<]+)</h5>',
|
||||
r'<span[^>]+class="title_thumb">([^<]+)</span>'], webpage, 'title').strip()
|
||||
thumbnails = [
|
||||
{
|
||||
'url': thumb_url,
|
||||
} for thumb_url in re.findall(r'<img src="([^"]+)" alt="" />', webpage)
|
||||
]
|
||||
thumbnail = thumbnails[0]['url'] if thumbnails else None
|
||||
duration = parse_duration(self._html_search_regex(
|
||||
[r'<i class="fa fa-clock-o"></i>\s*(\d{2}:\d{2})',
|
||||
r'<span[^>]+class="view_time">([^<]+)</span>'], webpage, 'duration', fatal=False))
|
||||
formats = [{
|
||||
'url': source,
|
||||
'format_id': qualities.get(quality),
|
||||
'height': int_or_none(qualities.get(quality)[:-1]),
|
||||
} for quality, source in video_data.get('files').items() if source]
|
||||
|
||||
self._check_formats(formats, video_id)
|
||||
self._sort_formats(formats)
|
||||
|
||||
title = video_data.get('title')
|
||||
thumbnail_base_url = try_get(video_data, lambda x: x['thumbs']['url'])
|
||||
thumbnail_extension = try_get(video_data, lambda x: x['thumbs']['extension'])
|
||||
thumbnail_id = self._search_regex(
|
||||
r'/media/videos/tmb/6523263/preview/(/d+)' + thumbnail_extension, video_data.get('poster', ''), 'thumbnail id', default=19)
|
||||
thumbnail = f'{thumbnail_base_url}player/{thumbnail_id}{thumbnail_extension}'
|
||||
duration = parse_duration(video_data.get('duration') or video_data.get('duration_format'))
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'formats': formats,
|
||||
'title': title,
|
||||
'thumbnails': thumbnails,
|
||||
'thumbnail': thumbnail,
|
||||
'duration': duration,
|
||||
'age_limit': 18,
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .brightcove import BrightcoveNewIE
|
||||
from .common import InfoExtractor
|
||||
|
||||
from ..compat import compat_str
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
traverse_obj
|
||||
)
|
||||
|
||||
|
||||
class NZHeraldIE(InfoExtractor):
|
||||
IE_NAME = 'nzherald'
|
||||
_VALID_URL = r'https?://(?:www\.)?nzherald\.co\.nz/[\w\/-]+\/(?P<id>[A-Z0-9]+)'
|
||||
_TESTS = [
|
||||
{
|
||||
'url': 'https://www.nzherald.co.nz/nz/weather-heavy-rain-gales-across-nz-most-days-this-week/PTG7QWY4E2225YHZ5NAIRBTYTQ/',
|
||||
'info_dict': {
|
||||
'id': '6271084466001',
|
||||
'ext': 'mp4',
|
||||
'title': 'MetService severe weather warning: September 6th - 7th',
|
||||
'timestamp': 1630891576,
|
||||
'upload_date': '20210906',
|
||||
'uploader_id': '1308227299001',
|
||||
'description': 'md5:db6ca335a22e2cdf37ab9d2bcda52902'
|
||||
}
|
||||
|
||||
}, {
|
||||
# Webpage has brightcove embed player url
|
||||
'url': 'https://www.nzherald.co.nz/travel/pencarrow-coastal-trail/HDVTPJEPP46HJ2UEMK4EGD2DFI/',
|
||||
'info_dict': {
|
||||
'id': '6261791733001',
|
||||
'ext': 'mp4',
|
||||
'title': 'Pencarrow Coastal Trail',
|
||||
'timestamp': 1625102897,
|
||||
'upload_date': '20210701',
|
||||
'uploader_id': '1308227299001',
|
||||
'description': 'md5:d361aaa0c6498f7ac1bc4fc0a0aec1e4'
|
||||
}
|
||||
|
||||
}, {
|
||||
# two video embeds of the same video
|
||||
'url': 'https://www.nzherald.co.nz/nz/truck-driver-captured-cutting-off-motorist-on-state-highway-1-in-canterbury/FIHNJB7PLLPHWQPK4S7ZBDUC4I/',
|
||||
'info_dict': {
|
||||
'id': '6251114530001',
|
||||
'ext': 'mp4',
|
||||
'title': 'Truck travelling north from Rakaia runs car off road',
|
||||
'timestamp': 1619730509,
|
||||
'upload_date': '20210429',
|
||||
'uploader_id': '1308227299001',
|
||||
'description': 'md5:4cae7dfb7613ac4c73b9e73a75c6b5d7'
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.nzherald.co.nz/kahu/kaupapa-companies-my-taiao-supporting-maori-in-study-and-business/PQBO2J25WCG77VGRX7W7BVYEAI/',
|
||||
'only_matching': True
|
||||
}, {
|
||||
'url': 'https://nzherald.co.nz/the-country/video/focus-nzs-first-mass-covid-19-vaccination-event/N5I7IL3BRFLZSD33TLDLYJDGK4/',
|
||||
'only_matching': True
|
||||
}, {
|
||||
'url': 'https://www.nzherald.co.nz/the-vision-is-clear/news/tvic-damian-roper-planting-trees-an-addiction/AN2AAEPNRK5VLISDWQAJZB6ATQ',
|
||||
'only_matching': True
|
||||
}
|
||||
]
|
||||
|
||||
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/1308227299001/S1BXZn8t_default/index.html?videoId=%s'
|
||||
|
||||
def _extract_bc_embed_url(self, webpage):
|
||||
"""The initial webpage may include the brightcove player embed url"""
|
||||
bc_url = BrightcoveNewIE._extract_url(self, webpage)
|
||||
return bc_url or self._search_regex(
|
||||
r'(?:embedUrl)\"\s*:\s*\"(?P<embed_url>%s)' % BrightcoveNewIE._VALID_URL,
|
||||
webpage, 'embed url', default=None, group='embed_url')
|
||||
|
||||
def _real_extract(self, url):
|
||||
article_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, article_id)
|
||||
bc_url = self._extract_bc_embed_url(webpage)
|
||||
|
||||
if not bc_url:
|
||||
fusion_metadata = self._parse_json(
|
||||
self._search_regex(r'Fusion\.globalContent\s*=\s*({.+?})\s*;', webpage, 'fusion metadata'), article_id)
|
||||
|
||||
video_metadata = fusion_metadata.get('video')
|
||||
bc_video_id = traverse_obj(
|
||||
video_metadata or fusion_metadata, # fusion metadata is the video metadata for video-only pages
|
||||
'brightcoveId', ('content_elements', ..., 'referent', 'id'),
|
||||
get_all=False, expected_type=compat_str)
|
||||
|
||||
if not bc_video_id:
|
||||
if isinstance(video_metadata, dict) and len(video_metadata) == 0:
|
||||
raise ExtractorError('This article does not have a video.', expected=True)
|
||||
else:
|
||||
raise ExtractorError('Failed to extract brightcove video id')
|
||||
bc_url = self.BRIGHTCOVE_URL_TEMPLATE % bc_video_id
|
||||
|
||||
return self.url_result(bc_url, 'BrightcoveNew')
|
||||
@@ -0,0 +1,56 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import unified_strdate
|
||||
|
||||
|
||||
class OlympicsReplayIE(InfoExtractor):
|
||||
_VALID_URL = r'(?:https?://)(?:www\.)?olympics\.com/tokyo-2020/(?:[a-z]{2}/)?replay/(?P<id>[^/#&?]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://olympics.com/tokyo-2020/en/replay/300622eb-abc0-43ea-b03b-c5f2d429ec7b/jumping-team-qualifier',
|
||||
'info_dict': {
|
||||
'id': '300622eb-abc0-43ea-b03b-c5f2d429ec7b',
|
||||
'ext': 'mp4',
|
||||
'title': 'Jumping Team Qualifier',
|
||||
'release_date': '20210806',
|
||||
'upload_date': '20210713',
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://olympics.com/tokyo-2020/en/replay/bd242924-4b22-49a5-a846-f1d4c809250d/mens-bronze-medal-match-hun-esp',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
id = self._match_id(url)
|
||||
# The parameters are hardcoded in the webpage, it's not necessary to download the webpage just for these parameters.
|
||||
# If in downloading webpage serves other functions aswell, then extract these parameters from it.
|
||||
token_url = 'https://appovptok.ovpobs.tv/api/identity/app/token?api_key=OTk5NDcxOjpvY3N3LWFwaXVzZXI%3D&api_secret=ODY4ODM2MjE3ODMwYmVjNTAxMWZlMDJiMTYxZmY0MjFiMjMwMjllMjJmNDA1YWRiYzA5ODcxYTZjZTljZDkxOTo6NTM2NWIzNjRlMTM1ZmI2YWNjNmYzMGMzOGM3NzZhZTY%3D'
|
||||
token = self._download_webpage(token_url, id)
|
||||
headers = {'x-obs-app-token': token}
|
||||
data_json = self._download_json(f'https://appocswtok.ovpobs.tv/api/schedule-sessions/{id}?include=stream',
|
||||
id, headers=headers)
|
||||
meta_data = data_json['data']['attributes']
|
||||
for t_dict in data_json['included']:
|
||||
if t_dict.get('type') == 'Stream':
|
||||
stream_data = t_dict['attributes']
|
||||
m3u8_url = self._download_json(
|
||||
'https://meteringtok.ovpobs.tv/api/playback-sessions', id, headers=headers, query={
|
||||
'alias': stream_data['alias'],
|
||||
'stream': stream_data['stream'],
|
||||
'type': 'vod'
|
||||
})['data']['attributes']['url']
|
||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, id)
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': id,
|
||||
'title': meta_data['title'],
|
||||
'release_date': unified_strdate(meta_data.get('start') or meta_data.get('broadcastPublished')),
|
||||
'upload_date': unified_strdate(meta_data.get('publishedAt')),
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
}
|
||||
+10
-21
@@ -600,6 +600,7 @@ class PBSIE(InfoExtractor):
|
||||
|
||||
formats = []
|
||||
http_url = None
|
||||
hls_subs = {}
|
||||
for num, redirect in enumerate(redirects):
|
||||
redirect_id = redirect.get('eeid')
|
||||
|
||||
@@ -622,8 +623,9 @@ class PBSIE(InfoExtractor):
|
||||
continue
|
||||
|
||||
if determine_ext(format_url) == 'm3u8':
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
format_url, display_id, 'mp4', m3u8_id='hls', fatal=False))
|
||||
hls_formats, hls_subs = self._extract_m3u8_formats_and_subtitles(
|
||||
format_url, display_id, 'mp4', m3u8_id='hls', fatal=False)
|
||||
formats.extend(hls_formats)
|
||||
else:
|
||||
formats.append({
|
||||
'url': format_url,
|
||||
@@ -666,25 +668,12 @@ class PBSIE(InfoExtractor):
|
||||
age_limit = US_RATINGS.get(rating_str)
|
||||
|
||||
subtitles = {}
|
||||
closed_captions_url = info.get('closed_captions_url')
|
||||
if closed_captions_url:
|
||||
subtitles['en'] = [{
|
||||
'ext': 'ttml',
|
||||
'url': closed_captions_url,
|
||||
}]
|
||||
mobj = re.search(r'/(\d+)_Encoded\.dfxp', closed_captions_url)
|
||||
if mobj:
|
||||
ttml_caption_suffix, ttml_caption_id = mobj.group(0, 1)
|
||||
ttml_caption_id = int(ttml_caption_id)
|
||||
subtitles['en'].extend([{
|
||||
'url': closed_captions_url.replace(
|
||||
ttml_caption_suffix, '/%d_Encoded.srt' % (ttml_caption_id + 1)),
|
||||
'ext': 'srt',
|
||||
}, {
|
||||
'url': closed_captions_url.replace(
|
||||
ttml_caption_suffix, '/%d_Encoded.vtt' % (ttml_caption_id + 2)),
|
||||
'ext': 'vtt',
|
||||
}])
|
||||
captions = info.get('cc') or {}
|
||||
for caption_url in captions.values():
|
||||
subtitles.setdefault('en', []).append({
|
||||
'url': caption_url
|
||||
})
|
||||
subtitles = self._merge_subtitles(subtitles, hls_subs)
|
||||
|
||||
# info['title'] is often incomplete (e.g. 'Full Episode', 'Episode 5', etc)
|
||||
# Try turning it to 'program - title' naming scheme if possible
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import functools
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
@@ -13,12 +14,644 @@ from ..utils import (
|
||||
unified_timestamp,
|
||||
url_or_none,
|
||||
urljoin,
|
||||
OnDemandPagedList,
|
||||
)
|
||||
|
||||
|
||||
class PeerTubeIE(InfoExtractor):
|
||||
_INSTANCES_RE = r'''(?:
|
||||
# Taken from https://instances.joinpeertube.org/instances
|
||||
40two\.tube|
|
||||
a\.metube\.ch|
|
||||
advtv\.ml|
|
||||
algorithmic\.tv|
|
||||
alimulama\.com|
|
||||
arcana\.fun|
|
||||
archive\.vidicon\.org|
|
||||
artefac-paris\.tv|
|
||||
auf1\.eu|
|
||||
battlepenguin\.video|
|
||||
beertube\.epgn\.ch|
|
||||
befree\.nohost\.me|
|
||||
bideoak\.argia\.eus|
|
||||
birkeundnymphe\.de|
|
||||
bitcointv\.com|
|
||||
cattube\.org|
|
||||
clap\.nerv-project\.eu|
|
||||
climatejustice\.video|
|
||||
comf\.tube|
|
||||
conspiracydistillery\.com|
|
||||
darkvapor\.nohost\.me|
|
||||
daschauher\.aksel\.rocks|
|
||||
digitalcourage\.video|
|
||||
dreiecksnebel\.alex-detsch\.de|
|
||||
eduvid\.org|
|
||||
evangelisch\.video|
|
||||
exo\.tube|
|
||||
fair\.tube|
|
||||
fediverse\.tv|
|
||||
film\.k-prod\.fr|
|
||||
flim\.txmn\.tk|
|
||||
fotogramas\.politicaconciencia\.org|
|
||||
ftsi\.ru|
|
||||
gary\.vger\.cloud|
|
||||
graeber\.video|
|
||||
greatview\.video|
|
||||
grypstube\.uni-greifswald\.de|
|
||||
highvoltage\.tv|
|
||||
hpstube\.fr|
|
||||
htp\.live|
|
||||
hyperreal\.tube|
|
||||
juggling\.digital|
|
||||
kino\.kompot\.si|
|
||||
kino\.schuerz\.at|
|
||||
kinowolnosc\.pl|
|
||||
kirche\.peertube-host\.de|
|
||||
kodcast\.com|
|
||||
kolektiva\.media|
|
||||
kraut\.zone|
|
||||
kumi\.tube|
|
||||
lastbreach\.tv|
|
||||
lepetitmayennais\.fr\.nf|
|
||||
lexx\.impa\.me|
|
||||
libertynode\.tv|
|
||||
libra\.syntazia\.org|
|
||||
libremedia\.video|
|
||||
live\.libratoi\.org|
|
||||
live\.nanao\.moe|
|
||||
live\.toobnix\.org|
|
||||
livegram\.net|
|
||||
lolitube\.freedomchan\.moe|
|
||||
lucarne\.balsamine\.be|
|
||||
maindreieck-tv\.de|
|
||||
mani\.tube|
|
||||
manicphase\.me|
|
||||
media\.gzevd\.de|
|
||||
media\.inno3\.cricket|
|
||||
media\.kaitaia\.life|
|
||||
media\.krashboyz\.org|
|
||||
media\.over-world\.org|
|
||||
media\.skewed\.de|
|
||||
media\.undeadnetwork\.de|
|
||||
medias\.pingbase\.net|
|
||||
melsungen\.peertube-host\.de|
|
||||
mirametube\.fr|
|
||||
mojotube\.net|
|
||||
monplaisirtube\.ddns\.net|
|
||||
mountaintown\.video|
|
||||
my\.bunny\.cafe|
|
||||
myfreetube\.de|
|
||||
mytube\.kn-cloud\.de|
|
||||
mytube\.madzel\.de|
|
||||
myworkoutarenapeertube\.cf|
|
||||
nanawel-peertube\.dyndns\.org|
|
||||
nastub\.cz|
|
||||
offenes\.tv|
|
||||
orgdup\.media|
|
||||
ovaltube\.codinglab\.ch|
|
||||
p2ptv\.ru|
|
||||
p\.eertu\.be|
|
||||
p\.lu|
|
||||
peer\.azurs\.fr|
|
||||
peertube1\.zeteo\.me|
|
||||
peertube\.020\.pl|
|
||||
peertube\.0x5e\.eu|
|
||||
peertube\.alpharius\.io|
|
||||
peertube\.am-networks\.fr|
|
||||
peertube\.anduin\.net|
|
||||
peertube\.anzui\.dev|
|
||||
peertube\.arbleizez\.bzh|
|
||||
peertube\.art3mis\.de|
|
||||
peertube\.atilla\.org|
|
||||
peertube\.atsuchan\.page|
|
||||
peertube\.aukfood\.net|
|
||||
peertube\.aventer\.biz|
|
||||
peertube\.b38\.rural-it\.org|
|
||||
peertube\.beeldengeluid\.nl|
|
||||
peertube\.be|
|
||||
peertube\.bgzashtita\.es|
|
||||
peertube\.bitsandlinux\.com|
|
||||
peertube\.biz|
|
||||
peertube\.boba\.best|
|
||||
peertube\.br0\.fr|
|
||||
peertube\.bridaahost\.ynh\.fr|
|
||||
peertube\.bubbletea\.dev|
|
||||
peertube\.bubuit\.net|
|
||||
peertube\.cabaal\.net|
|
||||
peertube\.cats-home\.net|
|
||||
peertube\.chemnitz\.freifunk\.net|
|
||||
peertube\.chevro\.fr|
|
||||
peertube\.chrisspiegl\.com|
|
||||
peertube\.chtisurel\.net|
|
||||
peertube\.cipherbliss\.com|
|
||||
peertube\.cloud\.sans\.pub|
|
||||
peertube\.cpge-brizeux\.fr|
|
||||
peertube\.ctseuro\.com|
|
||||
peertube\.cuatrolibertades\.org|
|
||||
peertube\.cybercirujas\.club|
|
||||
peertube\.cythin\.com|
|
||||
peertube\.davigge\.com|
|
||||
peertube\.dc\.pini\.fr|
|
||||
peertube\.debian\.social|
|
||||
peertube\.demonix\.fr|
|
||||
peertube\.designersethiques\.org|
|
||||
peertube\.desmu\.fr|
|
||||
peertube\.devloprog\.org|
|
||||
peertube\.devol\.it|
|
||||
peertube\.dtmf\.ca|
|
||||
peertube\.ecologie\.bzh|
|
||||
peertube\.eu\.org|
|
||||
peertube\.european-pirates\.eu|
|
||||
peertube\.euskarabildua\.eus|
|
||||
peertube\.fenarinarsa\.com|
|
||||
peertube\.fomin\.site|
|
||||
peertube\.forsud\.be|
|
||||
peertube\.francoispelletier\.org|
|
||||
peertube\.freenet\.ru|
|
||||
peertube\.freetalklive\.com|
|
||||
peertube\.functional\.cafe|
|
||||
peertube\.gardeludwig\.fr|
|
||||
peertube\.gargantia\.fr|
|
||||
peertube\.gcfamily\.fr|
|
||||
peertube\.genma\.fr|
|
||||
peertube\.get-racing\.de|
|
||||
peertube\.gidikroon\.eu|
|
||||
peertube\.gruezishop\.ch|
|
||||
peertube\.habets\.house|
|
||||
peertube\.hackerfraternity\.org|
|
||||
peertube\.ichigo\.everydayimshuflin\.com|
|
||||
peertube\.ignifi\.me|
|
||||
peertube\.inapurna\.org|
|
||||
peertube\.informaction\.info|
|
||||
peertube\.interhop\.org|
|
||||
peertube\.iselfhost\.com|
|
||||
peertube\.it|
|
||||
peertube\.jensdiemer\.de|
|
||||
peertube\.joffreyverd\.fr|
|
||||
peertube\.kalua\.im|
|
||||
peertube\.kathryl\.fr|
|
||||
peertube\.keazilla\.net|
|
||||
peertube\.klaewyss\.fr|
|
||||
peertube\.kodcast\.com|
|
||||
peertube\.kx\.studio|
|
||||
peertube\.lagvoid\.com|
|
||||
peertube\.lavallee\.tech|
|
||||
peertube\.le5emeaxe\.fr|
|
||||
peertube\.lestutosdeprocessus\.fr|
|
||||
peertube\.librenet\.co\.za|
|
||||
peertube\.logilab\.fr|
|
||||
peertube\.louisematic\.site|
|
||||
peertube\.luckow\.org|
|
||||
peertube\.luga\.at|
|
||||
peertube\.lyceeconnecte\.fr|
|
||||
peertube\.manalejandro\.com|
|
||||
peertube\.marud\.fr|
|
||||
peertube\.mattone\.net|
|
||||
peertube\.maxweiss\.io|
|
||||
peertube\.monlycee\.net|
|
||||
peertube\.mxinfo\.fr|
|
||||
peertube\.myrasp\.eu|
|
||||
peertube\.nebelcloud\.de|
|
||||
peertube\.netzbegruenung\.de|
|
||||
peertube\.newsocial\.tech|
|
||||
peertube\.nicolastissot\.fr|
|
||||
peertube\.nz|
|
||||
peertube\.offerman\.com|
|
||||
peertube\.opencloud\.lu|
|
||||
peertube\.orthus\.link|
|
||||
peertube\.patapouf\.xyz|
|
||||
peertube\.pi2\.dev|
|
||||
peertube\.plataformess\.org|
|
||||
peertube\.pl|
|
||||
peertube\.portaesgnos\.org|
|
||||
peertube\.r2\.enst\.fr|
|
||||
peertube\.r5c3\.fr|
|
||||
peertube\.radres\.xyz|
|
||||
peertube\.red|
|
||||
peertube\.robonomics\.network|
|
||||
peertube\.rtnkv\.cloud|
|
||||
peertube\.runfox\.tk|
|
||||
peertube\.satoshishop\.de|
|
||||
peertube\.scic-tetris\.org|
|
||||
peertube\.securitymadein\.lu|
|
||||
peertube\.semweb\.pro|
|
||||
peertube\.social\.my-wan\.de|
|
||||
peertube\.soykaf\.org|
|
||||
peertube\.stefofficiel\.me|
|
||||
peertube\.stream|
|
||||
peertube\.su|
|
||||
peertube\.swrs\.net|
|
||||
peertube\.takeko\.cyou|
|
||||
peertube\.tangentfox\.com|
|
||||
peertube\.taxinachtegel\.de|
|
||||
peertube\.thenewoil\.xyz|
|
||||
peertube\.ti-fr\.com|
|
||||
peertube\.tiennot\.net|
|
||||
peertube\.troback\.com|
|
||||
peertube\.tspu\.edu\.ru|
|
||||
peertube\.tux\.ovh|
|
||||
peertube\.tv|
|
||||
peertube\.tweb\.tv|
|
||||
peertube\.ucy\.de|
|
||||
peertube\.underworld\.fr|
|
||||
peertube\.us\.to|
|
||||
peertube\.ventresmous\.fr|
|
||||
peertube\.vlaki\.cz|
|
||||
peertube\.w\.utnw\.de|
|
||||
peertube\.westring\.digital|
|
||||
peertube\.xwiki\.com|
|
||||
peertube\.zoz-serv\.org|
|
||||
peervideo\.ru|
|
||||
periscope\.numenaute\.org|
|
||||
perron-tube\.de|
|
||||
petitlutinartube\.fr|
|
||||
phijkchu\.com|
|
||||
pierre\.tube|
|
||||
piraten\.space|
|
||||
play\.rosano\.ca|
|
||||
player\.ojamajo\.moe|
|
||||
plextube\.nl|
|
||||
pocketnetpeertube1\.nohost\.me|
|
||||
pocketnetpeertube3\.nohost\.me|
|
||||
pocketnetpeertube4\.nohost\.me|
|
||||
pocketnetpeertube5\.nohost\.me|
|
||||
pocketnetpeertube6\.nohost\.me|
|
||||
pt\.24-7\.ro|
|
||||
pt\.apathy\.top|
|
||||
pt\.diaspodon\.fr|
|
||||
pt\.fedi\.tech|
|
||||
pt\.maciej\.website|
|
||||
ptb\.lunarviews\.net|
|
||||
ptmir1\.inter21\.net|
|
||||
ptmir2\.inter21\.net|
|
||||
ptmir3\.inter21\.net|
|
||||
ptmir4\.inter21\.net|
|
||||
ptmir5\.inter21\.net|
|
||||
ptube\.horsentiers\.fr|
|
||||
ptube\.xmanifesto\.club|
|
||||
queermotion\.org|
|
||||
re-wizja\.re-medium\.com|
|
||||
regarder\.sans\.pub|
|
||||
ruraletv\.ovh|
|
||||
s1\.gegenstimme\.tv|
|
||||
s2\.veezee\.tube|
|
||||
sdmtube\.fr|
|
||||
sender-fm\.veezee\.tube|
|
||||
serv1\.wiki-tube\.de|
|
||||
serv3\.wiki-tube\.de|
|
||||
sickstream\.net|
|
||||
sleepy\.tube|
|
||||
sovran\.video|
|
||||
spectra\.video|
|
||||
stream\.elven\.pw|
|
||||
stream\.k-prod\.fr|
|
||||
stream\.shahab\.nohost\.me|
|
||||
streamsource\.video|
|
||||
studios\.racer159\.com|
|
||||
testtube\.florimond\.eu|
|
||||
tgi\.hosted\.spacebear\.ee|
|
||||
thaitube\.in\.th|
|
||||
the\.jokertv\.eu|
|
||||
theater\.ethernia\.net|
|
||||
thecool\.tube|
|
||||
tilvids\.com|
|
||||
toob\.bub\.org|
|
||||
tpaw\.video|
|
||||
truetube\.media|
|
||||
tuba\.lhub\.pl|
|
||||
tube-aix-marseille\.beta\.education\.fr|
|
||||
tube-amiens\.beta\.education\.fr|
|
||||
tube-besancon\.beta\.education\.fr|
|
||||
tube-bordeaux\.beta\.education\.fr|
|
||||
tube-clermont-ferrand\.beta\.education\.fr|
|
||||
tube-corse\.beta\.education\.fr|
|
||||
tube-creteil\.beta\.education\.fr|
|
||||
tube-dijon\.beta\.education\.fr|
|
||||
tube-education\.beta\.education\.fr|
|
||||
tube-grenoble\.beta\.education\.fr|
|
||||
tube-lille\.beta\.education\.fr|
|
||||
tube-limoges\.beta\.education\.fr|
|
||||
tube-montpellier\.beta\.education\.fr|
|
||||
tube-nancy\.beta\.education\.fr|
|
||||
tube-nantes\.beta\.education\.fr|
|
||||
tube-nice\.beta\.education\.fr|
|
||||
tube-normandie\.beta\.education\.fr|
|
||||
tube-orleans-tours\.beta\.education\.fr|
|
||||
tube-outremer\.beta\.education\.fr|
|
||||
tube-paris\.beta\.education\.fr|
|
||||
tube-poitiers\.beta\.education\.fr|
|
||||
tube-reims\.beta\.education\.fr|
|
||||
tube-rennes\.beta\.education\.fr|
|
||||
tube-strasbourg\.beta\.education\.fr|
|
||||
tube-toulouse\.beta\.education\.fr|
|
||||
tube-versailles\.beta\.education\.fr|
|
||||
tube1\.it\.tuwien\.ac\.at|
|
||||
tube\.abolivier\.bzh|
|
||||
tube\.ac-amiens\.fr|
|
||||
tube\.aerztefueraufklaerung\.de|
|
||||
tube\.alexx\.ml|
|
||||
tube\.amic37\.fr|
|
||||
tube\.anufrij\.de|
|
||||
tube\.apolut\.net|
|
||||
tube\.arkhalabs\.io|
|
||||
tube\.arthack\.nz|
|
||||
tube\.as211696\.net|
|
||||
tube\.avensio\.de|
|
||||
tube\.azbyka\.ru|
|
||||
tube\.azkware\.net|
|
||||
tube\.bachaner\.fr|
|
||||
tube\.bmesh\.org|
|
||||
tube\.borked\.host|
|
||||
tube\.bstly\.de|
|
||||
tube\.chaoszone\.tv|
|
||||
tube\.chatelet\.ovh|
|
||||
tube\.cloud-libre\.eu|
|
||||
tube\.cms\.garden|
|
||||
tube\.cowfee\.moe|
|
||||
tube\.cryptography\.dog|
|
||||
tube\.darknight-coffee\.org|
|
||||
tube\.dev\.lhub\.pl|
|
||||
tube\.distrilab\.fr|
|
||||
tube\.dsocialize\.net|
|
||||
tube\.ebin\.club|
|
||||
tube\.fdn\.fr|
|
||||
tube\.florimond\.eu|
|
||||
tube\.foxarmy\.ml|
|
||||
tube\.foxden\.party|
|
||||
tube\.frischesicht\.de|
|
||||
tube\.futuretic\.fr|
|
||||
tube\.gnous\.eu|
|
||||
tube\.grap\.coop|
|
||||
tube\.graz\.social|
|
||||
tube\.grin\.hu|
|
||||
tube\.hackerscop\.org|
|
||||
tube\.hordearii\.fr|
|
||||
tube\.jeena\.net|
|
||||
tube\.kai-stuht\.com|
|
||||
tube\.kockatoo\.org|
|
||||
tube\.kotur\.org|
|
||||
tube\.lacaveatonton\.ovh|
|
||||
tube\.linkse\.media|
|
||||
tube\.lokad\.com|
|
||||
tube\.lucie-philou\.com|
|
||||
tube\.melonbread\.xyz|
|
||||
tube\.mfraters\.net|
|
||||
tube\.motuhake\.xyz|
|
||||
tube\.mrbesen\.de|
|
||||
tube\.nah\.re|
|
||||
tube\.nchoco\.net|
|
||||
tube\.novg\.net|
|
||||
tube\.nox-rhea\.org|
|
||||
tube\.nuagelibre\.fr|
|
||||
tube\.nx12\.net|
|
||||
tube\.octaplex\.net|
|
||||
tube\.odat\.xyz|
|
||||
tube\.oisux\.org|
|
||||
tube\.opportunis\.me|
|
||||
tube\.org\.il|
|
||||
tube\.ortion\.xyz|
|
||||
tube\.others\.social|
|
||||
tube\.picasoft\.net|
|
||||
tube\.plomlompom\.com|
|
||||
tube\.pmj\.rocks|
|
||||
tube\.portes-imaginaire\.org|
|
||||
tube\.pyngu\.com|
|
||||
tube\.rebellion\.global|
|
||||
tube\.rhythms-of-resistance\.org|
|
||||
tube\.rita\.moe|
|
||||
tube\.rsi\.cnr\.it|
|
||||
tube\.s1gm4\.eu|
|
||||
tube\.saumon\.io|
|
||||
tube\.schleuss\.online|
|
||||
tube\.schule\.social|
|
||||
tube\.seditio\.fr|
|
||||
tube\.shanti\.cafe|
|
||||
tube\.shela\.nu|
|
||||
tube\.skrep\.in|
|
||||
tube\.sp-codes\.de|
|
||||
tube\.sp4ke\.com|
|
||||
tube\.superseriousbusiness\.org|
|
||||
tube\.systest\.eu|
|
||||
tube\.tappret\.fr|
|
||||
tube\.tardis\.world|
|
||||
tube\.toontoet\.nl|
|
||||
tube\.tpshd\.de|
|
||||
tube\.troopers\.agency|
|
||||
tube\.tylerdavis\.xyz|
|
||||
tube\.undernet\.uy|
|
||||
tube\.vigilian-consulting\.nl|
|
||||
tube\.vraphim\.com|
|
||||
tube\.wehost\.lgbt|
|
||||
tube\.wien\.rocks|
|
||||
tube\.wolfe\.casa|
|
||||
tube\.xd0\.de|
|
||||
tube\.xy-space\.de|
|
||||
tube\.yapbreak\.fr|
|
||||
tubedu\.org|
|
||||
tubes\.jodh\.us|
|
||||
tuktube\.com|
|
||||
turkum\.me|
|
||||
tututu\.tube|
|
||||
tuvideo\.encanarias\.info|
|
||||
tv1\.cocu\.cc|
|
||||
tv1\.gomntu\.space|
|
||||
tv2\.cocu\.cc|
|
||||
tv\.adn\.life|
|
||||
tv\.atmx\.ca|
|
||||
tv\.bitma\.st|
|
||||
tv\.generallyrubbish\.net\.au|
|
||||
tv\.lumbung\.space|
|
||||
tv\.mattchristiansenmedia\.com|
|
||||
tv\.netwhood\.online|
|
||||
tv\.neue\.city|
|
||||
tv\.piejacker\.net|
|
||||
tv\.pirateradio\.social|
|
||||
tv\.undersco\.re|
|
||||
tvox\.ru|
|
||||
twctube\.twc-zone\.eu|
|
||||
unfilter\.tube|
|
||||
v\.basspistol\.org|
|
||||
v\.kisombrella\.top|
|
||||
v\.lastorder\.xyz|
|
||||
v\.lor\.sh|
|
||||
v\.phreedom\.club|
|
||||
v\.sil\.sh|
|
||||
v\.szy\.io|
|
||||
v\.xxxapex\.com|
|
||||
veezee\.tube|
|
||||
vid\.dascoyote\.xyz|
|
||||
vid\.garwood\.io|
|
||||
vid\.ncrypt\.at|
|
||||
vid\.pravdastalina\.info|
|
||||
vid\.qorg11\.net|
|
||||
vid\.rajeshtaylor\.com|
|
||||
vid\.samtripoli\.com|
|
||||
vid\.werefox\.dev|
|
||||
vid\.wildeboer\.net|
|
||||
video-cave-v2\.de|
|
||||
video\.076\.ne\.jp|
|
||||
video\.1146\.nohost\.me|
|
||||
video\.altertek\.org|
|
||||
video\.anartist\.org|
|
||||
video\.apps\.thedoodleproject\.net|
|
||||
video\.artist\.cx|
|
||||
video\.asgardius\.company|
|
||||
video\.balsillie\.net|
|
||||
video\.bards\.online|
|
||||
video\.binarydad\.com|
|
||||
video\.blast-info\.fr|
|
||||
video\.catgirl\.biz|
|
||||
video\.cigliola\.com|
|
||||
video\.cm-en-transition\.fr|
|
||||
video\.cnt\.social|
|
||||
video\.coales\.co|
|
||||
video\.codingfield\.com|
|
||||
video\.comptoir\.net|
|
||||
video\.comune\.trento\.it|
|
||||
video\.cpn\.so|
|
||||
video\.csc49\.fr|
|
||||
video\.cybre\.town|
|
||||
video\.demokratischer-sommer\.de|
|
||||
video\.discord-insoumis\.fr|
|
||||
video\.dolphincastle\.com|
|
||||
video\.dresden\.network|
|
||||
video\.ecole-89\.com|
|
||||
video\.elgrillolibertario\.org|
|
||||
video\.emergeheart\.info|
|
||||
video\.eradicatinglove\.xyz|
|
||||
video\.ethantheenigma\.me|
|
||||
video\.exodus-privacy\.eu\.org|
|
||||
video\.fbxl\.net|
|
||||
video\.fhtagn\.org|
|
||||
video\.greenmycity\.eu|
|
||||
video\.guerredeclasse\.fr|
|
||||
video\.gyt\.is|
|
||||
video\.hackers\.town|
|
||||
video\.hardlimit\.com|
|
||||
video\.hooli\.co|
|
||||
video\.igem\.org|
|
||||
video\.internet-czas-dzialac\.pl|
|
||||
video\.islameye\.com|
|
||||
video\.kicik\.fr|
|
||||
video\.kuba-orlik\.name|
|
||||
video\.kyushojitsu\.ca|
|
||||
video\.lavolte\.net|
|
||||
video\.lespoesiesdheloise\.fr|
|
||||
video\.liberta\.vip|
|
||||
video\.liege\.bike|
|
||||
video\.linc\.systems|
|
||||
video\.linux\.it|
|
||||
video\.linuxtrent\.it|
|
||||
video\.lokal\.social|
|
||||
video\.lono\.space|
|
||||
video\.lunasqu\.ee|
|
||||
video\.lundi\.am|
|
||||
video\.marcorennmaus\.de|
|
||||
video\.mass-trespass\.uk|
|
||||
video\.mugoreve\.fr|
|
||||
video\.mundodesconocido\.com|
|
||||
video\.mycrowd\.ca|
|
||||
video\.nogafam\.es|
|
||||
video\.odayacres\.farm|
|
||||
video\.ozgurkon\.org|
|
||||
video\.p1ng0ut\.social|
|
||||
video\.p3x\.de|
|
||||
video\.pcf\.fr|
|
||||
video\.pony\.gallery|
|
||||
video\.potate\.space|
|
||||
video\.pourpenser\.pro|
|
||||
video\.progressiv\.dev|
|
||||
video\.resolutions\.it|
|
||||
video\.rw501\.de|
|
||||
video\.screamer\.wiki|
|
||||
video\.sdm-tools\.net|
|
||||
video\.sftblw\.moe|
|
||||
video\.shitposter\.club|
|
||||
video\.skyn3t\.in|
|
||||
video\.soi\.ch|
|
||||
video\.stuartbrand\.co\.uk|
|
||||
video\.thinkof\.name|
|
||||
video\.toot\.pt|
|
||||
video\.triplea\.fr|
|
||||
video\.turbo\.chat|
|
||||
video\.vaku\.org\.ua|
|
||||
video\.veloma\.org|
|
||||
video\.violoncello\.ch|
|
||||
video\.wilkie\.how|
|
||||
video\.wsf2021\.info|
|
||||
videorelay\.co|
|
||||
videos-passages\.huma-num\.fr|
|
||||
videos\.3d-wolf\.com|
|
||||
videos\.ahp-numerique\.fr|
|
||||
videos\.alexandrebadalo\.pt|
|
||||
videos\.archigny\.net|
|
||||
videos\.benjaminbrady\.ie|
|
||||
videos\.buceoluegoexisto\.com|
|
||||
videos\.capas\.se|
|
||||
videos\.casually\.cat|
|
||||
videos\.cloudron\.io|
|
||||
videos\.coletivos\.org|
|
||||
videos\.danksquad\.org|
|
||||
videos\.denshi\.live|
|
||||
videos\.fromouter\.space|
|
||||
videos\.fsci\.in|
|
||||
videos\.globenet\.org|
|
||||
videos\.hauspie\.fr|
|
||||
videos\.hush\.is|
|
||||
videos\.john-livingston\.fr|
|
||||
videos\.jordanwarne\.xyz|
|
||||
videos\.lavoixdessansvoix\.org|
|
||||
videos\.leslionsfloorball\.fr|
|
||||
videos\.lucero\.top|
|
||||
videos\.martyn\.berlin|
|
||||
videos\.mastodont\.cat|
|
||||
videos\.monstro1\.com|
|
||||
videos\.npo\.city|
|
||||
videos\.optoutpod\.com|
|
||||
videos\.petch\.rocks|
|
||||
videos\.pzelawski\.xyz|
|
||||
videos\.rampin\.org|
|
||||
videos\.scanlines\.xyz|
|
||||
videos\.shmalls\.pw|
|
||||
videos\.sibear\.fr|
|
||||
videos\.stadtfabrikanten\.org|
|
||||
videos\.tankernn\.eu|
|
||||
videos\.testimonia\.org|
|
||||
videos\.thisishowidontdisappear\.com|
|
||||
videos\.traumaheilung\.net|
|
||||
videos\.trom\.tf|
|
||||
videos\.wakkerewereld\.nu|
|
||||
videos\.weblib\.re|
|
||||
videos\.yesil\.club|
|
||||
vids\.roshless\.me|
|
||||
vids\.tekdmn\.me|
|
||||
vidz\.dou\.bet|
|
||||
vod\.lumikko\.dev|
|
||||
vs\.uniter\.network|
|
||||
vulgarisation-informatique\.fr|
|
||||
watch\.breadtube\.tv|
|
||||
watch\.deranalyst\.ch|
|
||||
watch\.ignorance\.eu|
|
||||
watch\.krazy\.party|
|
||||
watch\.libertaria\.space|
|
||||
watch\.rt4mn\.org|
|
||||
watch\.softinio\.com|
|
||||
watch\.tubelab\.video|
|
||||
web-fellow\.de|
|
||||
webtv\.vandoeuvre\.net|
|
||||
wechill\.space|
|
||||
wikileaks\.video|
|
||||
wiwi\.video|
|
||||
worldofvids\.com|
|
||||
wwtube\.net|
|
||||
www4\.mir\.inter21\.net|
|
||||
www\.birkeundnymphe\.de|
|
||||
www\.captain-german\.com|
|
||||
www\.wiki-tube\.de|
|
||||
xxivproduction\.video|
|
||||
xxx\.noho\.st|
|
||||
|
||||
# from youtube-dl
|
||||
peertube\.rainbowswingers\.net|
|
||||
tube\.stanisic\.nl|
|
||||
peer\.suiri\.us|
|
||||
@@ -439,9 +1072,9 @@ class PeerTubeIE(InfoExtractor):
|
||||
'uploader': 'Framasoft',
|
||||
'uploader_id': '3',
|
||||
'uploader_url': 'https://framatube.org/accounts/framasoft',
|
||||
'channel': 'Les vidéos de Framasoft',
|
||||
'channel_id': '2',
|
||||
'channel_url': 'https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8',
|
||||
'channel': 'A propos de PeerTube',
|
||||
'channel_id': '2215',
|
||||
'channel_url': 'https://framatube.org/video-channels/joinpeertube',
|
||||
'language': 'en',
|
||||
'license': 'Attribution - Share Alike',
|
||||
'duration': 113,
|
||||
@@ -497,20 +1130,20 @@ class PeerTubeIE(InfoExtractor):
|
||||
'uploader': 'Drew DeVault',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://peertube.tamanoir.foucry.net/videos/watch/0b04f13d-1e18-4f1d-814e-4979aa7c9c44',
|
||||
'url': 'https://peertube.debian.social/videos/watch/0b04f13d-1e18-4f1d-814e-4979aa7c9c44',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
# nsfw
|
||||
'url': 'https://tube.22decembre.eu/videos/watch/9bb88cd3-9959-46d9-9ab9-33d2bb704c39',
|
||||
'url': 'https://vod.ksite.de/videos/watch/9bb88cd3-9959-46d9-9ab9-33d2bb704c39',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://tube.22decembre.eu/videos/embed/fed67262-6edb-4d1c-833b-daa9085c71d7',
|
||||
'url': 'https://vod.ksite.de/videos/embed/fed67262-6edb-4d1c-833b-daa9085c71d7',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://tube.openalgeria.org/api/v1/videos/c1875674-97d0-4c94-a058-3f7e64c962e8',
|
||||
'url': 'https://peertube.tv/api/v1/videos/c1875674-97d0-4c94-a058-3f7e64c962e8',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'peertube:video.blender.org:b37a5b9f-e6b5-415c-b700-04a5cd6ec205',
|
||||
'url': 'peertube:framatube.org:b37a5b9f-e6b5-415c-b700-04a5cd6ec205',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
@@ -660,3 +1293,110 @@ class PeerTubeIE(InfoExtractor):
|
||||
'subtitles': subtitles,
|
||||
'webpage_url': webpage_url,
|
||||
}
|
||||
|
||||
|
||||
class PeerTubePlaylistIE(InfoExtractor):
|
||||
IE_NAME = 'PeerTube:Playlist'
|
||||
_TYPES = {
|
||||
'a': 'accounts',
|
||||
'c': 'video-channels',
|
||||
'w/p': 'video-playlists',
|
||||
}
|
||||
_VALID_URL = r'''(?x)
|
||||
https?://(?P<host>%s)/(?P<type>(?:%s))/
|
||||
(?P<id>[^/]+)
|
||||
''' % (PeerTubeIE._INSTANCES_RE, '|'.join(_TYPES.keys()))
|
||||
_TESTS = [{
|
||||
'url': 'https://peertube.tux.ovh/w/p/3af94cba-95e8-4b74-b37a-807ab6d82526',
|
||||
'info_dict': {
|
||||
'id': '3af94cba-95e8-4b74-b37a-807ab6d82526',
|
||||
'description': 'playlist',
|
||||
'timestamp': 1611171863,
|
||||
'title': 'playlist',
|
||||
},
|
||||
'playlist_mincount': 6,
|
||||
}, {
|
||||
'url': 'https://peertube.tux.ovh/w/p/wkyqcQBnsvFxtUB2pkYc1e',
|
||||
'info_dict': {
|
||||
'id': 'wkyqcQBnsvFxtUB2pkYc1e',
|
||||
'description': 'Cette liste de vidéos contient uniquement les jeux qui peuvent être terminés en une seule vidéo.',
|
||||
'title': 'Let\'s Play',
|
||||
'timestamp': 1604147331,
|
||||
},
|
||||
'playlist_mincount': 6,
|
||||
}, {
|
||||
'url': 'https://peertube.debian.social/w/p/hFdJoTuyhNJVa1cDWd1d12',
|
||||
'info_dict': {
|
||||
'id': 'hFdJoTuyhNJVa1cDWd1d12',
|
||||
'description': 'Diversas palestras do Richard Stallman no Brasil.',
|
||||
'title': 'Richard Stallman no Brasil',
|
||||
'timestamp': 1599676222,
|
||||
},
|
||||
'playlist_mincount': 9,
|
||||
}, {
|
||||
'url': 'https://peertube2.cpy.re/a/chocobozzz/videos',
|
||||
'info_dict': {
|
||||
'id': 'chocobozzz',
|
||||
'timestamp': 1553874564,
|
||||
'title': 'chocobozzz',
|
||||
},
|
||||
'playlist_mincount': 2,
|
||||
}, {
|
||||
'url': 'https://framatube.org/c/bf54d359-cfad-4935-9d45-9d6be93f63e8/videos',
|
||||
'info_dict': {
|
||||
'id': 'bf54d359-cfad-4935-9d45-9d6be93f63e8',
|
||||
'timestamp': 1519917377,
|
||||
'title': 'Les vidéos de Framasoft',
|
||||
},
|
||||
'playlist_mincount': 345,
|
||||
}, {
|
||||
'url': 'https://peertube2.cpy.re/c/blender_open_movies@video.blender.org/videos',
|
||||
'info_dict': {
|
||||
'id': 'blender_open_movies@video.blender.org',
|
||||
'timestamp': 1542287810,
|
||||
'title': 'Official Blender Open Movies',
|
||||
},
|
||||
'playlist_mincount': 11,
|
||||
}]
|
||||
_API_BASE = 'https://%s/api/v1/%s/%s%s'
|
||||
_PAGE_SIZE = 30
|
||||
|
||||
def call_api(self, host, name, path, base, **kwargs):
|
||||
return self._download_json(
|
||||
self._API_BASE % (host, base, name, path), name, **kwargs)
|
||||
|
||||
def fetch_page(self, host, id, type, page):
|
||||
page += 1
|
||||
video_data = self.call_api(
|
||||
host, id,
|
||||
f'/videos?sort=-createdAt&start={self._PAGE_SIZE * (page - 1)}&count={self._PAGE_SIZE}&nsfw=both',
|
||||
type, note=f'Downloading page {page}').get('data', [])
|
||||
for video in video_data:
|
||||
shortUUID = video.get('shortUUID') or try_get(video, lambda x: x['video']['shortUUID'])
|
||||
video_title = video.get('name') or try_get(video, lambda x: x['video']['name'])
|
||||
yield self.url_result(
|
||||
f'https://{host}/w/{shortUUID}', PeerTubeIE.ie_key(),
|
||||
video_id=shortUUID, video_title=video_title)
|
||||
|
||||
def _extract_playlist(self, host, type, id):
|
||||
info = self.call_api(host, id, '', type, note='Downloading playlist information', fatal=False)
|
||||
|
||||
playlist_title = info.get('displayName')
|
||||
playlist_description = info.get('description')
|
||||
playlist_timestamp = unified_timestamp(info.get('createdAt'))
|
||||
channel = try_get(info, lambda x: x['ownerAccount']['name']) or info.get('displayName')
|
||||
channel_id = try_get(info, lambda x: x['ownerAccount']['id']) or info.get('id')
|
||||
thumbnail = info.get('thumbnailPath')
|
||||
thumbnail = f'https://{host}{thumbnail}' if thumbnail else None
|
||||
|
||||
entries = OnDemandPagedList(functools.partial(
|
||||
self.fetch_page, host, id, type), self._PAGE_SIZE)
|
||||
|
||||
return self.playlist_result(
|
||||
entries, id, playlist_title, playlist_description,
|
||||
timestamp=playlist_timestamp, channel=channel, channel_id=channel_id, thumbnail=thumbnail)
|
||||
|
||||
def _real_extract(self, url):
|
||||
type, host, id = self._match_valid_url(url).group('type', 'host', 'id')
|
||||
type = self._TYPES[type]
|
||||
return self._extract_playlist(host, type, id)
|
||||
|
||||
+15
-10
@@ -19,7 +19,16 @@ from ..utils import (
|
||||
|
||||
|
||||
class PlutoTVIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?pluto\.tv(?:/en)?/on-demand/(?P<video_type>movies|series)/(?P<slug>.*)/?$'
|
||||
_VALID_URL = r'''(?x)
|
||||
https?://(?:www\.)?pluto\.tv(?:/en)?/on-demand
|
||||
/(?P<video_type>movies|series)
|
||||
/(?P<series_or_movie_slug>[^/]+)
|
||||
(?:
|
||||
/seasons?/(?P<season_no>\d+)
|
||||
(?:/episode/(?P<episode_slug>[^/]+))?
|
||||
)?
|
||||
/?(?:$|[#?])'''
|
||||
|
||||
_INFO_URL = 'https://service-vod.clusters.pluto.tv/v3/vod/slugs/'
|
||||
_INFO_QUERY_PARAMS = {
|
||||
'appName': 'web',
|
||||
@@ -146,17 +155,13 @@ class PlutoTVIE(InfoExtractor):
|
||||
return info
|
||||
|
||||
def _real_extract(self, url):
|
||||
path = compat_urlparse.urlparse(url).path
|
||||
path_components = path.split('/')
|
||||
video_type = path_components[2]
|
||||
info_slug = path_components[3]
|
||||
video_json = self._download_json(self._INFO_URL + info_slug, info_slug,
|
||||
query=self._INFO_QUERY_PARAMS)
|
||||
mobj = self._match_valid_url(url).groupdict()
|
||||
info_slug = mobj['series_or_movie_slug']
|
||||
video_json = self._download_json(self._INFO_URL + info_slug, info_slug, query=self._INFO_QUERY_PARAMS)
|
||||
|
||||
if video_type == 'series':
|
||||
if mobj['video_type'] == 'series':
|
||||
series_name = video_json.get('name', info_slug)
|
||||
season_number = int_or_none(try_get(path_components, lambda x: x[5]))
|
||||
episode_slug = try_get(path_components, lambda x: x[7])
|
||||
season_number, episode_slug = mobj.get('season_number'), mobj.get('episode_slug')
|
||||
|
||||
videos = []
|
||||
for season in video_json['seasons']:
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import json
|
||||
|
||||
from ..utils import ExtractorError, traverse_obj, try_get, unified_timestamp
|
||||
from .common import InfoExtractor
|
||||
|
||||
|
||||
class RadLiveIE(InfoExtractor):
|
||||
IE_NAME = 'radlive'
|
||||
_VALID_URL = r'https?://(?:www\.)?rad\.live/content/(?P<content_type>feature|episode)/(?P<id>[a-f0-9-]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://rad.live/content/feature/dc5acfbc-761b-4bec-9564-df999905116a',
|
||||
'md5': '6219d5d31d52de87d21c9cf5b7cb27ff',
|
||||
'info_dict': {
|
||||
'id': 'dc5acfbc-761b-4bec-9564-df999905116a',
|
||||
'ext': 'mp4',
|
||||
'title': 'Deathpact - Digital Mirage 2 [Full Set]',
|
||||
'language': 'en',
|
||||
'thumbnail': 'https://static.12core.net/cb65ae077a079c68380e38f387fbc438.png',
|
||||
'description': '',
|
||||
'release_timestamp': 1600185600.0,
|
||||
'channel': 'Proximity',
|
||||
'channel_id': '9ce6dd01-70a4-4d59-afb6-d01f807cd009',
|
||||
'channel_url': 'https://rad.live/content/channel/9ce6dd01-70a4-4d59-afb6-d01f807cd009',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://rad.live/content/episode/bbcf66ec-0d02-4ca0-8dc0-4213eb2429bf',
|
||||
'md5': '40b2175f347592125d93e9a344080125',
|
||||
'info_dict': {
|
||||
'id': 'bbcf66ec-0d02-4ca0-8dc0-4213eb2429bf',
|
||||
'ext': 'mp4',
|
||||
'title': 'E01: Bad Jokes 1',
|
||||
'language': 'en',
|
||||
'thumbnail': 'https://lsp.littlstar.com/channels/WHISTLE/BAD_JOKES/SEASON_1/BAD_JOKES_101/poster.jpg',
|
||||
'description': 'Bad Jokes - Champions, Adam Pally, Super Troopers, Team Edge and 2Hype',
|
||||
'release_timestamp': None,
|
||||
'channel': None,
|
||||
'channel_id': None,
|
||||
'channel_url': None,
|
||||
'episode': 'E01: Bad Jokes 1',
|
||||
'episode_number': 1,
|
||||
'episode_id': '336',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
content_type, video_id = self._match_valid_url(url).groups()
|
||||
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
content_info = json.loads(self._search_regex(
|
||||
r'<script[^>]*type=([\'"])application/json\1[^>]*>(?P<json>{.+?})</script>',
|
||||
webpage, 'video info', group='json'))['props']['pageProps']['initialContentData']
|
||||
video_info = content_info[content_type]
|
||||
|
||||
if not video_info:
|
||||
raise ExtractorError('Unable to extract video info, make sure the URL is valid')
|
||||
|
||||
formats = self._extract_m3u8_formats(video_info['assets']['videos'][0]['url'], video_id)
|
||||
self._sort_formats(formats)
|
||||
|
||||
data = video_info.get('structured_data', {})
|
||||
|
||||
release_date = unified_timestamp(traverse_obj(data, ('releasedEvent', 'startDate')))
|
||||
channel = next(iter(content_info.get('channels', [])), {})
|
||||
channel_id = channel.get('lrn', '').split(':')[-1] or None
|
||||
|
||||
result = {
|
||||
'id': video_id,
|
||||
'title': video_info['title'],
|
||||
'formats': formats,
|
||||
'language': traverse_obj(data, ('potentialAction', 'target', 'inLanguage')),
|
||||
'thumbnail': traverse_obj(data, ('image', 'contentUrl')),
|
||||
'description': data.get('description'),
|
||||
'release_timestamp': release_date,
|
||||
'channel': channel.get('name'),
|
||||
'channel_id': channel_id,
|
||||
'channel_url': f'https://rad.live/content/channel/{channel_id}' if channel_id else None,
|
||||
|
||||
}
|
||||
if content_type == 'episode':
|
||||
result.update({
|
||||
# TODO: Get season number when downloading single episode
|
||||
'episode': video_info.get('title'),
|
||||
'episode_number': video_info.get('number'),
|
||||
'episode_id': video_info.get('id'),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class RadLiveSeasonIE(RadLiveIE):
|
||||
IE_NAME = 'radlive:season'
|
||||
_VALID_URL = r'https?://(?:www\.)?rad\.live/content/season/(?P<id>[a-f0-9-]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://rad.live/content/season/08a290f7-c9ef-4e22-9105-c255995a2e75',
|
||||
'md5': '40b2175f347592125d93e9a344080125',
|
||||
'info_dict': {
|
||||
'id': '08a290f7-c9ef-4e22-9105-c255995a2e75',
|
||||
'title': 'Bad Jokes - Season 1',
|
||||
},
|
||||
'playlist_mincount': 5,
|
||||
}]
|
||||
|
||||
@classmethod
|
||||
def suitable(cls, url):
|
||||
return False if RadLiveIE.suitable(url) else super(RadLiveSeasonIE, cls).suitable(url)
|
||||
|
||||
def _real_extract(self, url):
|
||||
season_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, season_id)
|
||||
|
||||
content_info = json.loads(self._search_regex(
|
||||
r'<script[^>]*type=([\'"])application/json\1[^>]*>(?P<json>{.+?})</script>',
|
||||
webpage, 'video info', group='json'))['props']['pageProps']['initialContentData']
|
||||
video_info = content_info['season']
|
||||
|
||||
entries = [{
|
||||
'_type': 'url_transparent',
|
||||
'id': episode['structured_data']['url'].split('/')[-1],
|
||||
'url': episode['structured_data']['url'],
|
||||
'series': try_get(content_info, lambda x: x['series']['title']),
|
||||
'season': video_info['title'],
|
||||
'season_number': video_info.get('number'),
|
||||
'season_id': video_info.get('id'),
|
||||
'ie_key': RadLiveIE.ie_key(),
|
||||
} for episode in video_info['episodes']]
|
||||
|
||||
return self.playlist_result(entries, season_id, video_info.get('title'))
|
||||
|
||||
|
||||
class RadLiveChannelIE(RadLiveIE):
|
||||
IE_NAME = 'radlive:channel'
|
||||
_VALID_URL = r'https?://(?:www\.)?rad\.live/content/channel/(?P<id>[a-f0-9-]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://rad.live/content/channel/5c4d8df4-6fa0-413c-81e3-873479b49274',
|
||||
'md5': '625156a08b7f2b0b849f234e664457ac',
|
||||
'info_dict': {
|
||||
'id': '5c4d8df4-6fa0-413c-81e3-873479b49274',
|
||||
'title': 'Whistle Sports',
|
||||
},
|
||||
'playlist_mincount': 7,
|
||||
}]
|
||||
|
||||
_QUERY = '''
|
||||
query WebChannelListing ($lrn: ID!) {
|
||||
channel (id:$lrn) {
|
||||
name
|
||||
features {
|
||||
structured_data
|
||||
}
|
||||
}
|
||||
}'''
|
||||
|
||||
@classmethod
|
||||
def suitable(cls, url):
|
||||
return False if RadLiveIE.suitable(url) else super(RadLiveChannelIE, cls).suitable(url)
|
||||
|
||||
def _real_extract(self, url):
|
||||
channel_id = self._match_id(url)
|
||||
|
||||
graphql = self._download_json(
|
||||
'https://content.mhq.12core.net/graphql', channel_id,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({
|
||||
'query': self._QUERY,
|
||||
'variables': {'lrn': f'lrn:12core:media:content:channel:{channel_id}'}
|
||||
}).encode('utf-8'))
|
||||
|
||||
data = traverse_obj(graphql, ('data', 'channel'))
|
||||
if not data:
|
||||
raise ExtractorError('Unable to extract video info, make sure the URL is valid')
|
||||
|
||||
entries = [{
|
||||
'_type': 'url_transparent',
|
||||
'url': feature['structured_data']['url'],
|
||||
'ie_key': RadLiveIE.ie_key(),
|
||||
} for feature in data['features']]
|
||||
|
||||
return self.playlist_result(entries, channel_id, data.get('name'))
|
||||
@@ -49,7 +49,7 @@ class RedditIE(InfoExtractor):
|
||||
|
||||
|
||||
class RedditRIE(InfoExtractor):
|
||||
_VALID_URL = r'(?P<url>https?://(?:[^/]+\.)?reddit\.com/r/[^/]+/comments/(?P<id>[^/?#&]+))'
|
||||
_VALID_URL = r'https?://(?:[^/]+\.)?reddit\.com/r/(?P<slug>[^/]+/comments/(?P<id>[^/?#&]+))'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.reddit.com/r/videos/comments/6rrwyj/that_small_heart_attack/',
|
||||
'info_dict': {
|
||||
@@ -97,15 +97,11 @@ class RedditRIE(InfoExtractor):
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = self._match_valid_url(url)
|
||||
url, video_id = mobj.group('url', 'id')
|
||||
|
||||
video_id = self._match_id(url)
|
||||
slug, video_id = self._match_valid_url(url).group('slug', 'id')
|
||||
|
||||
self._set_cookie('reddit.com', '_options', '%7B%22pref_quarantine_optin%22%3A%20true%7D')
|
||||
|
||||
data = self._download_json(
|
||||
url + '/.json', video_id)[0]['data']['children'][0]['data']
|
||||
f'https://old.reddit.com/r/{slug}/.json', video_id)[0]['data']['children'][0]['data']
|
||||
|
||||
video_url = data['url']
|
||||
|
||||
|
||||
@@ -98,13 +98,14 @@ class RedTubeIE(InfoExtractor):
|
||||
format_id = media.get('quality')
|
||||
formats.append({
|
||||
'url': format_url,
|
||||
'ext': 'mp4',
|
||||
'format_id': format_id,
|
||||
'height': int_or_none(format_id),
|
||||
})
|
||||
if not formats:
|
||||
video_url = self._html_search_regex(
|
||||
r'<source src="(.+?)" type="video/mp4">', webpage, 'video URL')
|
||||
formats.append({'url': video_url})
|
||||
formats.append({'url': video_url, 'ext': 'mp4'})
|
||||
self._sort_formats(formats)
|
||||
|
||||
thumbnail = self._og_search_thumbnail(webpage)
|
||||
|
||||
@@ -193,7 +193,12 @@ class SafariApiIE(SafariBaseIE):
|
||||
part = self._download_json(
|
||||
url, '%s/%s' % (mobj.group('course_id'), mobj.group('part')),
|
||||
'Downloading part JSON')
|
||||
return self.url_result(part['web_url'], SafariIE.ie_key())
|
||||
web_url = part['web_url']
|
||||
if 'library/view' in web_url:
|
||||
web_url = web_url.replace('library/view', 'videos')
|
||||
natural_keys = part['natural_key']
|
||||
web_url = f'{web_url.rsplit("/", 1)[0]}/{natural_keys[0]}-{natural_keys[1][:-5]}'
|
||||
return self.url_result(web_url, SafariIE.ie_key())
|
||||
|
||||
|
||||
class SafariCourseIE(SafariBaseIE):
|
||||
|
||||
@@ -14,7 +14,6 @@ from ..compat import (
|
||||
compat_HTTPError,
|
||||
compat_kwargs,
|
||||
compat_str,
|
||||
compat_urlparse,
|
||||
)
|
||||
from ..utils import (
|
||||
error_to_compat_str,
|
||||
@@ -24,6 +23,8 @@ from ..utils import (
|
||||
int_or_none,
|
||||
KNOWN_EXTENSIONS,
|
||||
mimetype2ext,
|
||||
remove_end,
|
||||
parse_qs,
|
||||
str_or_none,
|
||||
try_get,
|
||||
unified_timestamp,
|
||||
@@ -49,8 +50,7 @@ class SoundcloudEmbedIE(InfoExtractor):
|
||||
webpage)]
|
||||
|
||||
def _real_extract(self, url):
|
||||
query = compat_urlparse.parse_qs(
|
||||
compat_urlparse.urlparse(url).query)
|
||||
query = parse_qs(url)
|
||||
api_url = query['url'][0]
|
||||
secret_token = query.get('secret_token')
|
||||
if secret_token:
|
||||
@@ -305,7 +305,7 @@ class SoundcloudIE(InfoExtractor):
|
||||
raise
|
||||
|
||||
def _real_initialize(self):
|
||||
self._CLIENT_ID = self._downloader.cache.load('soundcloud', 'client_id') or 'fSSdm5yTnDka1g0Fz1CO5Yx6z0NbeHAj'
|
||||
self._CLIENT_ID = self._downloader.cache.load('soundcloud', 'client_id') or 'a3e059563d7fd3372b49b37f00a00bcf'
|
||||
self._login()
|
||||
|
||||
_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36'
|
||||
@@ -656,65 +656,59 @@ class SoundcloudSetIE(SoundcloudPlaylistBaseIE):
|
||||
|
||||
class SoundcloudPagedPlaylistBaseIE(SoundcloudIE):
|
||||
def _extract_playlist(self, base_url, playlist_id, playlist_title):
|
||||
# Per the SoundCloud documentation, the maximum limit for a linked partitioning query is 200.
|
||||
# https://developers.soundcloud.com/blog/offset-pagination-deprecated
|
||||
COMMON_QUERY = {
|
||||
'limit': 200,
|
||||
'linked_partitioning': '1',
|
||||
}
|
||||
|
||||
query = COMMON_QUERY.copy()
|
||||
query['offset'] = 0
|
||||
|
||||
next_href = base_url
|
||||
|
||||
entries = []
|
||||
for i in itertools.count():
|
||||
response = self._download_json(
|
||||
next_href, playlist_id,
|
||||
'Downloading track page %s' % (i + 1), query=query, headers=self._HEADERS)
|
||||
|
||||
collection = response['collection']
|
||||
|
||||
if not isinstance(collection, list):
|
||||
collection = []
|
||||
|
||||
# Empty collection may be returned, in this case we proceed
|
||||
# straight to next_href
|
||||
|
||||
def resolve_entry(candidates):
|
||||
for cand in candidates:
|
||||
if not isinstance(cand, dict):
|
||||
continue
|
||||
permalink_url = url_or_none(cand.get('permalink_url'))
|
||||
if not permalink_url:
|
||||
continue
|
||||
return self.url_result(
|
||||
permalink_url,
|
||||
SoundcloudIE.ie_key() if SoundcloudIE.suitable(permalink_url) else None,
|
||||
str_or_none(cand.get('id')), cand.get('title'))
|
||||
|
||||
for e in collection:
|
||||
entry = resolve_entry((e, e.get('track'), e.get('playlist')))
|
||||
if entry:
|
||||
entries.append(entry)
|
||||
|
||||
next_href = response.get('next_href')
|
||||
if not next_href:
|
||||
break
|
||||
|
||||
next_href = response['next_href']
|
||||
parsed_next_href = compat_urlparse.urlparse(next_href)
|
||||
query = compat_urlparse.parse_qs(parsed_next_href.query)
|
||||
query.update(COMMON_QUERY)
|
||||
|
||||
return {
|
||||
'_type': 'playlist',
|
||||
'id': playlist_id,
|
||||
'title': playlist_title,
|
||||
'entries': entries,
|
||||
'entries': self._entries(base_url, playlist_id),
|
||||
}
|
||||
|
||||
def _entries(self, url, playlist_id):
|
||||
# Per the SoundCloud documentation, the maximum limit for a linked partitioning query is 200.
|
||||
# https://developers.soundcloud.com/blog/offset-pagination-deprecated
|
||||
query = {
|
||||
'limit': 200,
|
||||
'linked_partitioning': '1',
|
||||
'offset': 0,
|
||||
}
|
||||
|
||||
retries = self.get_param('extractor_retries', 3)
|
||||
|
||||
for i in itertools.count():
|
||||
attempt, last_error = -1, None
|
||||
while attempt < retries:
|
||||
attempt += 1
|
||||
if last_error:
|
||||
self.report_warning('%s. Retrying ...' % remove_end(last_error, '.'), playlist_id)
|
||||
try:
|
||||
response = self._download_json(
|
||||
url, playlist_id, query=query, headers=self._HEADERS,
|
||||
note='Downloading track page %s%s' % (i + 1, f' (retry #{attempt})' if attempt else ''))
|
||||
break
|
||||
except ExtractorError as e:
|
||||
# Downloading page may result in intermittent 502 HTTP error
|
||||
# See https://github.com/yt-dlp/yt-dlp/issues/872
|
||||
if attempt >= retries or not isinstance(e.cause, compat_HTTPError) or e.cause.code != 502:
|
||||
raise
|
||||
last_error = str(e.cause or e.msg)
|
||||
|
||||
def resolve_entry(*candidates):
|
||||
for cand in candidates:
|
||||
if not isinstance(cand, dict):
|
||||
continue
|
||||
permalink_url = url_or_none(cand.get('permalink_url'))
|
||||
if permalink_url:
|
||||
return self.url_result(
|
||||
permalink_url,
|
||||
SoundcloudIE.ie_key() if SoundcloudIE.suitable(permalink_url) else None,
|
||||
str_or_none(cand.get('id')), cand.get('title'))
|
||||
|
||||
for e in response['collection'] or []:
|
||||
yield resolve_entry(e, e.get('track'), e.get('playlist'))
|
||||
|
||||
url = response.get('next_href')
|
||||
query.pop('offset', None)
|
||||
|
||||
|
||||
class SoundcloudUserIE(SoundcloudPagedPlaylistBaseIE):
|
||||
_VALID_URL = r'''(?x)
|
||||
|
||||
@@ -56,9 +56,7 @@ class SouthParkEsIE(SouthParkIE):
|
||||
|
||||
class SouthParkDeIE(SouthParkIE):
|
||||
IE_NAME = 'southpark.de'
|
||||
_VALID_URL = r'https?://(?:www\.)?(?P<url>southpark\.de/(?:(en/(videoclip|collections|episodes))|(videoclip|collections|folgen))/(?P<id>(?P<unique_id>.+?)/.+?)(?:\?|#|$))'
|
||||
# _FEED_URL = 'http://feeds.mtvnservices.com/od/feed/intl-mrss-player-feed'
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?(?P<url>southpark\.de/(?:(en/(videoclip|collections|episodes|video-clips))|(videoclip|collections|folgen))/(?P<id>(?P<unique_id>.+?)/.+?)(?:\?|#|$))'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.southpark.de/videoclip/rsribv/south-park-rueckzug-zum-gummibonbon-wald',
|
||||
'only_matching': True,
|
||||
@@ -68,6 +66,41 @@ class SouthParkDeIE(SouthParkIE):
|
||||
}, {
|
||||
'url': 'https://www.southpark.de/collections/zzno5a/south-park-good-eats/7q26gp',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
# clip
|
||||
'url': 'https://www.southpark.de/en/video-clips/ct46op/south-park-tooth-fairy-cartman',
|
||||
'info_dict': {
|
||||
'id': 'e99d45ea-ed00-11e0-aca6-0026b9414f30',
|
||||
'ext': 'mp4',
|
||||
'title': 'Tooth Fairy Cartman',
|
||||
'description': 'md5:db02e23818b4dc9cb5f0c5a7e8833a68',
|
||||
},
|
||||
}, {
|
||||
# episode
|
||||
'url': 'https://www.southpark.de/en/episodes/yy0vjs/south-park-the-pandemic-special-season-24-ep-1',
|
||||
'info_dict': {
|
||||
'id': 'f5fbd823-04bc-11eb-9b1b-0e40cf2fc285',
|
||||
'ext': 'mp4',
|
||||
'title': 'South Park',
|
||||
'description': 'md5:ae0d875eff169dcbed16b21531857ac1',
|
||||
},
|
||||
}, {
|
||||
# clip
|
||||
'url': 'https://www.southpark.de/videoclip/ct46op/south-park-zahnfee-cartman',
|
||||
'info_dict': {
|
||||
'id': 'e99d45ea-ed00-11e0-aca6-0026b9414f30',
|
||||
'ext': 'mp4',
|
||||
'title': 'Zahnfee Cartman',
|
||||
'description': 'md5:b917eec991d388811d911fd1377671ac'
|
||||
},
|
||||
}, {
|
||||
# episode
|
||||
'url': 'https://www.southpark.de/folgen/242csn/south-park-her-mit-dem-hirn-staffel-1-ep-7',
|
||||
'info_dict': {
|
||||
'id': '607115f3-496f-40c3-8647-2b0bcff486c0',
|
||||
'ext': 'mp4',
|
||||
'title': 'md5:South Park | Pink Eye | E 0107 | HDSS0107X deu | Version: 634312 | Comedy Central S1',
|
||||
},
|
||||
}]
|
||||
|
||||
def _get_feed_url(self, uri, url=None):
|
||||
@@ -76,6 +109,9 @@ class SouthParkDeIE(SouthParkIE):
|
||||
'http://media.mtvnservices.com/pmt/e1/access/index.html?uri=%s&configtype=edge&ref=%s' % (uri, url), video_id)
|
||||
return self._remove_template_parameter(config['feedWithQueryParams'])
|
||||
|
||||
def _get_feed_query(self, uri):
|
||||
return
|
||||
|
||||
|
||||
class SouthParkNlIE(SouthParkIE):
|
||||
IE_NAME = 'southpark.nl'
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
js_to_json,
|
||||
try_get,
|
||||
unified_timestamp
|
||||
)
|
||||
|
||||
|
||||
class SovietsClosetBaseIE(InfoExtractor):
|
||||
MEDIADELIVERY_REFERER = {'Referer': 'https://iframe.mediadelivery.net/'}
|
||||
|
||||
def parse_nuxt_jsonp(self, nuxt_jsonp_url, video_id, name):
|
||||
nuxt_jsonp = self._download_webpage(nuxt_jsonp_url, video_id, note=f'Downloading {name} __NUXT_JSONP__')
|
||||
js, arg_keys, arg_vals = self._search_regex(
|
||||
r'__NUXT_JSONP__\(.*?\(function\((?P<arg_keys>.*?)\)\{return\s(?P<js>\{.*?\})\}\((?P<arg_vals>.*?)\)',
|
||||
nuxt_jsonp, '__NUXT_JSONP__', group=['js', 'arg_keys', 'arg_vals'])
|
||||
|
||||
args = dict(zip(arg_keys.split(','), arg_vals.split(',')))
|
||||
|
||||
for key, val in args.items():
|
||||
if val in ('undefined', 'void 0'):
|
||||
args[key] = 'null'
|
||||
|
||||
return self._parse_json(js_to_json(js, args), video_id)['data'][0]
|
||||
|
||||
def video_meta(self, video_id, game_name, category_name, episode_number, stream_date):
|
||||
title = game_name
|
||||
if category_name and category_name != 'Misc':
|
||||
title += f' - {category_name}'
|
||||
if episode_number:
|
||||
title += f' #{episode_number}'
|
||||
|
||||
timestamp = unified_timestamp(stream_date)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'http_headers': self.MEDIADELIVERY_REFERER,
|
||||
'uploader': 'SovietWomble',
|
||||
'creator': 'SovietWomble',
|
||||
'release_timestamp': timestamp,
|
||||
'timestamp': timestamp,
|
||||
'uploader_id': 'SovietWomble',
|
||||
'uploader_url': 'https://www.twitch.tv/SovietWomble',
|
||||
'was_live': True,
|
||||
'availability': 'public',
|
||||
'series': game_name,
|
||||
'season': category_name,
|
||||
'episode_number': episode_number,
|
||||
}
|
||||
|
||||
|
||||
class SovietsClosetIE(SovietsClosetBaseIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?sovietscloset\.com/video/(?P<id>[0-9]+)/?'
|
||||
_TESTS = [
|
||||
{
|
||||
'url': 'https://sovietscloset.com/video/1337',
|
||||
'md5': '11e58781c4ca5b283307aa54db5b3f93',
|
||||
'info_dict': {
|
||||
'id': '1337',
|
||||
'ext': 'mp4',
|
||||
'title': 'The Witcher #13',
|
||||
'thumbnail': r're:^https?://.*\.b-cdn\.net/2f0cfbf4-3588-43a9-a7d6-7c9ea3755e67/thumbnail\.jpg$',
|
||||
'uploader': 'SovietWomble',
|
||||
'creator': 'SovietWomble',
|
||||
'release_timestamp': 1492091580,
|
||||
'release_date': '20170413',
|
||||
'timestamp': 1492091580,
|
||||
'upload_date': '20170413',
|
||||
'uploader_id': 'SovietWomble',
|
||||
'uploader_url': 'https://www.twitch.tv/SovietWomble',
|
||||
'was_live': True,
|
||||
'availability': 'public',
|
||||
'series': 'The Witcher',
|
||||
'season': 'Misc',
|
||||
'episode_number': 13,
|
||||
},
|
||||
},
|
||||
{
|
||||
'url': 'https://sovietscloset.com/video/1105',
|
||||
'md5': '578b1958a379e7110ba38697042e9efb',
|
||||
'info_dict': {
|
||||
'id': '1105',
|
||||
'ext': 'mp4',
|
||||
'title': 'Arma 3 - Zeus Games #3',
|
||||
'uploader': 'SovietWomble',
|
||||
'thumbnail': r're:^https?://.*\.b-cdn\.net/c0e5e76f-3a93-40b4-bf01-12343c2eec5d/thumbnail\.jpg$',
|
||||
'uploader': 'SovietWomble',
|
||||
'creator': 'SovietWomble',
|
||||
'release_timestamp': 1461157200,
|
||||
'release_date': '20160420',
|
||||
'timestamp': 1461157200,
|
||||
'upload_date': '20160420',
|
||||
'uploader_id': 'SovietWomble',
|
||||
'uploader_url': 'https://www.twitch.tv/SovietWomble',
|
||||
'was_live': True,
|
||||
'availability': 'public',
|
||||
'series': 'Arma 3',
|
||||
'season': 'Zeus Games',
|
||||
'episode_number': 3,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
def _extract_bunnycdn_iframe(self, video_id, bunnycdn_id):
|
||||
iframe = self._download_webpage(
|
||||
f'https://iframe.mediadelivery.net/embed/5105/{bunnycdn_id}',
|
||||
video_id, note='Downloading BunnyCDN iframe', headers=self.MEDIADELIVERY_REFERER)
|
||||
|
||||
m3u8_url = self._search_regex(r'(https?://.*?\.m3u8)', iframe, 'm3u8 url')
|
||||
thumbnail_url = self._search_regex(r'(https?://.*?thumbnail\.jpg)', iframe, 'thumbnail url')
|
||||
|
||||
m3u8_formats = self._extract_m3u8_formats(m3u8_url, video_id, headers=self.MEDIADELIVERY_REFERER)
|
||||
self._sort_formats(m3u8_formats)
|
||||
|
||||
return {
|
||||
'formats': m3u8_formats,
|
||||
'thumbnail': thumbnail_url,
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
static_assets_base = self._search_regex(r'staticAssetsBase:\"(.*?)\"', webpage, 'staticAssetsBase')
|
||||
static_assets_base = f'https://sovietscloset.com{static_assets_base}'
|
||||
|
||||
stream = self.parse_nuxt_jsonp(f'{static_assets_base}/video/{video_id}/payload.js', video_id, 'video')['stream']
|
||||
|
||||
return {
|
||||
**self.video_meta(
|
||||
video_id=video_id, game_name=stream['game']['name'],
|
||||
category_name=try_get(stream, lambda x: x['subcategory']['name'], str),
|
||||
episode_number=stream.get('number'), stream_date=stream.get('date')),
|
||||
**self._extract_bunnycdn_iframe(video_id, stream['bunnyId']),
|
||||
}
|
||||
|
||||
|
||||
class SovietsClosetPlaylistIE(SovietsClosetBaseIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?sovietscloset\.com/(?!video)(?P<id>[^#?]+)'
|
||||
_TESTS = [
|
||||
|
||||
{
|
||||
'url': 'https://sovietscloset.com/The-Witcher',
|
||||
'info_dict': {
|
||||
'id': 'The-Witcher',
|
||||
'title': 'The Witcher',
|
||||
},
|
||||
'playlist_mincount': 31,
|
||||
},
|
||||
{
|
||||
'url': 'https://sovietscloset.com/Arma-3/Zeus-Games',
|
||||
'info_dict': {
|
||||
'id': 'Arma-3/Zeus-Games',
|
||||
'title': 'Arma 3 - Zeus Games',
|
||||
},
|
||||
'playlist_mincount': 3,
|
||||
},
|
||||
{
|
||||
'url': 'https://sovietscloset.com/arma-3/zeus-games/',
|
||||
'info_dict': {
|
||||
'id': 'arma-3/zeus-games',
|
||||
'title': 'Arma 3 - Zeus Games',
|
||||
},
|
||||
'playlist_mincount': 3,
|
||||
},
|
||||
{
|
||||
'url': 'https://sovietscloset.com/Total-War-Warhammer',
|
||||
'info_dict': {
|
||||
'id': 'Total-War-Warhammer',
|
||||
'title': 'Total War: Warhammer - Greenskins',
|
||||
},
|
||||
'playlist_mincount': 33,
|
||||
},
|
||||
]
|
||||
|
||||
def _real_extract(self, url):
|
||||
playlist_id = self._match_id(url)
|
||||
if playlist_id.endswith('/'):
|
||||
playlist_id = playlist_id[:-1]
|
||||
|
||||
webpage = self._download_webpage(url, playlist_id)
|
||||
|
||||
static_assets_base = self._search_regex(r'staticAssetsBase:\"(.*?)\"', webpage, 'staticAssetsBase')
|
||||
static_assets_base = f'https://sovietscloset.com{static_assets_base}'
|
||||
|
||||
sovietscloset = self.parse_nuxt_jsonp(f'{static_assets_base}/payload.js', playlist_id, 'global')['games']
|
||||
|
||||
if '/' in playlist_id:
|
||||
game_slug, category_slug = playlist_id.lower().split('/')
|
||||
else:
|
||||
game_slug = playlist_id.lower()
|
||||
category_slug = 'misc'
|
||||
|
||||
game = next(game for game in sovietscloset if game['slug'].lower() == game_slug)
|
||||
category = next((cat for cat in game['subcategories'] if cat.get('slug', '').lower() == category_slug),
|
||||
game['subcategories'][0])
|
||||
category_slug = category.get('slug', '').lower() or category_slug
|
||||
playlist_title = game.get('name') or game_slug
|
||||
if category_slug != 'misc':
|
||||
playlist_title += f' - {category.get("name") or category_slug}'
|
||||
entries = [{
|
||||
**self.url_result(f'https://sovietscloset.com/video/{stream["id"]}', ie=SovietsClosetIE.ie_key()),
|
||||
**self.video_meta(
|
||||
video_id=stream['id'], game_name=game['name'], category_name=category.get('name'),
|
||||
episode_number=i + 1, stream_date=stream.get('date')),
|
||||
} for i, stream in enumerate(category['streams'])]
|
||||
|
||||
return self.playlist_result(entries, playlist_id, playlist_title)
|
||||
@@ -26,17 +26,18 @@ class SpankBangIE(InfoExtractor):
|
||||
)
|
||||
'''
|
||||
_TESTS = [{
|
||||
'url': 'http://spankbang.com/3vvn/video/fantasy+solo',
|
||||
'md5': '1cc433e1d6aa14bc376535b8679302f7',
|
||||
'url': 'https://spankbang.com/56b3d/video/the+slut+maker+hmv',
|
||||
'md5': '2D13903DE4ECC7895B5D55930741650A',
|
||||
'info_dict': {
|
||||
'id': '3vvn',
|
||||
'id': '56b3d',
|
||||
'ext': 'mp4',
|
||||
'title': 'fantasy solo',
|
||||
'description': 'dillion harper masturbates on a bed',
|
||||
'title': 'The Slut Maker HMV',
|
||||
'description': 'Girls getting converted into cock slaves.',
|
||||
'thumbnail': r're:^https?://.*\.jpg$',
|
||||
'uploader': 'silly2587',
|
||||
'timestamp': 1422571989,
|
||||
'upload_date': '20150129',
|
||||
'uploader': 'Mindself',
|
||||
'uploader_id': 'mindself',
|
||||
'timestamp': 1617109572,
|
||||
'upload_date': '20210330',
|
||||
'age_limit': 18,
|
||||
}
|
||||
}, {
|
||||
@@ -134,15 +135,15 @@ class SpankBangIE(InfoExtractor):
|
||||
info = self._search_json_ld(webpage, video_id, default={})
|
||||
|
||||
title = self._html_search_regex(
|
||||
r'(?s)<h1[^>]*>(.+?)</h1>', webpage, 'title', default=None)
|
||||
r'(?s)<h1[^>]+\btitle=["\']([^"]+)["\']>', webpage, 'title', default=None)
|
||||
description = self._search_regex(
|
||||
r'<div[^>]+\bclass=["\']bottom[^>]+>\s*<p>[^<]*</p>\s*<p>([^<]+)',
|
||||
webpage, 'description', default=None)
|
||||
thumbnail = self._og_search_thumbnail(webpage, default=None)
|
||||
uploader = self._html_search_regex(
|
||||
(r'(?s)<li[^>]+class=["\']profile[^>]+>(.+?)</a>',
|
||||
r'class="user"[^>]*><img[^>]+>([^<]+)'),
|
||||
webpage, 'uploader', default=None)
|
||||
r'<svg[^>]+\bclass="(?:[^"]*?user[^"]*?)">.*?</svg>([^<]+)', webpage, 'uploader', default=None)
|
||||
uploader_id = self._html_search_regex(
|
||||
r'<a[^>]+href="/profile/([^"]+)"', webpage, 'uploader_id', default=None)
|
||||
duration = parse_duration(self._search_regex(
|
||||
r'<div[^>]+\bclass=["\']right_side[^>]+>\s*<span>([^<]+)',
|
||||
webpage, 'duration', default=None))
|
||||
@@ -157,6 +158,7 @@ class SpankBangIE(InfoExtractor):
|
||||
'description': description,
|
||||
'thumbnail': thumbnail,
|
||||
'uploader': uploader,
|
||||
'uploader_id': uploader_id,
|
||||
'duration': duration,
|
||||
'view_count': view_count,
|
||||
'formats': formats,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
|
||||
|
||||
class StreamanityIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?streamanity\.com/video/(?P<id>[A-Za-z0-9]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://streamanity.com/video/9DFPTnuYi8f2',
|
||||
'md5': '6ab171e8d4a02ad5dcbff6bea44cf5a1',
|
||||
'info_dict': {
|
||||
'id': '9DFPTnuYi8f2',
|
||||
'ext': 'mp4',
|
||||
'title': 'Bitcoin vs The Lighting Network',
|
||||
'thumbnail': r're:https://res\.cloudinary\.com/.+\.png',
|
||||
'description': '',
|
||||
'uploader': 'Tom Bombadil (Freddy78)',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://streamanity.com/video/JktOUjSlfzTD',
|
||||
'md5': '31f131e28abd3377c38be586a59532dc',
|
||||
'info_dict': {
|
||||
'id': 'JktOUjSlfzTD',
|
||||
'ext': 'mp4',
|
||||
'title': 'Share data when you see it',
|
||||
'thumbnail': r're:https://res\.cloudinary\.com/.+\.png',
|
||||
'description': 'Reposting as data should be public and stored on blockchain',
|
||||
'uploader': 'digitalcurrencydaily',
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
video_info = self._download_json(
|
||||
f'https://app.streamanity.com/api/video/{video_id}', video_id)['data']['video']
|
||||
|
||||
formats = self._extract_m3u8_formats(
|
||||
f'https://stream.mux.com/{video_info["play_id"]}.m3u8?token={video_info["token"]}',
|
||||
video_id, ext='mp4', m3u8_id='hls')
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': video_info['title'],
|
||||
'description': video_info.get('description'),
|
||||
'uploader': video_info.get('author_name'),
|
||||
'is_live': False,
|
||||
'thumbnail': video_info.get('thumb'),
|
||||
'formats': formats,
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import try_get
|
||||
|
||||
|
||||
class ThetaIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?theta\.tv/(?P<id>[a-z0-9]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.theta.tv/davirus',
|
||||
'skip': 'The live may have ended',
|
||||
'info_dict': {
|
||||
'id': 'DaVirus',
|
||||
'ext': 'mp4',
|
||||
'title': 'I choose you - My Community is King -👀 - YO HABLO ESPANOL - CODE DAVIRUS',
|
||||
'thumbnail': r're:https://live-thumbnails-prod-theta-tv\.imgix\.net/thumbnail/.+\.jpg',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.theta.tv/mst3k',
|
||||
'note': 'This channel is live 24/7',
|
||||
'info_dict': {
|
||||
'id': 'MST3K',
|
||||
'ext': 'mp4',
|
||||
'title': 'Mystery Science Theatre 3000 24/7 Powered by the THETA Network.',
|
||||
'thumbnail': r're:https://user-prod-theta-tv\.imgix\.net/.+\.jpg',
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
channel_id = self._match_id(url)
|
||||
info = self._download_json(f'https://api.theta.tv/v1/channel?alias={channel_id}', channel_id)['body']
|
||||
|
||||
m3u8_playlist = next(
|
||||
data['url'] for data in info['live_stream']['video_urls']
|
||||
if data.get('type') != 'embed' and data.get('resolution') in ('master', 'source'))
|
||||
|
||||
formats = self._extract_m3u8_formats(m3u8_playlist, channel_id, 'mp4', m3u8_id='hls', live=True)
|
||||
self._sort_formats(formats)
|
||||
|
||||
channel = try_get(info, lambda x: x['user']['username']) # using this field instead of channel_id due to capitalization
|
||||
|
||||
return {
|
||||
'id': channel,
|
||||
'title': try_get(info, lambda x: x['live_stream']['title']),
|
||||
'channel': channel,
|
||||
'view_count': try_get(info, lambda x: x['live_stream']['view_count']),
|
||||
'is_live': True,
|
||||
'formats': formats,
|
||||
'thumbnail': try_get(info, lambda x: x['live_stream']['thumbnail_url']),
|
||||
}
|
||||
+258
-48
@@ -2,18 +2,206 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
import json
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
int_or_none,
|
||||
str_or_none,
|
||||
try_get
|
||||
traverse_obj,
|
||||
try_get,
|
||||
qualities,
|
||||
)
|
||||
|
||||
|
||||
class TikTokIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://www\.tiktok\.com/@[\w\._]+/video/(?P<id>\d+)'
|
||||
class TikTokBaseIE(InfoExtractor):
|
||||
_APP_VERSION = '20.9.3'
|
||||
_MANIFEST_APP_VERSION = '291'
|
||||
QUALITIES = ('360p', '540p', '720p')
|
||||
|
||||
def _call_api(self, ep, query, video_id, fatal=True,
|
||||
note='Downloading API JSON', errnote='Unable to download API page'):
|
||||
real_query = {
|
||||
**query,
|
||||
'version_name': self._APP_VERSION,
|
||||
'version_code': self._MANIFEST_APP_VERSION,
|
||||
'build_number': self._APP_VERSION,
|
||||
'manifest_version_code': self._MANIFEST_APP_VERSION,
|
||||
'update_version_code': self._MANIFEST_APP_VERSION,
|
||||
'openudid': ''.join(random.choice('0123456789abcdef') for i in range(16)),
|
||||
'uuid': ''.join([random.choice(string.digits) for num in range(16)]),
|
||||
'_rticket': int(time.time() * 1000),
|
||||
'ts': int(time.time()),
|
||||
'device_brand': 'Google',
|
||||
'device_type': 'Pixel 4',
|
||||
'device_platform': 'android',
|
||||
'resolution': '1080*1920',
|
||||
'dpi': 420,
|
||||
'os_version': '10',
|
||||
'os_api': '29',
|
||||
'carrier_region': 'US',
|
||||
'sys_region': 'US',
|
||||
'region': 'US',
|
||||
'app_name': 'trill',
|
||||
'app_language': 'en',
|
||||
'language': 'en',
|
||||
'timezone_name': 'America/New_York',
|
||||
'timezone_offset': '-14400',
|
||||
'channel': 'googleplay',
|
||||
'ac': 'wifi',
|
||||
'mcc_mnc': '310260',
|
||||
'is_my_cn': 0,
|
||||
'aid': 1180,
|
||||
'ssmix': 'a',
|
||||
'as': 'a1qwert123',
|
||||
'cp': 'cbfhckdckkde1',
|
||||
}
|
||||
self._set_cookie('.tiktokv.com', 'odin_tt', ''.join(random.choice('0123456789abcdef') for i in range(160)))
|
||||
return self._download_json(
|
||||
'https://api-t2.tiktokv.com/aweme/v1/%s/' % ep, video_id=video_id,
|
||||
fatal=fatal, note=note, errnote=errnote, headers={
|
||||
'User-Agent': f'com.ss.android.ugc.trill/{self._MANIFEST_APP_VERSION} (Linux; U; Android 10; en_US; Pixel 4; Build/QQ3A.200805.001; Cronet/58.0.2991.0)',
|
||||
'Accept': 'application/json',
|
||||
}, query=real_query)
|
||||
|
||||
def _parse_aweme_video(self, aweme_detail):
|
||||
aweme_id = aweme_detail['aweme_id']
|
||||
video_info = aweme_detail['video']
|
||||
|
||||
def parse_url_key(url_key):
|
||||
format_id, codec, res, bitrate = self._search_regex(
|
||||
r'v[^_]+_(?P<id>(?P<codec>[^_]+)_(?P<res>\d+p)_(?P<bitrate>\d+))', url_key,
|
||||
'url key', default=(None, None, None, None), group=('id', 'codec', 'res', 'bitrate'))
|
||||
if not format_id:
|
||||
return {}, None
|
||||
return {
|
||||
'format_id': format_id,
|
||||
'vcodec': 'h265' if codec == 'bytevc1' else codec,
|
||||
'tbr': int_or_none(bitrate, scale=1000) or None,
|
||||
'quality': qualities(self.QUALITIES)(res),
|
||||
}, res
|
||||
|
||||
known_resolutions = {}
|
||||
|
||||
def extract_addr(addr, add_meta={}):
|
||||
parsed_meta, res = parse_url_key(addr.get('url_key', ''))
|
||||
if res:
|
||||
known_resolutions.setdefault(res, {}).setdefault('height', add_meta.get('height'))
|
||||
known_resolutions[res].setdefault('width', add_meta.get('width'))
|
||||
parsed_meta.update(known_resolutions.get(res, {}))
|
||||
add_meta.setdefault('height', int_or_none(res[:-1]))
|
||||
return [{
|
||||
'url': url,
|
||||
'filesize': int_or_none(addr.get('data_size')),
|
||||
'ext': 'mp4',
|
||||
'acodec': 'aac',
|
||||
'source_preference': -2 if 'aweme/v1' in url else -1, # Downloads from API might get blocked
|
||||
**add_meta, **parsed_meta,
|
||||
'format_note': ' '.join(filter(None, (
|
||||
add_meta.get('format_note'), '(API)' if 'aweme/v1' in url else '')))
|
||||
} for url in addr.get('url_list') or []]
|
||||
|
||||
# Hack: Add direct video links first to prioritize them when removing duplicate formats
|
||||
formats = []
|
||||
if video_info.get('play_addr'):
|
||||
formats.extend(extract_addr(video_info['play_addr'], {
|
||||
'format_id': 'play_addr',
|
||||
'format_note': 'Direct video',
|
||||
'vcodec': 'h265' if traverse_obj(
|
||||
video_info, 'is_bytevc1', 'is_h265') else 'h264', # Always h264?
|
||||
'width': video_info.get('width'),
|
||||
'height': video_info.get('height'),
|
||||
}))
|
||||
if video_info.get('download_addr'):
|
||||
formats.extend(extract_addr(video_info['download_addr'], {
|
||||
'format_id': 'download_addr',
|
||||
'format_note': 'Download video%s' % (', watermarked' if video_info.get('has_watermark') else ''),
|
||||
'vcodec': 'h264',
|
||||
'width': video_info.get('width'),
|
||||
'height': video_info.get('height'),
|
||||
'preference': -2 if video_info.get('has_watermark') else -1,
|
||||
}))
|
||||
if video_info.get('play_addr_h264'):
|
||||
formats.extend(extract_addr(video_info['play_addr_h264'], {
|
||||
'format_id': 'play_addr_h264',
|
||||
'format_note': 'Direct video',
|
||||
'vcodec': 'h264',
|
||||
}))
|
||||
if video_info.get('play_addr_bytevc1'):
|
||||
formats.extend(extract_addr(video_info['play_addr_bytevc1'], {
|
||||
'format_id': 'play_addr_bytevc1',
|
||||
'format_note': 'Direct video',
|
||||
'vcodec': 'h265',
|
||||
}))
|
||||
|
||||
for bitrate in video_info.get('bit_rate', []):
|
||||
if bitrate.get('play_addr'):
|
||||
formats.extend(extract_addr(bitrate['play_addr'], {
|
||||
'format_id': bitrate.get('gear_name'),
|
||||
'format_note': 'Playback video',
|
||||
'tbr': try_get(bitrate, lambda x: x['bit_rate'] / 1000),
|
||||
'vcodec': 'h265' if traverse_obj(
|
||||
bitrate, 'is_bytevc1', 'is_h265') else 'h264',
|
||||
}))
|
||||
|
||||
self._remove_duplicate_formats(formats)
|
||||
self._sort_formats(formats, ('quality', 'codec', 'size', 'br'))
|
||||
|
||||
thumbnails = []
|
||||
for cover_id in ('cover', 'ai_dynamic_cover', 'animated_cover', 'ai_dynamic_cover_bak',
|
||||
'origin_cover', 'dynamic_cover'):
|
||||
cover = video_info.get(cover_id)
|
||||
if cover:
|
||||
for cover_url in cover['url_list']:
|
||||
thumbnails.append({
|
||||
'id': cover_id,
|
||||
'url': cover_url,
|
||||
})
|
||||
|
||||
stats_info = aweme_detail.get('statistics', {})
|
||||
author_info = aweme_detail.get('author', {})
|
||||
music_info = aweme_detail.get('music', {})
|
||||
user_id = str_or_none(author_info.get('nickname'))
|
||||
|
||||
contained_music_track = traverse_obj(
|
||||
music_info, ('matched_song', 'title'), ('matched_pgc_sound', 'title'), expected_type=str)
|
||||
contained_music_author = traverse_obj(
|
||||
music_info, ('matched_song', 'author'), ('matched_pgc_sound', 'author'), 'author', expected_type=str)
|
||||
|
||||
is_generic_og_trackname = music_info.get('is_original_sound') and music_info.get('title') == 'original sound - %s' % music_info.get('owner_handle')
|
||||
if is_generic_og_trackname:
|
||||
music_track, music_author = contained_music_track or 'original sound', contained_music_author
|
||||
else:
|
||||
music_track, music_author = music_info.get('title'), music_info.get('author')
|
||||
|
||||
return {
|
||||
'id': aweme_id,
|
||||
'title': aweme_detail['desc'],
|
||||
'description': aweme_detail['desc'],
|
||||
'view_count': int_or_none(stats_info.get('play_count')),
|
||||
'like_count': int_or_none(stats_info.get('digg_count')),
|
||||
'repost_count': int_or_none(stats_info.get('share_count')),
|
||||
'comment_count': int_or_none(stats_info.get('comment_count')),
|
||||
'uploader': str_or_none(author_info.get('unique_id')),
|
||||
'creator': user_id,
|
||||
'uploader_id': str_or_none(author_info.get('uid')),
|
||||
'uploader_url': f'https://www.tiktok.com/@{user_id}' if user_id else None,
|
||||
'track': music_track,
|
||||
'album': str_or_none(music_info.get('album')) or None,
|
||||
'artist': music_author,
|
||||
'timestamp': int_or_none(aweme_detail.get('create_time')),
|
||||
'formats': formats,
|
||||
'thumbnails': thumbnails,
|
||||
'duration': int_or_none(traverse_obj(video_info, 'duration', ('download_addr', 'duration')), scale=1000)
|
||||
}
|
||||
|
||||
|
||||
class TikTokIE(TikTokBaseIE):
|
||||
_VALID_URL = r'https?://www\.tiktok\.com/@[\w\.-]+/video/(?P<id>\d+)'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://www.tiktok.com/@leenabhushan/video/6748451240264420610',
|
||||
@@ -61,6 +249,10 @@ class TikTokIE(InfoExtractor):
|
||||
'repost_count': int,
|
||||
'comment_count': int,
|
||||
}
|
||||
}, {
|
||||
# Promoted content/ad
|
||||
'url': 'https://www.tiktok.com/@MS4wLjABAAAAAR29F6J2Ktu0Daw03BJyXPNoRQ-W7U5a0Mn3lVCq2rQhjOd_WNLclHUoFgwX8Eno/video/6932675057474981122',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _extract_aweme(self, props_data, webpage, url):
|
||||
@@ -68,6 +260,8 @@ class TikTokIE(InfoExtractor):
|
||||
props_data, lambda x: x['pageProps']['itemInfo']['itemStruct'], dict)
|
||||
author_info = try_get(
|
||||
props_data, lambda x: x['pageProps']['itemInfo']['itemStruct']['author'], dict) or {}
|
||||
music_info = try_get(
|
||||
props_data, lambda x: x['pageProps']['itemInfo']['itemStruct']['music'], dict) or {}
|
||||
stats_info = try_get(props_data, lambda x: x['pageProps']['itemInfo']['itemStruct']['stats'], dict) or {}
|
||||
|
||||
user_id = str_or_none(author_info.get('uniqueId'))
|
||||
@@ -99,6 +293,9 @@ class TikTokIE(InfoExtractor):
|
||||
'uploader': user_id,
|
||||
'uploader_id': str_or_none(author_info.get('id')),
|
||||
'uploader_url': f'https://www.tiktok.com/@{user_id}',
|
||||
'track': str_or_none(music_info.get('title')),
|
||||
'album': str_or_none(music_info.get('album')) or None,
|
||||
'artist': str_or_none(music_info.get('authorName')),
|
||||
'thumbnails': thumbnails,
|
||||
'description': str_or_none(video_info.get('desc')),
|
||||
'webpage_url': self._og_search_url(webpage),
|
||||
@@ -108,9 +305,19 @@ class TikTokIE(InfoExtractor):
|
||||
}
|
||||
}
|
||||
|
||||
def _extract_aweme_app(self, aweme_id):
|
||||
aweme_detail = self._call_api('aweme/detail', {'aweme_id': aweme_id}, aweme_id,
|
||||
note='Downloading video details', errnote='Unable to download video details')['aweme_detail']
|
||||
return self._parse_aweme_video(aweme_detail)
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
|
||||
try:
|
||||
return self._extract_aweme_app(video_id)
|
||||
except ExtractorError as e:
|
||||
self.report_warning(f'{e}; Retrying with webpage')
|
||||
|
||||
# If we only call once, we get a 403 when downlaoding the video.
|
||||
self._download_webpage(url, video_id)
|
||||
webpage = self._download_webpage(url, video_id, note='Downloading video webpage')
|
||||
@@ -130,76 +337,79 @@ class TikTokIE(InfoExtractor):
|
||||
raise ExtractorError('Video not available', video_id=video_id)
|
||||
|
||||
|
||||
class TikTokUserIE(InfoExtractor):
|
||||
class TikTokUserIE(TikTokBaseIE):
|
||||
IE_NAME = 'tiktok:user'
|
||||
_VALID_URL = r'(?!.*/video/)https?://www\.tiktok\.com/@(?P<id>[\w\._]+)'
|
||||
_VALID_URL = r'https?://(?:www\.)?tiktok\.com/@(?P<id>[\w\.-]+)/?(?:$|[#?])'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.tiktok.com/@corgibobaa?lang=en',
|
||||
'url': 'https://tiktok.com/@corgibobaa?lang=en',
|
||||
'playlist_mincount': 45,
|
||||
'info_dict': {
|
||||
'id': '6935371178089399301',
|
||||
},
|
||||
'skip': 'Cookies (not necessarily logged in) are needed.'
|
||||
}, {
|
||||
'url': 'https://www.tiktok.com/@meme',
|
||||
'playlist_mincount': 593,
|
||||
'info_dict': {
|
||||
'id': '79005827461758976',
|
||||
},
|
||||
'skip': 'Cookies (not necessarily logged in) are needed.'
|
||||
}]
|
||||
|
||||
def _entries(self, url, user_id):
|
||||
webpage = self._download_webpage(url, user_id)
|
||||
own_id = self._search_regex(r'\"id\":\"(?P<userid>\d+)', webpage, user_id, default=None)
|
||||
if not own_id:
|
||||
raise ExtractorError('Cookies (not necessarily logged in) are needed.', expected=True)
|
||||
secuid = self._search_regex(r'\"secUid\":\"(?P<secUid>[^\"]+)', webpage, user_id)
|
||||
r''' # TODO: Fix by adding _signature to api_url
|
||||
def _entries(self, webpage, user_id, username):
|
||||
secuid = self._search_regex(r'\"secUid\":\"(?P<secUid>[^\"]+)', webpage, username)
|
||||
verifyfp_cookie = self._get_cookies('https://www.tiktok.com').get('s_v_web_id')
|
||||
if not verifyfp_cookie:
|
||||
raise ExtractorError('Improper cookies (missing s_v_web_id).', expected=True)
|
||||
api_url = f'https://m.tiktok.com/api/post/item_list/?aid=1988&cookie_enabled=true&count=30&verifyFp={verifyfp_cookie.value}&secUid={secuid}&cursor='
|
||||
cursor = '0'
|
||||
for page in itertools.count():
|
||||
data_json = self._download_json(api_url + cursor, user_id, note='Downloading Page %d' % page)
|
||||
data_json = self._download_json(api_url + cursor, username, note='Downloading Page %d' % page)
|
||||
for video in data_json.get('itemList', []):
|
||||
video_id = video['id']
|
||||
video_url = f'https://www.tiktok.com/@{user_id}/video/{video_id}'
|
||||
download_url = try_get(video, (lambda x: x['video']['playAddr'],
|
||||
lambda x: x['video']['downloadAddr']))
|
||||
thumbnail = try_get(video, lambda x: x['video']['originCover'])
|
||||
height = try_get(video, lambda x: x['video']['height'], int)
|
||||
width = try_get(video, lambda x: x['video']['width'], int)
|
||||
yield {
|
||||
'id': video_id,
|
||||
'ie_key': TikTokIE.ie_key(),
|
||||
'extractor': 'TikTok',
|
||||
'url': download_url,
|
||||
'ext': 'mp4',
|
||||
'height': height,
|
||||
'width': width,
|
||||
'title': str_or_none(video.get('desc')),
|
||||
'duration': try_get(video, lambda x: x['video']['duration'], int),
|
||||
'view_count': try_get(video, lambda x: x['stats']['playCount'], int),
|
||||
'like_count': try_get(video, lambda x: x['stats']['diggCount'], int),
|
||||
'comment_count': try_get(video, lambda x: x['stats']['commentCount'], int),
|
||||
'repost_count': try_get(video, lambda x: x['stats']['shareCount'], int),
|
||||
'timestamp': video.get('createTime'),
|
||||
'creator': try_get(video, lambda x: x['author']['nickname'], str),
|
||||
'uploader': try_get(video, lambda x: x['author']['uniqueId'], str),
|
||||
'uploader_id': try_get(video, lambda x: x['author']['id'], str),
|
||||
'uploader_url': f'https://www.tiktok.com/@{user_id}',
|
||||
'thumbnails': [{'url': thumbnail, 'height': height, 'width': width}],
|
||||
'description': str_or_none(video.get('desc')),
|
||||
'webpage_url': video_url,
|
||||
'http_headers': {
|
||||
'Referer': video_url,
|
||||
}
|
||||
}
|
||||
if not data_json['hasMore']:
|
||||
yield self._url_result(video_url, 'TikTok', video_id, str_or_none(video.get('desc')))
|
||||
if not data_json.get('hasMore'):
|
||||
break
|
||||
cursor = data_json['cursor']
|
||||
'''
|
||||
|
||||
def _entries_api(self, webpage, user_id, username):
|
||||
query = {
|
||||
'user_id': user_id,
|
||||
'count': 21,
|
||||
'max_cursor': 0,
|
||||
'min_cursor': 0,
|
||||
'retry_type': 'no_retry',
|
||||
'device_id': ''.join(random.choice(string.digits) for i in range(19)), # Some endpoints don't like randomized device_id, so it isn't directly set in _call_api.
|
||||
}
|
||||
|
||||
max_retries = self.get_param('extractor_retries', 3)
|
||||
for page in itertools.count(1):
|
||||
for retries in itertools.count():
|
||||
try:
|
||||
post_list = self._call_api('aweme/post', query, username,
|
||||
note='Downloading user video list page %d%s' % (page, f' (attempt {retries})' if retries != 0 else ''),
|
||||
errnote='Unable to download user video list')
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, json.JSONDecodeError) and e.cause.pos == 0 and retries != max_retries:
|
||||
self.report_warning('%s. Retrying...' % str(e.cause or e.msg))
|
||||
continue
|
||||
raise
|
||||
break
|
||||
for video in post_list.get('aweme_list', []):
|
||||
yield {
|
||||
**self._parse_aweme_video(video),
|
||||
'ie_key': TikTokIE.ie_key(),
|
||||
'extractor': 'TikTok',
|
||||
}
|
||||
if not post_list.get('has_more'):
|
||||
break
|
||||
query['max_cursor'] = post_list['max_cursor']
|
||||
|
||||
def _real_extract(self, url):
|
||||
user_id = self._match_id(url)
|
||||
return self.playlist_result(self._entries(url, user_id), user_id)
|
||||
webpage = self._download_webpage(url, user_id, headers={
|
||||
'User-Agent': 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)'
|
||||
})
|
||||
own_id = self._html_search_regex(r'snssdk\d*://user/profile/(\d+)', webpage, 'user ID')
|
||||
return self.playlist_result(self._entries_api(webpage, own_id, user_id), user_id)
|
||||
|
||||
+34
-15
@@ -47,10 +47,24 @@ class VideaIE(InfoExtractor):
|
||||
},
|
||||
}, {
|
||||
'url': 'http://videa.hu/videok/origo/jarmuvek/supercars-elozes-jAHDWfWSJH5XuFhH',
|
||||
'only_matching': True,
|
||||
'md5': 'd57ccd8812c7fd491d33b1eab8c99975',
|
||||
'info_dict': {
|
||||
'id': 'jAHDWfWSJH5XuFhH',
|
||||
'ext': 'mp4',
|
||||
'title': 'Supercars előzés',
|
||||
'thumbnail': r're:^https?://.*',
|
||||
'duration': 64,
|
||||
},
|
||||
}, {
|
||||
'url': 'http://videa.hu/player?v=8YfIAjxwWGwT8HVQ',
|
||||
'only_matching': True,
|
||||
'md5': '97a7af41faeaffd9f1fc864a7c7e7603',
|
||||
'info_dict': {
|
||||
'id': '8YfIAjxwWGwT8HVQ',
|
||||
'ext': 'mp4',
|
||||
'title': 'Az őrült kígyász 285 kígyót enged szabadon',
|
||||
'thumbnail': r're:^https?://.*',
|
||||
'duration': 21,
|
||||
},
|
||||
}, {
|
||||
'url': 'http://videa.hu/player/v/8YfIAjxwWGwT8HVQ?autoplay=1',
|
||||
'only_matching': True,
|
||||
@@ -100,10 +114,14 @@ class VideaIE(InfoExtractor):
|
||||
|
||||
video_page = self._download_webpage(url, video_id)
|
||||
|
||||
player_url = self._search_regex(
|
||||
r'<iframe.*?src="(/player\?[^"]+)"', video_page, 'player url')
|
||||
player_url = urljoin(url, player_url)
|
||||
player_page = self._download_webpage(player_url, video_id)
|
||||
if 'videa.hu/player' in url:
|
||||
player_url = url
|
||||
player_page = video_page
|
||||
else:
|
||||
player_url = self._search_regex(
|
||||
r'<iframe.*?src="(/player\?[^"]+)"', video_page, 'player url')
|
||||
player_url = urljoin(url, player_url)
|
||||
player_page = self._download_webpage(player_url, video_id)
|
||||
|
||||
nonce = self._search_regex(
|
||||
r'_xt\s*=\s*"([^"]+)"', player_page, 'nonce')
|
||||
@@ -134,7 +152,7 @@ class VideaIE(InfoExtractor):
|
||||
sources = xpath_element(
|
||||
info, './video_sources', 'sources', fatal=True)
|
||||
hash_values = xpath_element(
|
||||
info, './hash_values', 'hash values', fatal=True)
|
||||
info, './hash_values', 'hash values', fatal=False)
|
||||
|
||||
title = xpath_text(video, './title', fatal=True)
|
||||
|
||||
@@ -143,15 +161,16 @@ class VideaIE(InfoExtractor):
|
||||
source_url = source.text
|
||||
source_name = source.get('name')
|
||||
source_exp = source.get('exp')
|
||||
if not (source_url and source_name and source_exp):
|
||||
if not (source_url and source_name):
|
||||
continue
|
||||
hash_value = xpath_text(hash_values, 'hash_value_' + source_name)
|
||||
if not hash_value:
|
||||
continue
|
||||
source_url = update_url_query(source_url, {
|
||||
'md5': hash_value,
|
||||
'expires': source_exp,
|
||||
})
|
||||
hash_value = None
|
||||
if hash_values:
|
||||
hash_value = xpath_text(hash_values, 'hash_value_' + source_name)
|
||||
if hash_value and source_exp:
|
||||
source_url = update_url_query(source_url, {
|
||||
'md5': hash_value,
|
||||
'expires': source_exp,
|
||||
})
|
||||
f = parse_codecs(source.get('codecs'))
|
||||
f.update({
|
||||
'url': self._proto_relative_url(source_url),
|
||||
|
||||
@@ -218,7 +218,7 @@ class VRVIE(VRVBaseIE):
|
||||
})
|
||||
|
||||
thumbnails = []
|
||||
for thumbnail in traverse_obj(video_data, ('images', 'thumbnail', ..., ...)):
|
||||
for thumbnail in traverse_obj(video_data, ('images', 'thumbnail', ..., ...)) or []:
|
||||
thumbnail_url = thumbnail.get('source')
|
||||
if not thumbnail_url:
|
||||
continue
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
determine_ext,
|
||||
@@ -142,3 +145,88 @@ class YandexVideoIE(InfoExtractor):
|
||||
'release_year': int_or_none(content.get('release_year')),
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
|
||||
class ZenYandexIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://zen\.yandex\.ru/media/(?:id/[^/]+/|[^/]+/)(?:[a-z0-9-]+)-(?P<id>[a-z0-9-]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://zen.yandex.ru/media/popmech/izverjenie-vulkana-iz-spichek-zreliscnyi-opyt-6002240ff8b1af50bb2da5e3',
|
||||
'info_dict': {
|
||||
'id': '6002240ff8b1af50bb2da5e3',
|
||||
'ext': 'mp4',
|
||||
'title': 'Извержение вулкана из спичек: зрелищный опыт',
|
||||
'description': 'md5:053ad3c61b5596d510c9a199dc8ee633',
|
||||
'thumbnail': 'https://avatars.mds.yandex.net/get-zen-pub-og/3558619/pub_6002240ff8b1af50bb2da5e3_600bad814d953e4132a30b5e/orig',
|
||||
'uploader': 'Популярная механика',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://zen.yandex.ru/media/id/606fd806cc13cb3c58c05cf5/vot-eto-focus-dedy-morozy-na-gidrociklah-60c7c443da18892ebfe85ed7',
|
||||
'info_dict': {
|
||||
'id': '60c7c443da18892ebfe85ed7',
|
||||
'ext': 'mp4',
|
||||
'title': 'ВОТ ЭТО Focus. Деды Морозы на гидроциклах',
|
||||
'description': 'md5:8684912f6086f298f8078d4af0e8a600',
|
||||
'thumbnail': 'https://avatars.mds.yandex.net/get-zen-pub-og/4410519/pub_60c7c443da18892ebfe85ed7_60c7c48e060a163121f42cc3/orig',
|
||||
'uploader': 'AcademeG DailyStream'
|
||||
},
|
||||
}, {
|
||||
'url': 'https://zen.yandex.ru/media/id/606fd806cc13cb3c58c05cf5/novyi-samsung-fold-3-moskvich-barahlit-612f93b7f8d48e7e945792a2?from=channel&rid=2286618386.482.1630817595976.42360',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, id)
|
||||
data_json = self._parse_json(self._search_regex(r'w\._data\s?=\s?({.+?});', webpage, 'metadata'), id)
|
||||
stream_json = try_get(data_json, lambda x: x['publication']['content']['gifContent'], dict)
|
||||
stream_url = stream_json.get('stream') or try_get(stream_json, lambda x: x['streams']['url'])
|
||||
formats = self._extract_m3u8_formats(stream_url, id)
|
||||
self._sort_formats(formats)
|
||||
return {
|
||||
'id': id,
|
||||
'title': try_get(data_json, (lambda x: x['og']['title'], lambda x: x['publication']['content']['preview']['title'])),
|
||||
'uploader': data_json.get('authorName') or try_get(data_json, lambda x: x['publisher']['name']),
|
||||
'description': try_get(data_json, lambda x: x['og']['description']),
|
||||
'thumbnail': try_get(data_json, lambda x: x['og']['imageUrl']),
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
|
||||
class ZenYandexChannelIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://zen\.yandex\.ru/(?!media)(?:id/)?(?P<id>[a-z0-9-_]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://zen.yandex.ru/tok_media',
|
||||
'info_dict': {
|
||||
'id': 'tok_media',
|
||||
},
|
||||
'playlist_mincount': 169,
|
||||
}, {
|
||||
'url': 'https://zen.yandex.ru/id/606fd806cc13cb3c58c05cf5',
|
||||
'info_dict': {
|
||||
'id': '606fd806cc13cb3c58c05cf5',
|
||||
},
|
||||
'playlist_mincount': 657,
|
||||
}]
|
||||
|
||||
def _entries(self, id, url):
|
||||
webpage = self._download_webpage(url, id)
|
||||
data_json = self._parse_json(re.findall(r'var\s?data\s?=\s?({.+?})\s?;', webpage)[-1], id)
|
||||
for key in data_json.keys():
|
||||
if key.startswith('__serverState__'):
|
||||
data_json = data_json[key]
|
||||
items = list(try_get(data_json, lambda x: x['feed']['items'], dict).values())
|
||||
more = try_get(data_json, lambda x: x['links']['more']) or None
|
||||
for page in itertools.count(1):
|
||||
for item in items:
|
||||
video_id = item.get('publication_id') or item.get('publicationId')
|
||||
video_url = item.get('link')
|
||||
yield self.url_result(video_url, ie=ZenYandexIE.ie_key(), video_id=video_id.split(':')[-1])
|
||||
if not more:
|
||||
break
|
||||
data_json = self._download_json(more, id, note='Downloading Page %d' % page)
|
||||
items = data_json.get('items', [])
|
||||
more = try_get(data_json, lambda x: x['more']['link']) or None
|
||||
|
||||
def _real_extract(self, url):
|
||||
id = self._match_id(url)
|
||||
return self.playlist_result(self._entries(id, url), playlist_id=id)
|
||||
|
||||
+140
-103
@@ -48,6 +48,7 @@ from ..utils import (
|
||||
parse_iso8601,
|
||||
parse_qs,
|
||||
qualities,
|
||||
remove_end,
|
||||
remove_start,
|
||||
smuggle_url,
|
||||
str_or_none,
|
||||
@@ -116,6 +117,7 @@ INNERTUBE_CLIENTS = {
|
||||
}
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 3,
|
||||
'REQUIRE_JS_PLAYER': False
|
||||
},
|
||||
'android_embedded': {
|
||||
'INNERTUBE_API_KEY': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
|
||||
@@ -125,7 +127,8 @@ INNERTUBE_CLIENTS = {
|
||||
'clientVersion': '16.20',
|
||||
},
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 55
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 55,
|
||||
'REQUIRE_JS_PLAYER': False
|
||||
},
|
||||
'android_music': {
|
||||
'INNERTUBE_API_KEY': 'AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30',
|
||||
@@ -137,6 +140,7 @@ INNERTUBE_CLIENTS = {
|
||||
}
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 21,
|
||||
'REQUIRE_JS_PLAYER': False
|
||||
},
|
||||
'android_creator': {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
@@ -145,7 +149,8 @@ INNERTUBE_CLIENTS = {
|
||||
'clientVersion': '21.24.100',
|
||||
},
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 14
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 14,
|
||||
'REQUIRE_JS_PLAYER': False
|
||||
},
|
||||
# ios has HLS live streams
|
||||
# See: https://github.com/TeamNewPipe/NewPipeExtractor/issues/680
|
||||
@@ -157,7 +162,8 @@ INNERTUBE_CLIENTS = {
|
||||
'clientVersion': '16.20',
|
||||
}
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 5
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 5,
|
||||
'REQUIRE_JS_PLAYER': False
|
||||
},
|
||||
'ios_embedded': {
|
||||
'INNERTUBE_API_KEY': 'AIzaSyDCU8hByM-4DrUqRUYnGn-3llEO78bcxq8',
|
||||
@@ -167,7 +173,8 @@ INNERTUBE_CLIENTS = {
|
||||
'clientVersion': '16.20',
|
||||
},
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 66
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 66,
|
||||
'REQUIRE_JS_PLAYER': False
|
||||
},
|
||||
'ios_music': {
|
||||
'INNERTUBE_API_KEY': 'AIzaSyDK3iBpDP9nHVTk2qL73FLJICfOC3c51Og',
|
||||
@@ -178,7 +185,8 @@ INNERTUBE_CLIENTS = {
|
||||
'clientVersion': '4.32',
|
||||
},
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 26
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 26,
|
||||
'REQUIRE_JS_PLAYER': False
|
||||
},
|
||||
'ios_creator': {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
@@ -187,7 +195,8 @@ INNERTUBE_CLIENTS = {
|
||||
'clientVersion': '21.24.100',
|
||||
},
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 15
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 15,
|
||||
'REQUIRE_JS_PLAYER': False
|
||||
},
|
||||
# mweb has 'ultralow' formats
|
||||
# See: https://github.com/yt-dlp/yt-dlp/pull/557
|
||||
@@ -214,6 +223,7 @@ def build_innertube_clients():
|
||||
for client, ytcfg in tuple(INNERTUBE_CLIENTS.items()):
|
||||
ytcfg.setdefault('INNERTUBE_API_KEY', 'AIzaSyDCU8hByM-4DrUqRUYnGn-3llEO78bcxq8')
|
||||
ytcfg.setdefault('INNERTUBE_HOST', 'www.youtube.com')
|
||||
ytcfg.setdefault('REQUIRE_JS_PLAYER', True)
|
||||
ytcfg['INNERTUBE_CONTEXT']['client'].setdefault('hl', 'en')
|
||||
ytcfg['priority'] = 10 * priority(client.split('_', 1)[0])
|
||||
|
||||
@@ -236,7 +246,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
"""Provide base functions for Youtube extractors"""
|
||||
|
||||
_RESERVED_NAMES = (
|
||||
r'channel|c|user|playlist|watch|w|v|embed|e|watch_popup|'
|
||||
r'channel|c|user|playlist|watch|w|v|embed|e|watch_popup|clip|'
|
||||
r'shorts|movies|results|shared|hashtag|trending|feed|feeds|'
|
||||
r'browse|oembed|get_video_info|iframe_api|s/player|'
|
||||
r'storefront|oops|index|account|reporthistory|t/terms|about|upload|signin|logout')
|
||||
@@ -498,13 +508,6 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
ytcfg, (lambda x: x['INNERTUBE_CLIENT_NAME'],
|
||||
lambda x: x['INNERTUBE_CONTEXT']['client']['clientName']), compat_str, default_client)
|
||||
|
||||
@staticmethod
|
||||
def _extract_session_index(*data):
|
||||
for ytcfg in data:
|
||||
session_index = int_or_none(try_get(ytcfg, lambda x: x['SESSION_INDEX']))
|
||||
if session_index is not None:
|
||||
return session_index
|
||||
|
||||
def _extract_client_version(self, ytcfg, default_client='web'):
|
||||
return self._ytcfg_get_safe(
|
||||
ytcfg, (lambda x: x['INNERTUBE_CLIENT_VERSION'],
|
||||
@@ -583,17 +586,27 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
self._YT_INITIAL_DATA_RE), webpage, 'yt initial data'),
|
||||
video_id)
|
||||
|
||||
def _extract_identity_token(self, webpage, item_id):
|
||||
if not webpage:
|
||||
return None
|
||||
ytcfg = self.extract_ytcfg(item_id, webpage)
|
||||
@staticmethod
|
||||
def _extract_session_index(*data):
|
||||
"""
|
||||
Index of current account in account list.
|
||||
See: https://github.com/yt-dlp/yt-dlp/pull/519
|
||||
"""
|
||||
for ytcfg in data:
|
||||
session_index = int_or_none(try_get(ytcfg, lambda x: x['SESSION_INDEX']))
|
||||
if session_index is not None:
|
||||
return session_index
|
||||
|
||||
# Deprecated?
|
||||
def _extract_identity_token(self, ytcfg=None, webpage=None):
|
||||
if ytcfg:
|
||||
token = try_get(ytcfg, lambda x: x['ID_TOKEN'], compat_str)
|
||||
if token:
|
||||
return token
|
||||
return self._search_regex(
|
||||
r'\bID_TOKEN["\']\s*:\s*["\'](.+?)["\']', webpage,
|
||||
'identity token', default=None)
|
||||
if webpage:
|
||||
return self._search_regex(
|
||||
r'\bID_TOKEN["\']\s*:\s*["\'](.+?)["\']', webpage,
|
||||
'identity token', default=None, fatal=False)
|
||||
|
||||
@staticmethod
|
||||
def _extract_account_syncid(*args):
|
||||
@@ -614,6 +627,10 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
# and just "user_syncid||" for primary channel. We only want the channel_syncid
|
||||
return sync_ids[0]
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return bool(self._generate_sapisidhash_header())
|
||||
|
||||
def extract_ytcfg(self, video_id, webpage):
|
||||
if not webpage:
|
||||
return {}
|
||||
@@ -623,33 +640,30 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
default='{}'), video_id, fatal=False) or {}
|
||||
|
||||
def generate_api_headers(
|
||||
self, ytcfg=None, identity_token=None, account_syncid=None,
|
||||
visitor_data=None, api_hostname=None, default_client='web', session_index=None):
|
||||
self, *, ytcfg=None, account_syncid=None, session_index=None,
|
||||
visitor_data=None, identity_token=None, api_hostname=None, default_client='web'):
|
||||
|
||||
origin = 'https://' + (api_hostname if api_hostname else self._get_innertube_host(default_client))
|
||||
headers = {
|
||||
'X-YouTube-Client-Name': compat_str(
|
||||
self._ytcfg_get_safe(ytcfg, lambda x: x['INNERTUBE_CONTEXT_CLIENT_NAME'], default_client=default_client)),
|
||||
'X-YouTube-Client-Version': self._extract_client_version(ytcfg, default_client),
|
||||
'Origin': origin
|
||||
}
|
||||
if not visitor_data and ytcfg:
|
||||
visitor_data = try_get(
|
||||
'Origin': origin,
|
||||
'X-Youtube-Identity-Token': identity_token or self._extract_identity_token(ytcfg),
|
||||
'X-Goog-PageId': account_syncid or self._extract_account_syncid(ytcfg),
|
||||
'X-Goog-Visitor-Id': visitor_data or try_get(
|
||||
self._extract_context(ytcfg, default_client), lambda x: x['client']['visitorData'], compat_str)
|
||||
if identity_token:
|
||||
headers['X-Youtube-Identity-Token'] = identity_token
|
||||
if account_syncid:
|
||||
headers['X-Goog-PageId'] = account_syncid
|
||||
if session_index is None and ytcfg:
|
||||
}
|
||||
if session_index is None:
|
||||
session_index = self._extract_session_index(ytcfg)
|
||||
if account_syncid or session_index is not None:
|
||||
headers['X-Goog-AuthUser'] = session_index if session_index is not None else 0
|
||||
if visitor_data:
|
||||
headers['X-Goog-Visitor-Id'] = visitor_data
|
||||
|
||||
auth = self._generate_sapisidhash_header(origin)
|
||||
if auth is not None:
|
||||
headers['Authorization'] = auth
|
||||
headers['X-Origin'] = origin
|
||||
return headers
|
||||
return {h: v for h, v in headers.items() if v is not None}
|
||||
|
||||
@staticmethod
|
||||
def _build_api_continuation_query(continuation, ctp=None):
|
||||
@@ -720,7 +734,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
if message:
|
||||
yield alert_type, message
|
||||
|
||||
def _report_alerts(self, alerts, expected=True, fatal=True):
|
||||
def _report_alerts(self, alerts, expected=True, fatal=True, only_once=False):
|
||||
errors = []
|
||||
warnings = []
|
||||
for alert_type, alert_message in alerts:
|
||||
@@ -730,7 +744,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
warnings.append([alert_type, alert_message])
|
||||
|
||||
for alert_type, alert_message in (warnings + errors[:-1]):
|
||||
self.report_warning('YouTube said: %s - %s' % (alert_type, alert_message))
|
||||
self.report_warning('YouTube said: %s - %s' % (alert_type, alert_message), only_once=only_once)
|
||||
if errors:
|
||||
raise ExtractorError('YouTube said: %s' % errors[-1][1], expected=expected)
|
||||
|
||||
@@ -779,7 +793,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
while count < retries:
|
||||
count += 1
|
||||
if last_error:
|
||||
self.report_warning('%s. Retrying ...' % last_error)
|
||||
self.report_warning('%s. Retrying ...' % remove_end(last_error, '.'))
|
||||
try:
|
||||
response = self._call_api(
|
||||
ep=ep, fatal=True, headers=headers,
|
||||
@@ -802,7 +816,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
# We also want to catch all other network exceptions since errors in later pages can be troublesome
|
||||
# See https://github.com/yt-dlp/yt-dlp/issues/507#issuecomment-880188210
|
||||
if not isinstance(e.cause, compat_HTTPError) or e.cause.code not in (403, 429):
|
||||
last_error = error_to_compat_str(e.cause or e)
|
||||
last_error = error_to_compat_str(e.cause or e.msg)
|
||||
if count < retries:
|
||||
continue
|
||||
if fatal:
|
||||
@@ -814,8 +828,13 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
else:
|
||||
# Youtube may send alerts if there was an issue with the continuation page
|
||||
try:
|
||||
self._extract_and_report_alerts(response, expected=False)
|
||||
self._extract_and_report_alerts(response, expected=False, only_once=True)
|
||||
except ExtractorError as e:
|
||||
# YouTube servers may return errors we want to retry on in a 200 OK response
|
||||
# See: https://github.com/yt-dlp/yt-dlp/issues/839
|
||||
if 'unknown error' in e.msg.lower():
|
||||
last_error = e.msg
|
||||
continue
|
||||
if fatal:
|
||||
raise
|
||||
self.report_warning(error_to_compat_str(e))
|
||||
@@ -854,7 +873,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
'_type': 'url',
|
||||
'ie_key': YoutubeIE.ie_key(),
|
||||
'id': video_id,
|
||||
'url': video_id,
|
||||
'url': f'https://www.youtube.com/watch?v={video_id}',
|
||||
'title': title,
|
||||
'description': description,
|
||||
'duration': duration,
|
||||
@@ -1852,14 +1871,12 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
self._code_cache = {}
|
||||
self._player_cache = {}
|
||||
|
||||
def _extract_player_url(self, ytcfg=None, webpage=None):
|
||||
player_url = try_get(ytcfg, (lambda x: x['PLAYER_JS_URL']), str)
|
||||
if not player_url and webpage:
|
||||
player_url = self._search_regex(
|
||||
r'"(?:PLAYER_JS_URL|jsUrl)"\s*:\s*"([^"]+)"',
|
||||
webpage, 'player URL', fatal=False)
|
||||
def _extract_player_url(self, *ytcfgs, webpage=None):
|
||||
player_url = traverse_obj(
|
||||
ytcfgs, (..., 'PLAYER_JS_URL'), (..., 'WEB_PLAYER_CONTEXT_CONFIGS', ..., 'jsUrl'),
|
||||
get_all=False, expected_type=compat_str)
|
||||
if not player_url:
|
||||
return None
|
||||
return
|
||||
if player_url.startswith('//'):
|
||||
player_url = 'https:' + player_url
|
||||
elif not re.match(r'https?://', player_url):
|
||||
@@ -1867,6 +1884,16 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'https://www.youtube.com', player_url)
|
||||
return player_url
|
||||
|
||||
def _download_player_url(self, video_id, fatal=False):
|
||||
res = self._download_webpage(
|
||||
'https://www.youtube.com/iframe_api',
|
||||
note='Downloading iframe API JS', video_id=video_id, fatal=fatal)
|
||||
if res:
|
||||
player_version = self._search_regex(
|
||||
r'player\\?/([0-9a-fA-F]{8})\\?/', res, 'player version', fatal=fatal)
|
||||
if player_version:
|
||||
return f'https://www.youtube.com/s/player/{player_version}/player_ias.vflset/en_US/base.js'
|
||||
|
||||
def _signature_cache_id(self, example_sig):
|
||||
""" Return a string representation of a signature """
|
||||
return '.'.join(compat_str(len(part)) for part in example_sig.split('.'))
|
||||
@@ -2201,8 +2228,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'parent': parent or 'root'
|
||||
}
|
||||
|
||||
def _comment_entries(self, root_continuation_data, identity_token, account_syncid,
|
||||
ytcfg, video_id, parent=None, comment_counts=None):
|
||||
def _comment_entries(self, root_continuation_data, ytcfg, video_id, parent=None, comment_counts=None):
|
||||
|
||||
def extract_header(contents):
|
||||
_total_comments = 0
|
||||
@@ -2260,8 +2286,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
if comment_replies_renderer:
|
||||
comment_counts[2] += 1
|
||||
comment_entries_iter = self._comment_entries(
|
||||
comment_replies_renderer, identity_token, account_syncid, ytcfg,
|
||||
video_id, parent=comment.get('id'), comment_counts=comment_counts)
|
||||
comment_replies_renderer, ytcfg, video_id,
|
||||
parent=comment.get('id'), comment_counts=comment_counts)
|
||||
|
||||
for reply_comment in comment_entries_iter:
|
||||
yield reply_comment
|
||||
@@ -2286,7 +2312,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
for page_num in itertools.count(0):
|
||||
if not continuation:
|
||||
break
|
||||
headers = self.generate_api_headers(ytcfg, identity_token, account_syncid, visitor_data)
|
||||
headers = self.generate_api_headers(ytcfg=ytcfg, visitor_data=visitor_data)
|
||||
comment_prog_str = '(%d/%d)' % (comment_counts[0], comment_counts[1])
|
||||
if page_num == 0:
|
||||
if is_first_continuation:
|
||||
@@ -2386,18 +2412,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
def _extract_comments(self, ytcfg, video_id, contents, webpage):
|
||||
"""Entry for comment extraction"""
|
||||
def _real_comment_extract(contents):
|
||||
if isinstance(contents, list):
|
||||
for entry in contents:
|
||||
for key, renderer in entry.items():
|
||||
if key not in known_entry_comment_renderers:
|
||||
continue
|
||||
yield from self._comment_entries(
|
||||
renderer, video_id=video_id, ytcfg=ytcfg,
|
||||
identity_token=self._extract_identity_token(webpage, item_id=video_id),
|
||||
account_syncid=self._extract_account_syncid(ytcfg))
|
||||
break
|
||||
yield from self._comment_entries(
|
||||
traverse_obj(contents, (..., 'itemSectionRenderer'), get_all=False), ytcfg, video_id)
|
||||
|
||||
comments = []
|
||||
known_entry_comment_renderers = ('itemSectionRenderer',)
|
||||
estimated_total = 0
|
||||
max_comments = int_or_none(self._configuration_arg('max_comments', [''])[0]) or float('inf')
|
||||
# Force English regardless of account setting to prevent parsing issues
|
||||
@@ -2422,7 +2440,11 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _generate_player_context(sts=None):
|
||||
def _get_checkok_params():
|
||||
return {'contentCheckOk': True, 'racyCheckOk': True}
|
||||
|
||||
@classmethod
|
||||
def _generate_player_context(cls, sts=None):
|
||||
context = {
|
||||
'html5Preference': 'HTML5_PREF_WANTS',
|
||||
}
|
||||
@@ -2432,8 +2454,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'playbackContext': {
|
||||
'contentPlaybackContext': context
|
||||
},
|
||||
'contentCheckOk': True,
|
||||
'racyCheckOk': True
|
||||
**cls._get_checkok_params()
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -2452,14 +2473,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
def _is_unplayable(player_response):
|
||||
return traverse_obj(player_response, ('playabilityStatus', 'status')) == 'UNPLAYABLE'
|
||||
|
||||
def _extract_player_response(self, client, video_id, master_ytcfg, player_ytcfg, identity_token, player_url, initial_pr):
|
||||
def _extract_player_response(self, client, video_id, master_ytcfg, player_ytcfg, player_url, initial_pr):
|
||||
|
||||
session_index = self._extract_session_index(player_ytcfg, master_ytcfg)
|
||||
syncid = self._extract_account_syncid(player_ytcfg, master_ytcfg, initial_pr)
|
||||
sts = self._extract_signature_timestamp(video_id, player_url, master_ytcfg, fatal=False)
|
||||
sts = self._extract_signature_timestamp(video_id, player_url, master_ytcfg, fatal=False) if player_url else None
|
||||
headers = self.generate_api_headers(
|
||||
player_ytcfg, identity_token, syncid,
|
||||
default_client=client, session_index=session_index)
|
||||
ytcfg=player_ytcfg, account_syncid=syncid, session_index=session_index, default_client=client)
|
||||
|
||||
yt_query = {'videoId': video_id}
|
||||
yt_query.update(self._generate_player_context(sts))
|
||||
@@ -2501,7 +2521,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
webpage = self._download_webpage(url, video_id, fatal=False, note=f'Downloading {client} config')
|
||||
return self.extract_ytcfg(video_id, webpage) or {}
|
||||
|
||||
def _extract_player_responses(self, clients, video_id, webpage, master_ytcfg, player_url, identity_token):
|
||||
def _extract_player_responses(self, clients, video_id, webpage, master_ytcfg):
|
||||
initial_pr = None
|
||||
if webpage:
|
||||
initial_pr = self._extract_yt_initial_variable(
|
||||
@@ -2510,6 +2530,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
|
||||
original_clients = clients
|
||||
clients = clients[::-1]
|
||||
prs = []
|
||||
|
||||
def append_client(client_name):
|
||||
if client_name in INNERTUBE_CLIENTS and client_name not in original_clients:
|
||||
@@ -2519,23 +2540,33 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
# extraction of some data. So we return the initial_pr with formats
|
||||
# stripped out even if not requested by the user
|
||||
# See: https://github.com/yt-dlp/yt-dlp/issues/501
|
||||
yielded_pr = False
|
||||
if initial_pr:
|
||||
pr = dict(initial_pr)
|
||||
pr['streamingData'] = None
|
||||
yielded_pr = True
|
||||
yield pr
|
||||
prs.append(pr)
|
||||
|
||||
last_error = None
|
||||
tried_iframe_fallback = False
|
||||
player_url = None
|
||||
while clients:
|
||||
client = clients.pop()
|
||||
player_ytcfg = master_ytcfg if client == 'web' else {}
|
||||
if 'configs' not in self._configuration_arg('player_skip'):
|
||||
player_ytcfg = self._extract_player_ytcfg(client, video_id) or player_ytcfg
|
||||
|
||||
player_url = player_url or self._extract_player_url(master_ytcfg, player_ytcfg, webpage=webpage)
|
||||
require_js_player = self._get_default_ytcfg(client).get('REQUIRE_JS_PLAYER')
|
||||
if 'js' in self._configuration_arg('player_skip'):
|
||||
require_js_player = False
|
||||
player_url = None
|
||||
|
||||
if not player_url and not tried_iframe_fallback and require_js_player:
|
||||
player_url = self._download_player_url(video_id)
|
||||
tried_iframe_fallback = True
|
||||
|
||||
try:
|
||||
pr = initial_pr if client == 'web' and initial_pr else self._extract_player_response(
|
||||
client, video_id, player_ytcfg or master_ytcfg, player_ytcfg, identity_token, player_url, initial_pr)
|
||||
client, video_id, player_ytcfg or master_ytcfg, player_ytcfg, player_url if require_js_player else None, initial_pr)
|
||||
except ExtractorError as e:
|
||||
if last_error:
|
||||
self.report_warning(last_error)
|
||||
@@ -2543,19 +2574,19 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
continue
|
||||
|
||||
if pr:
|
||||
yielded_pr = True
|
||||
yield pr
|
||||
prs.append(pr)
|
||||
|
||||
# creator clients can bypass AGE_VERIFICATION_REQUIRED if logged in
|
||||
if client.endswith('_agegate') and self._is_unplayable(pr) and self._generate_sapisidhash_header():
|
||||
if client.endswith('_agegate') and self._is_unplayable(pr) and self.is_authenticated:
|
||||
append_client(client.replace('_agegate', '_creator'))
|
||||
elif self._is_agegated(pr):
|
||||
append_client(f'{client}_agegate')
|
||||
|
||||
if last_error:
|
||||
if not yielded_pr:
|
||||
if not len(prs):
|
||||
raise last_error
|
||||
self.report_warning(last_error)
|
||||
return prs, player_url
|
||||
|
||||
def _extract_formats(self, streaming_data, video_id, player_url, is_live):
|
||||
itags, stream_ids = [], []
|
||||
@@ -2702,16 +2733,16 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
|
||||
base_url = self.http_scheme() + '//www.youtube.com/'
|
||||
webpage_url = base_url + 'watch?v=' + video_id
|
||||
webpage = self._download_webpage(
|
||||
webpage_url + '&bpctr=9999999999&has_verified=1', video_id, fatal=False)
|
||||
webpage = None
|
||||
if 'webpage' not in self._configuration_arg('player_skip'):
|
||||
webpage = self._download_webpage(
|
||||
webpage_url + '&bpctr=9999999999&has_verified=1', video_id, fatal=False)
|
||||
|
||||
master_ytcfg = self.extract_ytcfg(video_id, webpage) or self._get_default_ytcfg()
|
||||
player_url = self._extract_player_url(master_ytcfg, webpage)
|
||||
identity_token = self._extract_identity_token(webpage, video_id)
|
||||
|
||||
player_responses = list(self._extract_player_responses(
|
||||
player_responses, player_url = self._extract_player_responses(
|
||||
self._get_requested_clients(url, smuggled_data),
|
||||
video_id, webpage, master_ytcfg, player_url, identity_token))
|
||||
video_id, webpage, master_ytcfg)
|
||||
|
||||
get_first = lambda obj, keys, **kwargs: traverse_obj(obj, (..., *variadic(keys)), **kwargs, get_all=False)
|
||||
|
||||
@@ -3024,13 +3055,12 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
webpage, self._YT_INITIAL_DATA_RE, video_id,
|
||||
'yt initial data')
|
||||
if not initial_data:
|
||||
headers = self.generate_api_headers(
|
||||
master_ytcfg, identity_token, self._extract_account_syncid(master_ytcfg),
|
||||
session_index=self._extract_session_index(master_ytcfg))
|
||||
|
||||
query = {'videoId': video_id}
|
||||
query.update(self._get_checkok_params())
|
||||
initial_data = self._extract_response(
|
||||
item_id=video_id, ep='next', fatal=False,
|
||||
ytcfg=master_ytcfg, headers=headers, query={'videoId': video_id},
|
||||
ytcfg=master_ytcfg, query=query,
|
||||
headers=self.generate_api_headers(ytcfg=master_ytcfg),
|
||||
note='Downloading initial data API JSON')
|
||||
|
||||
try:
|
||||
@@ -3802,7 +3832,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
if entry:
|
||||
yield entry
|
||||
'''
|
||||
def _entries(self, tab, item_id, identity_token, account_syncid, ytcfg):
|
||||
def _entries(self, tab, item_id, account_syncid, ytcfg):
|
||||
|
||||
def extract_entries(parent_renderer): # this needs to called again for continuation to work with feeds
|
||||
contents = try_get(parent_renderer, lambda x: x['contents'], list) or []
|
||||
@@ -3859,7 +3889,8 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
for page_num in itertools.count(1):
|
||||
if not continuation:
|
||||
break
|
||||
headers = self.generate_api_headers(ytcfg, identity_token, account_syncid, visitor_data)
|
||||
headers = self.generate_api_headers(
|
||||
ytcfg=ytcfg, account_syncid=account_syncid, visitor_data=visitor_data)
|
||||
response = self._extract_response(
|
||||
item_id='%s page %s' % (item_id, page_num),
|
||||
query=continuation, headers=headers, ytcfg=ytcfg,
|
||||
@@ -4013,7 +4044,6 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
return self.playlist_result(
|
||||
self._entries(
|
||||
selected_tab, playlist_id,
|
||||
self._extract_identity_token(webpage, item_id),
|
||||
self._extract_account_syncid(ytcfg, data), ytcfg),
|
||||
**metadata)
|
||||
|
||||
@@ -4021,8 +4051,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
first_id = last_id = None
|
||||
ytcfg = self.extract_ytcfg(playlist_id, webpage)
|
||||
headers = self.generate_api_headers(
|
||||
ytcfg, account_syncid=self._extract_account_syncid(ytcfg, data),
|
||||
identity_token=self._extract_identity_token(webpage, item_id=playlist_id))
|
||||
ytcfg=ytcfg, account_syncid=self._extract_account_syncid(ytcfg, data))
|
||||
for page_num in itertools.count(1):
|
||||
videos = list(self._playlist_entries(playlist))
|
||||
if not videos:
|
||||
@@ -4138,10 +4167,8 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
|
||||
ytcfg = self.extract_ytcfg(item_id, webpage)
|
||||
headers = self.generate_api_headers(
|
||||
ytcfg, account_syncid=self._extract_account_syncid(ytcfg, data),
|
||||
identity_token=self._extract_identity_token(webpage, item_id=item_id),
|
||||
visitor_data=try_get(
|
||||
self._extract_context(ytcfg), lambda x: x['client']['visitorData'], compat_str))
|
||||
ytcfg=ytcfg, account_syncid=self._extract_account_syncid(ytcfg, data),
|
||||
visitor_data=try_get(self._extract_context(ytcfg), lambda x: x['client']['visitorData'], compat_str))
|
||||
query = {
|
||||
'params': params or 'wgYCCAA=',
|
||||
'browseId': browse_id or 'VL%s' % item_id
|
||||
@@ -4249,7 +4276,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
if video_id and playlist_id:
|
||||
if self.get_param('noplaylist'):
|
||||
self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
|
||||
return self.url_result(video_id, ie=YoutubeIE.ie_key(), video_id=video_id)
|
||||
return self.url_result(f'https://www.youtube.com/watch?v={video_id}', ie=YoutubeIE.ie_key(), video_id=video_id)
|
||||
self.to_screen('Downloading playlist %s; add --no-playlist to just download video %s' % (playlist_id, video_id))
|
||||
|
||||
webpage, data = self._extract_webpage(url, item_id)
|
||||
@@ -4285,7 +4312,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
# YouTube sometimes provides a button to reload playlist with unavailable videos.
|
||||
if 'no-youtube-unavailable-videos' not in compat_opts:
|
||||
data = self._reload_with_unavailable_videos(item_id, data, webpage) or data
|
||||
self._extract_and_report_alerts(data)
|
||||
self._extract_and_report_alerts(data, only_once=True)
|
||||
tabs = try_get(
|
||||
data, lambda x: x['contents']['twoColumnBrowseResultsRenderer']['tabs'], list)
|
||||
if tabs:
|
||||
@@ -4302,7 +4329,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
if video_id:
|
||||
if mobj['tab'] != '/live': # live tab is expected to redirect to video
|
||||
self.report_warning('Unable to recognize playlist. Downloading just video %s' % video_id)
|
||||
return self.url_result(video_id, ie=YoutubeIE.ie_key(), video_id=video_id)
|
||||
return self.url_result(f'https://www.youtube.com/watch?v={video_id}', ie=YoutubeIE.ie_key(), video_id=video_id)
|
||||
|
||||
raise ExtractorError('Unable to recognize tab page')
|
||||
|
||||
@@ -4692,6 +4719,16 @@ class YoutubeTruncatedURLIE(InfoExtractor):
|
||||
expected=True)
|
||||
|
||||
|
||||
class YoutubeClipIE(InfoExtractor):
|
||||
IE_NAME = 'youtube:clip'
|
||||
IE_DESC = False # Do not list
|
||||
_VALID_URL = r'https?://(?:www\.)?youtube\.com/clip/'
|
||||
|
||||
def _real_extract(self, url):
|
||||
self.report_warning('YouTube clips are not currently supported. The entire video will be downloaded instead')
|
||||
return self.url_result(url, 'Generic')
|
||||
|
||||
|
||||
class YoutubeTruncatedIDIE(InfoExtractor):
|
||||
IE_NAME = 'youtube:truncated_id'
|
||||
IE_DESC = False # Do not list
|
||||
|
||||
+22
-20
@@ -14,6 +14,7 @@ from ..utils import (
|
||||
orderedSet,
|
||||
parse_codecs,
|
||||
qualities,
|
||||
str_or_none,
|
||||
try_get,
|
||||
unified_timestamp,
|
||||
update_url_query,
|
||||
@@ -49,35 +50,35 @@ class ZDFBaseIE(InfoExtractor):
|
||||
|
||||
def _extract_format(self, video_id, formats, format_urls, meta):
|
||||
format_url = url_or_none(meta.get('url'))
|
||||
if not format_url:
|
||||
return
|
||||
if format_url in format_urls:
|
||||
if not format_url or format_url in format_urls:
|
||||
return
|
||||
format_urls.add(format_url)
|
||||
mime_type = meta.get('mimeType')
|
||||
ext = determine_ext(format_url)
|
||||
|
||||
mime_type, ext = meta.get('mimeType'), determine_ext(format_url)
|
||||
if mime_type == 'application/x-mpegURL' or ext == 'm3u8':
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
new_formats = self._extract_m3u8_formats(
|
||||
format_url, video_id, 'mp4', m3u8_id='hls',
|
||||
entry_protocol='m3u8_native', fatal=False))
|
||||
entry_protocol='m3u8_native', fatal=False)
|
||||
elif mime_type == 'application/f4m+xml' or ext == 'f4m':
|
||||
formats.extend(self._extract_f4m_formats(
|
||||
update_url_query(format_url, {'hdcore': '3.7.0'}), video_id, f4m_id='hds', fatal=False))
|
||||
new_formats = self._extract_f4m_formats(
|
||||
update_url_query(format_url, {'hdcore': '3.7.0'}), video_id, f4m_id='hds', fatal=False)
|
||||
else:
|
||||
f = parse_codecs(meta.get('mimeCodec'))
|
||||
format_id = ['http']
|
||||
for p in (meta.get('type'), meta.get('quality')):
|
||||
if p and isinstance(p, compat_str):
|
||||
format_id.append(p)
|
||||
if not f and meta.get('type'):
|
||||
data = meta['type'].split('_')
|
||||
if try_get(data, lambda x: x[2]) == ext:
|
||||
f = {'vcodec': data[0], 'acodec': data[1]}
|
||||
f.update({
|
||||
'url': format_url,
|
||||
'format_id': '-'.join(format_id),
|
||||
'format_note': meta.get('quality'),
|
||||
'language': meta.get('language'),
|
||||
'quality': qualities(self._QUALITIES)(meta.get('quality')),
|
||||
'preference': -10,
|
||||
'format_id': '-'.join(filter(str_or_none, ('http', meta.get('type'), meta.get('quality')))),
|
||||
})
|
||||
formats.append(f)
|
||||
new_formats = [f]
|
||||
formats.extend(merge_dicts(f, {
|
||||
'format_note': ', '.join(filter(None, (meta.get('quality'), meta.get('class')))),
|
||||
'language': meta.get('language'),
|
||||
'language_preference': 10 if meta.get('class') == 'main' else -10 if meta.get('class') == 'ad' else -1,
|
||||
'quality': qualities(self._QUALITIES)(meta.get('quality')),
|
||||
}) for f in new_formats)
|
||||
|
||||
def _extract_ptmd(self, ptmd_url, video_id, api_token, referrer):
|
||||
ptmd = self._call_api(
|
||||
@@ -106,9 +107,10 @@ class ZDFBaseIE(InfoExtractor):
|
||||
'type': f.get('type'),
|
||||
'mimeType': f.get('mimeType'),
|
||||
'quality': quality.get('quality'),
|
||||
'class': track.get('class'),
|
||||
'language': track.get('language'),
|
||||
})
|
||||
self._sort_formats(formats)
|
||||
self._sort_formats(formats, ('hasaud', 'res', 'quality', 'language_preference'))
|
||||
|
||||
duration = float_or_none(try_get(
|
||||
ptmd, lambda x: x['attributes']['duration']['value']), scale=1000)
|
||||
|
||||
@@ -56,6 +56,8 @@ class ZypeIE(InfoExtractor):
|
||||
video = response['video']
|
||||
title = video['title']
|
||||
|
||||
subtitles = {}
|
||||
|
||||
if isinstance(body, dict):
|
||||
formats = []
|
||||
for output in body.get('outputs', []):
|
||||
@@ -64,7 +66,7 @@ class ZypeIE(InfoExtractor):
|
||||
continue
|
||||
name = output.get('name')
|
||||
if name == 'm3u8':
|
||||
formats = self._extract_m3u8_formats(
|
||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
||||
output_url, video_id, 'mp4',
|
||||
'm3u8_native', m3u8_id='hls', fatal=False)
|
||||
else:
|
||||
@@ -97,7 +99,7 @@ class ZypeIE(InfoExtractor):
|
||||
|
||||
if get_attr('integration') == 'verizon-media':
|
||||
m3u8_url = 'https://content.uplynk.com/%s.m3u8' % get_attr('id')
|
||||
formats = self._extract_m3u8_formats(
|
||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
||||
m3u8_url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls')
|
||||
text_tracks = self._search_regex(
|
||||
r'textTracks\s*:\s*(\[[^]]+\])',
|
||||
@@ -107,7 +109,6 @@ class ZypeIE(InfoExtractor):
|
||||
text_tracks, video_id, js_to_json, False)
|
||||
self._sort_formats(formats)
|
||||
|
||||
subtitles = {}
|
||||
if text_tracks:
|
||||
for text_track in text_tracks:
|
||||
tt_url = dict_get(text_track, ('file', 'src'))
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import os
|
||||
|
||||
from threading import Lock
|
||||
from .utils import compat_os_name, get_windows_version
|
||||
|
||||
|
||||
class MultilinePrinterBase():
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.end()
|
||||
|
||||
def print_at_line(self, text, pos):
|
||||
pass
|
||||
|
||||
def end(self):
|
||||
pass
|
||||
|
||||
|
||||
class MultilinePrinter(MultilinePrinterBase):
|
||||
|
||||
def __init__(self, stream, lines):
|
||||
"""
|
||||
@param stream stream to write to
|
||||
@lines number of lines to be written
|
||||
"""
|
||||
self.stream = stream
|
||||
|
||||
is_win10 = compat_os_name == 'nt' and get_windows_version() >= (10, )
|
||||
self.CARRIAGE_RETURN = '\r'
|
||||
if os.getenv('TERM') and self._isatty() or is_win10:
|
||||
# reason not to use curses https://github.com/yt-dlp/yt-dlp/pull/1036#discussion_r713851492
|
||||
# escape sequences for Win10 https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
|
||||
self.UP = '\x1b[A'
|
||||
self.DOWN = '\n'
|
||||
self.ERASE_LINE = '\x1b[K'
|
||||
self._HAVE_FULLCAP = self._isatty() or is_win10
|
||||
else:
|
||||
self.UP = self.DOWN = self.ERASE_LINE = None
|
||||
self._HAVE_FULLCAP = False
|
||||
|
||||
# lines are numbered from top to bottom, counting from 0 to self.maximum
|
||||
self.maximum = lines - 1
|
||||
self.lastline = 0
|
||||
self.lastlength = 0
|
||||
|
||||
self.movelock = Lock()
|
||||
|
||||
@property
|
||||
def have_fullcap(self):
|
||||
"""
|
||||
True if the TTY is allowing to control cursor,
|
||||
so that multiline progress works
|
||||
"""
|
||||
return self._HAVE_FULLCAP
|
||||
|
||||
def _isatty(self):
|
||||
try:
|
||||
return self.stream.isatty()
|
||||
except BaseException:
|
||||
return False
|
||||
|
||||
def _move_cursor(self, dest):
|
||||
current = min(self.lastline, self.maximum)
|
||||
self.stream.write(self.CARRIAGE_RETURN)
|
||||
if current == dest:
|
||||
# current and dest are at same position, no need to move cursor
|
||||
return
|
||||
elif current > dest:
|
||||
# when maximum == 2,
|
||||
# 0. dest
|
||||
# 1.
|
||||
# 2. current
|
||||
self.stream.write(self.UP * (current - dest))
|
||||
elif current < dest:
|
||||
# when maximum == 2,
|
||||
# 0. current
|
||||
# 1.
|
||||
# 2. dest
|
||||
self.stream.write(self.DOWN * (dest - current))
|
||||
self.lastline = dest
|
||||
|
||||
def print_at_line(self, text, pos):
|
||||
with self.movelock:
|
||||
if self.have_fullcap:
|
||||
self._move_cursor(pos)
|
||||
self.stream.write(self.ERASE_LINE)
|
||||
self.stream.write(text)
|
||||
else:
|
||||
if self.maximum != 0:
|
||||
# let user know about which line is updating the status
|
||||
text = f'{pos + 1}: {text}'
|
||||
textlen = len(text)
|
||||
if self.lastline == pos:
|
||||
# move cursor at the start of progress when writing to same line
|
||||
self.stream.write(self.CARRIAGE_RETURN)
|
||||
if self.lastlength > textlen:
|
||||
text += ' ' * (self.lastlength - textlen)
|
||||
self.lastlength = textlen
|
||||
else:
|
||||
# otherwise, break the line
|
||||
self.stream.write('\n')
|
||||
self.lastlength = 0
|
||||
self.stream.write(text)
|
||||
self.lastline = pos
|
||||
|
||||
def end(self):
|
||||
with self.movelock:
|
||||
# move cursor to the end of the last line, and write line break
|
||||
# so that other to_screen calls can precede
|
||||
self._move_cursor(self.maximum)
|
||||
self.stream.write('\n')
|
||||
|
||||
|
||||
class QuietMultilinePrinter(MultilinePrinterBase):
|
||||
def __init__(self):
|
||||
self.have_fullcap = True
|
||||
|
||||
|
||||
class BreaklineStatusPrinter(MultilinePrinterBase):
|
||||
|
||||
def __init__(self, stream, lines):
|
||||
"""
|
||||
@param stream stream to write to
|
||||
"""
|
||||
self.stream = stream
|
||||
self.maximum = lines
|
||||
self.have_fullcap = True
|
||||
|
||||
def print_at_line(self, text, pos):
|
||||
if self.maximum != 0:
|
||||
# let user know about which line is updating the status
|
||||
text = f'{pos + 1}: {text}'
|
||||
self.stream.write(text + '\n')
|
||||
+55
-59
@@ -116,19 +116,19 @@ def parseOpts(overrideArguments=None):
|
||||
|
||||
return ''.join(opts)
|
||||
|
||||
def _list_from_options_callback(option, opt_str, value, parser, append=True, delim=','):
|
||||
def _list_from_options_callback(option, opt_str, value, parser, append=True, delim=',', process=str.strip):
|
||||
# append can be True, False or -1 (prepend)
|
||||
current = getattr(parser.values, option.dest) if append else []
|
||||
value = [value] if delim is None else value.split(delim)
|
||||
value = list(filter(None, [process(value)] if delim is None else map(process, value.split(delim))))
|
||||
setattr(
|
||||
parser.values, option.dest,
|
||||
current + value if append is True else value + current)
|
||||
|
||||
def _set_from_options_callback(
|
||||
option, opt_str, value, parser,
|
||||
delim=',', allowed_values=None, process=str.lower, aliases={}):
|
||||
option, opt_str, value, parser, delim=',', allowed_values=None, aliases={},
|
||||
process=lambda x: x.lower().strip()):
|
||||
current = getattr(parser.values, option.dest)
|
||||
values = [process(value)] if delim is None else process(value).split(delim)[::-1]
|
||||
values = [process(value)] if delim is None else list(map(process, value.split(delim)[::-1]))
|
||||
while values:
|
||||
actual_val = val = values.pop()
|
||||
if val == 'all':
|
||||
@@ -206,9 +206,13 @@ def parseOpts(overrideArguments=None):
|
||||
action='store_true', dest='update_self',
|
||||
help='Update this program to latest version. Make sure that you have sufficient permissions (run with sudo if needed)')
|
||||
general.add_option(
|
||||
'-i', '--ignore-errors', '--no-abort-on-error',
|
||||
action='store_true', dest='ignoreerrors', default=None,
|
||||
help='Continue on download errors, for example to skip unavailable videos in a playlist (default) (Alias: --no-abort-on-error)')
|
||||
'-i', '--ignore-errors',
|
||||
action='store_true', dest='ignoreerrors',
|
||||
help='Ignore download and postprocessing errors. The download will be considered successfull even if the postprocessing fails')
|
||||
general.add_option(
|
||||
'--no-abort-on-error',
|
||||
action='store_const', dest='ignoreerrors', const='only_download',
|
||||
help='Continue with next video on download errors; e.g. to skip unavailable videos in a playlist (default)')
|
||||
general.add_option(
|
||||
'--abort-on-error', '--no-ignore-errors',
|
||||
action='store_false', dest='ignoreerrors',
|
||||
@@ -235,7 +239,7 @@ def parseOpts(overrideArguments=None):
|
||||
help='Use this prefix for unqualified URLs. For example "gvsearch2:" downloads two videos from google videos for the search term "large apple". Use the value "auto" to let yt-dlp guess ("auto_warning" to emit a warning when guessing). "error" just throws an error. The default value "fixup_error" repairs broken URLs, but emits an error if this is not possible instead of searching')
|
||||
general.add_option(
|
||||
'--ignore-config', '--no-config',
|
||||
action='store_true',
|
||||
action='store_true', dest='ignoreconfig',
|
||||
help=(
|
||||
'Disable loading any configuration files except the one provided by --config-location. '
|
||||
'When given inside a configuration file, no further configuration files are loaded. '
|
||||
@@ -275,8 +279,7 @@ def parseOpts(overrideArguments=None):
|
||||
'multistreams', 'no-live-chat', 'playlist-index', 'list-formats', 'no-direct-merge',
|
||||
'no-youtube-channel-redirect', 'no-youtube-unavailable-videos', 'no-attach-info-json',
|
||||
'embed-thumbnail-atomicparsley', 'seperate-video-versions', 'no-clean-infojson', 'no-keep-subs',
|
||||
},
|
||||
'aliases': {
|
||||
}, 'aliases': {
|
||||
'youtube-dl': ['-multistreams', 'all'],
|
||||
'youtube-dlc': ['-no-youtube-channel-redirect', '-no-live-chat', 'all'],
|
||||
}
|
||||
@@ -479,6 +482,10 @@ def parseOpts(overrideArguments=None):
|
||||
'-n', '--netrc',
|
||||
action='store_true', dest='usenetrc', default=False,
|
||||
help='Use .netrc authentication data')
|
||||
authentication.add_option(
|
||||
'--netrc-location',
|
||||
dest='netrc_location', metavar='PATH',
|
||||
help='Location of .netrc authentication data; either the path or its containing directory. Defaults to ~/.netrc')
|
||||
authentication.add_option(
|
||||
'--video-password',
|
||||
dest='videopassword', metavar='PASSWORD',
|
||||
@@ -1105,7 +1112,7 @@ def parseOpts(overrideArguments=None):
|
||||
'The comments are fetched even without this option if the extraction is known to be quick (Alias: --get-comments)'))
|
||||
filesystem.add_option(
|
||||
'--no-write-comments', '--no-get-comments',
|
||||
action='store_true', dest='getcomments', default=False,
|
||||
action='store_false', dest='getcomments',
|
||||
help='Do not retrieve video comments unless the extraction is known to be quick (Alias: --no-get-comments)')
|
||||
filesystem.add_option(
|
||||
'--load-info-json', '--load-info',
|
||||
@@ -1128,7 +1135,7 @@ def parseOpts(overrideArguments=None):
|
||||
'You can specify the user profile name or directory using '
|
||||
'"BROWSER:PROFILE_NAME" or "BROWSER:PROFILE_PATH". '
|
||||
'If no profile is given, the most recently accessed one is used'.format(
|
||||
'|'.join(sorted(SUPPORTED_BROWSERS)))))
|
||||
', '.join(sorted(SUPPORTED_BROWSERS)))))
|
||||
filesystem.add_option(
|
||||
'--no-cookies-from-browser',
|
||||
action='store_const', const=None, dest='cookiesfrombrowser',
|
||||
@@ -1394,7 +1401,7 @@ def parseOpts(overrideArguments=None):
|
||||
'SponsorBlock categories to create chapters for, separated by commas. '
|
||||
'Available categories are all, %s. You can prefix the category with a "-" to exempt it. '
|
||||
'See https://wiki.sponsor.ajay.app/index.php/Segment_Categories for description of the categories. '
|
||||
'Eg: --sponsorblock-query all,-preview' % ', '.join(SponsorBlockPP.CATEGORIES.keys())))
|
||||
'Eg: --sponsorblock-mark all,-preview' % ', '.join(SponsorBlockPP.CATEGORIES.keys())))
|
||||
sponsorblock.add_option(
|
||||
'--sponsorblock-remove', metavar='CATS',
|
||||
dest='sponsorblock_remove', default=set(), action='callback', type='str',
|
||||
@@ -1421,7 +1428,7 @@ def parseOpts(overrideArguments=None):
|
||||
|
||||
sponsorblock.add_option(
|
||||
'--sponskrub',
|
||||
action='store_true', dest='sponskrub', default=None,
|
||||
action='store_true', dest='sponskrub', default=False,
|
||||
help=optparse.SUPPRESS_HELP)
|
||||
sponsorblock.add_option(
|
||||
'--no-sponskrub',
|
||||
@@ -1533,57 +1540,47 @@ def parseOpts(overrideArguments=None):
|
||||
'command-line': compat_conf(sys.argv[1:]),
|
||||
'custom': [], 'home': [], 'portable': [], 'user': [], 'system': []}
|
||||
paths = {'command-line': False}
|
||||
opts, args = parser.parse_args(configs['command-line'])
|
||||
|
||||
def read_options(name, path, user=False):
|
||||
''' loads config files and returns ignoreconfig '''
|
||||
# Multiple package names can be given here
|
||||
# Eg: ('yt-dlp', 'youtube-dlc', 'youtube-dl') will look for
|
||||
# the configuration file of any of these three packages
|
||||
for package in ('yt-dlp',):
|
||||
if user:
|
||||
config, current_path = _readUserConf(package, default=None)
|
||||
else:
|
||||
current_path = os.path.join(path, '%s.conf' % package)
|
||||
config = _readOptions(current_path, default=None)
|
||||
if config is not None:
|
||||
configs[name], paths[name] = config, current_path
|
||||
return parser.parse_args(config)[0].ignoreconfig
|
||||
return False
|
||||
|
||||
def get_configs():
|
||||
if '--config-location' in configs['command-line']:
|
||||
opts, _ = parser.parse_args(configs['command-line'])
|
||||
if opts.config_location is not None:
|
||||
location = compat_expanduser(opts.config_location)
|
||||
if os.path.isdir(location):
|
||||
location = os.path.join(location, 'yt-dlp.conf')
|
||||
if not os.path.exists(location):
|
||||
parser.error('config-location %s does not exist.' % location)
|
||||
configs['custom'] = _readOptions(location, default=None)
|
||||
if configs['custom'] is None:
|
||||
configs['custom'] = []
|
||||
else:
|
||||
paths['custom'] = location
|
||||
if '--ignore-config' in configs['command-line']:
|
||||
config = _readOptions(location, default=None)
|
||||
if config:
|
||||
configs['custom'], paths['config'] = config, location
|
||||
|
||||
if opts.ignoreconfig:
|
||||
return
|
||||
if '--ignore-config' in configs['custom']:
|
||||
if parser.parse_args(configs['custom'])[0].ignoreconfig:
|
||||
return
|
||||
|
||||
def read_options(path, user=False):
|
||||
# Multiple package names can be given here
|
||||
# Eg: ('yt-dlp', 'youtube-dlc', 'youtube-dl') will look for
|
||||
# the configuration file of any of these three packages
|
||||
for package in ('yt-dlp',):
|
||||
if user:
|
||||
config, current_path = _readUserConf(package, default=None)
|
||||
else:
|
||||
current_path = os.path.join(path, '%s.conf' % package)
|
||||
config = _readOptions(current_path, default=None)
|
||||
if config is not None:
|
||||
return config, current_path
|
||||
return [], None
|
||||
|
||||
configs['portable'], paths['portable'] = read_options(get_executable_path())
|
||||
if '--ignore-config' in configs['portable']:
|
||||
if read_options('portable', get_executable_path()):
|
||||
return
|
||||
|
||||
def get_home_path():
|
||||
opts = parser.parse_args(configs['portable'] + configs['custom'] + configs['command-line'])[0]
|
||||
return expand_path(opts.paths.get('home', '')).strip()
|
||||
|
||||
configs['home'], paths['home'] = read_options(get_home_path())
|
||||
if '--ignore-config' in configs['home']:
|
||||
opts, _ = parser.parse_args(configs['portable'] + configs['custom'] + configs['command-line'])
|
||||
if read_options('home', expand_path(opts.paths.get('home', '')).strip()):
|
||||
return
|
||||
|
||||
configs['system'], paths['system'] = read_options('/etc')
|
||||
if '--ignore-config' in configs['system']:
|
||||
if read_options('system', '/etc'):
|
||||
return
|
||||
|
||||
configs['user'], paths['user'] = read_options('', True)
|
||||
if '--ignore-config' in configs['user']:
|
||||
if read_options('user', None, user=True):
|
||||
configs['system'], paths['system'] = [], None
|
||||
|
||||
get_configs()
|
||||
@@ -1592,10 +1589,9 @@ def parseOpts(overrideArguments=None):
|
||||
if opts.verbose:
|
||||
for label in ('System', 'User', 'Portable', 'Home', 'Custom', 'Command-line'):
|
||||
key = label.lower()
|
||||
if paths.get(key) is None:
|
||||
continue
|
||||
if paths[key]:
|
||||
write_string('[debug] %s config file: %s\n' % (label, paths[key]))
|
||||
write_string('[debug] %s config: %s\n' % (label, repr(_hide_login_info(configs[key]))))
|
||||
if paths.get(key):
|
||||
write_string(f'[debug] {label} config file: {paths[key]}\n')
|
||||
if paths.get(key) is not None:
|
||||
write_string(f'[debug] {label} config: {_hide_login_info(configs[key])!r}\n')
|
||||
|
||||
return parser, opts, args
|
||||
|
||||
@@ -52,6 +52,7 @@ class PostProcessor(object):
|
||||
return self._downloader.report_warning(text, *args, **kwargs)
|
||||
|
||||
def report_error(self, text, *args, **kwargs):
|
||||
# Exists only for compatibility. Do not use
|
||||
if self._downloader:
|
||||
return self._downloader.report_error(text, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from ..utils import (
|
||||
dfxp2srt,
|
||||
encodeArgument,
|
||||
encodeFilename,
|
||||
float_or_none,
|
||||
get_exe_version,
|
||||
is_outdated_version,
|
||||
ISO639Utils,
|
||||
@@ -233,6 +234,23 @@ class FFmpegPostProcessor(PostProcessor):
|
||||
None)
|
||||
return num, len(streams)
|
||||
|
||||
def _get_real_video_duration(self, info, fatal=True):
|
||||
try:
|
||||
if '_real_duration' not in info:
|
||||
info['_real_duration'] = float_or_none(
|
||||
traverse_obj(self.get_metadata_object(info['filepath']), ('format', 'duration')))
|
||||
if not info['_real_duration']:
|
||||
raise PostProcessingError('ffprobe returned empty duration')
|
||||
except PostProcessingError as e:
|
||||
if fatal:
|
||||
raise PostProcessingError(f'Unable to determine video duration; {e}')
|
||||
return info.setdefault('_real_duration', None)
|
||||
|
||||
def _duration_mismatch(self, d1, d2):
|
||||
if not d1 or not d2:
|
||||
return None
|
||||
return abs(d1 - d2) > 1
|
||||
|
||||
def run_ffmpeg_multiple_files(self, input_paths, out_path, opts, **kwargs):
|
||||
return self.real_run_ffmpeg(
|
||||
[(path, []) for path in input_paths],
|
||||
@@ -270,8 +288,7 @@ class FFmpegPostProcessor(PostProcessor):
|
||||
stdout, stderr = process_communicate_or_kill(p)
|
||||
if p.returncode not in variadic(expected_retcodes):
|
||||
stderr = stderr.decode('utf-8', 'replace').strip()
|
||||
if self.get_param('verbose', False):
|
||||
self.report_error(stderr)
|
||||
self.write_debug(stderr)
|
||||
raise FFmpegPostProcessorError(stderr.split('\n')[-1])
|
||||
for out_path, _ in output_path_opts:
|
||||
if out_path:
|
||||
@@ -528,6 +545,10 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
|
||||
return [], information
|
||||
|
||||
filename = information['filepath']
|
||||
if self._duration_mismatch(
|
||||
self._get_real_video_duration(information, False), information['duration']):
|
||||
self.to_screen(f'Skipping {self.pp_key()} since the real and expected durations mismatch')
|
||||
return [], information
|
||||
|
||||
ext = information['ext']
|
||||
sub_langs, sub_names, sub_filenames = [], [], []
|
||||
@@ -708,9 +729,13 @@ class FFmpegMergerPP(FFmpegPostProcessor):
|
||||
filename = info['filepath']
|
||||
temp_filename = prepend_extension(filename, 'temp')
|
||||
args = ['-c', 'copy']
|
||||
audio_streams = 0
|
||||
for (i, fmt) in enumerate(info['requested_formats']):
|
||||
if fmt.get('acodec') != 'none':
|
||||
args.extend(['-map', '%u:a:0' % (i)])
|
||||
args.extend(['-map', f'{i}:a:0'])
|
||||
if self.get_audio_codec(fmt['filepath']) == 'aac':
|
||||
args.extend([f'-bsf:a:{audio_streams}', 'aac_adtstoasc'])
|
||||
audio_streams += 1
|
||||
if fmt.get('vcodec') != 'none':
|
||||
args.extend(['-map', '%u:v:0' % (i)])
|
||||
self.to_screen('Merging formats into "%s"' % filename)
|
||||
|
||||
@@ -9,15 +9,13 @@ from .ffmpeg import (
|
||||
)
|
||||
from .sponsorblock import SponsorBlockPP
|
||||
from ..utils import (
|
||||
float_or_none,
|
||||
orderedSet,
|
||||
PostProcessingError,
|
||||
prepend_extension,
|
||||
traverse_obj,
|
||||
)
|
||||
|
||||
|
||||
_TINY_SPONSOR_OVERLAP_DURATION = 1
|
||||
_TINY_CHAPTER_DURATION = 1
|
||||
DEFAULT_SPONSORBLOCK_CHAPTER_TITLE = '[SponsorBlock]: %(category_names)l'
|
||||
|
||||
|
||||
@@ -37,7 +35,7 @@ class ModifyChaptersPP(FFmpegPostProcessor):
|
||||
if not chapters and not sponsor_chapters:
|
||||
return [], info
|
||||
|
||||
real_duration = self._get_real_video_duration(info['filepath'])
|
||||
real_duration = self._get_real_video_duration(info)
|
||||
if not chapters:
|
||||
chapters = [{'start_time': 0, 'end_time': real_duration, 'title': info['title']}]
|
||||
|
||||
@@ -45,14 +43,13 @@ class ModifyChaptersPP(FFmpegPostProcessor):
|
||||
if not cuts:
|
||||
return [], info
|
||||
|
||||
if abs(real_duration - info['duration']) > 1:
|
||||
if abs(real_duration - info['chapters'][-1]['end_time']) < 1:
|
||||
if self._duration_mismatch(real_duration, info.get('duration')):
|
||||
if not self._duration_mismatch(real_duration, info['chapters'][-1]['end_time']):
|
||||
self.to_screen(f'Skipping {self.pp_key()} since the video appears to be already cut')
|
||||
return [], info
|
||||
if not info.get('__real_download'):
|
||||
raise PostProcessingError('Cannot cut video since the real and expected durations mismatch. '
|
||||
'Different chapters may have already been removed')
|
||||
return [], info
|
||||
else:
|
||||
self.write_debug('Expected and actual durations mismatch')
|
||||
|
||||
@@ -72,6 +69,7 @@ class ModifyChaptersPP(FFmpegPostProcessor):
|
||||
os.replace(out_file, in_file)
|
||||
files_to_remove.append(uncut_file)
|
||||
|
||||
info['_real_duration'] = info['chapters'][-1]['end_time']
|
||||
return files_to_remove, info
|
||||
|
||||
def _mark_chapters_to_remove(self, chapters, sponsor_chapters):
|
||||
@@ -101,13 +99,6 @@ class ModifyChaptersPP(FFmpegPostProcessor):
|
||||
|
||||
return chapters, sponsor_chapters
|
||||
|
||||
def _get_real_video_duration(self, filename):
|
||||
duration = float_or_none(
|
||||
traverse_obj(self.get_metadata_object(filename), ('format', 'duration')))
|
||||
if duration is None:
|
||||
raise PostProcessingError('ffprobe returned empty duration')
|
||||
return duration
|
||||
|
||||
def _get_supported_subs(self, info):
|
||||
for sub in (info.get('requested_subtitles') or {}).values():
|
||||
sub_file = sub.get('filepath')
|
||||
@@ -153,38 +144,15 @@ class ModifyChaptersPP(FFmpegPostProcessor):
|
||||
|
||||
new_chapters = []
|
||||
|
||||
def chapter_length(c):
|
||||
return c['end_time'] - c['start_time']
|
||||
|
||||
def original_uncut_chapter(c):
|
||||
return '_was_cut' not in c and '_categories' not in c
|
||||
|
||||
def append_chapter(c):
|
||||
assert 'remove' not in c
|
||||
length = chapter_length(c) - excess_duration(c)
|
||||
length = c['end_time'] - c['start_time'] - excess_duration(c)
|
||||
# Chapter is completely covered by cuts or sponsors.
|
||||
if length <= 0:
|
||||
return
|
||||
start = new_chapters[-1]['end_time'] if new_chapters else 0
|
||||
c.update(start_time=start, end_time=start + length)
|
||||
# Append without checking for tininess to prevent having
|
||||
# a completely empty chapter list.
|
||||
if not new_chapters:
|
||||
new_chapters.append(c)
|
||||
return
|
||||
old_c = new_chapters[-1]
|
||||
# Merge with the previous if the chapter is tiny.
|
||||
# Only tiny chapters resulting from a cut can be skipped.
|
||||
# Chapters that were already tiny in the original list will be preserved.
|
||||
if not original_uncut_chapter(c) and length < _TINY_SPONSOR_OVERLAP_DURATION:
|
||||
old_c['end_time'] = c['end_time']
|
||||
# Previous tiny chapter was appended for the sake of preventing an empty chapter list.
|
||||
# Replace it with the current one.
|
||||
elif not original_uncut_chapter(old_c) and chapter_length(old_c) < _TINY_SPONSOR_OVERLAP_DURATION:
|
||||
c['start_time'] = old_c['start_time']
|
||||
new_chapters[-1] = c
|
||||
else:
|
||||
new_chapters.append(c)
|
||||
new_chapters.append(c)
|
||||
|
||||
# Turn into a priority queue, index is a tie breaker.
|
||||
# Plain stack sorted by start_time is not enough: after splitting the chapter,
|
||||
@@ -283,10 +251,36 @@ class ModifyChaptersPP(FFmpegPostProcessor):
|
||||
append_chapter(cur_chapter)
|
||||
cur_i, cur_chapter = i, c
|
||||
(append_chapter if 'remove' not in cur_chapter else append_cut)(cur_chapter)
|
||||
return self._remove_tiny_rename_sponsors(new_chapters), cuts
|
||||
|
||||
def _remove_tiny_rename_sponsors(self, chapters):
|
||||
new_chapters = []
|
||||
for i, c in enumerate(chapters):
|
||||
# Merge with the previous/next if the chapter is tiny.
|
||||
# Only tiny chapters resulting from a cut can be skipped.
|
||||
# Chapters that were already tiny in the original list will be preserved.
|
||||
if (('_was_cut' in c or '_categories' in c)
|
||||
and c['end_time'] - c['start_time'] < _TINY_CHAPTER_DURATION):
|
||||
if not new_chapters:
|
||||
# Prepend tiny chapter to the next one if possible.
|
||||
if i < len(chapters) - 1:
|
||||
chapters[i + 1]['start_time'] = c['start_time']
|
||||
continue
|
||||
else:
|
||||
old_c = new_chapters[-1]
|
||||
if i < len(chapters) - 1:
|
||||
next_c = chapters[i + 1]
|
||||
# Not a typo: key names in old_c and next_c are really different.
|
||||
prev_is_sponsor = 'categories' in old_c
|
||||
next_is_sponsor = '_categories' in next_c
|
||||
# Preferentially prepend tiny normals to normals and sponsors to sponsors.
|
||||
if (('_categories' not in c and prev_is_sponsor and not next_is_sponsor)
|
||||
or ('_categories' in c and not prev_is_sponsor and next_is_sponsor)):
|
||||
next_c['start_time'] = c['start_time']
|
||||
continue
|
||||
old_c['end_time'] = c['end_time']
|
||||
continue
|
||||
|
||||
i = -1
|
||||
for c in new_chapters.copy():
|
||||
i += 1
|
||||
c.pop('_was_cut', None)
|
||||
cats = c.pop('_categories', None)
|
||||
if cats:
|
||||
@@ -300,18 +294,19 @@ class ModifyChaptersPP(FFmpegPostProcessor):
|
||||
})
|
||||
outtmpl, tmpl_dict = self._downloader.prepare_outtmpl(self._sponsorblock_chapter_title, c)
|
||||
c['title'] = self._downloader.escape_outtmpl(outtmpl) % tmpl_dict
|
||||
if i > 0 and c['title'] == new_chapters[i - 1]['title']:
|
||||
new_chapters[i - 1]['end_time'] = c['end_time']
|
||||
new_chapters.pop(i)
|
||||
i -= 1
|
||||
|
||||
return new_chapters, cuts
|
||||
# Merge identically named sponsors.
|
||||
if (new_chapters and 'categories' in new_chapters[-1]
|
||||
and new_chapters[-1]['title'] == c['title']):
|
||||
new_chapters[-1]['end_time'] = c['end_time']
|
||||
continue
|
||||
new_chapters.append(c)
|
||||
return new_chapters
|
||||
|
||||
def remove_chapters(self, filename, ranges_to_cut, concat_opts, force_keyframes=False):
|
||||
in_file = filename
|
||||
out_file = prepend_extension(in_file, 'temp')
|
||||
if force_keyframes:
|
||||
in_file = self.force_keyframes(in_file, (t for r in ranges_to_cut for t in r))
|
||||
in_file = self.force_keyframes(in_file, (t for c in ranges_to_cut for t in (c['start_time'], c['end_time'])))
|
||||
self.to_screen(f'Removing chapters from {filename}')
|
||||
self.concat_files([in_file] * len(concat_opts), out_file, concat_opts)
|
||||
if in_file != filename:
|
||||
|
||||
@@ -5,6 +5,7 @@ from ..compat import compat_os_name
|
||||
from ..utils import (
|
||||
hyphenate_date,
|
||||
write_xattr,
|
||||
PostProcessingError,
|
||||
XAttrMetadataError,
|
||||
XAttrUnavailableError,
|
||||
)
|
||||
@@ -57,8 +58,7 @@ class XAttrMetadataPP(PostProcessor):
|
||||
return [], info
|
||||
|
||||
except XAttrUnavailableError as e:
|
||||
self.report_error(str(e))
|
||||
return [], info
|
||||
raise PostProcessingError(str(e))
|
||||
|
||||
except XAttrMetadataError as e:
|
||||
if e.reason == 'NO_SPACE':
|
||||
@@ -74,5 +74,5 @@ class XAttrMetadataPP(PostProcessor):
|
||||
msg += 'You need to use NTFS.'
|
||||
else:
|
||||
msg += '(You may have to enable them in your /etc/fstab)'
|
||||
self.report_error(msg)
|
||||
raise PostProcessingError(str(e))
|
||||
return [], info
|
||||
|
||||
+21
-13
@@ -31,6 +31,18 @@ def rsa_verify(message, signature, key):
|
||||
'''
|
||||
|
||||
|
||||
def detect_variant():
|
||||
if hasattr(sys, 'frozen') and getattr(sys, '_MEIPASS', None):
|
||||
if sys._MEIPASS == os.path.dirname(sys.executable):
|
||||
return 'dir'
|
||||
return 'exe'
|
||||
elif isinstance(globals().get('__loader__'), zipimporter):
|
||||
return 'zip'
|
||||
elif os.path.basename(sys.argv[0]) == '__main__.py':
|
||||
return 'source'
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def update_self(to_screen, verbose, opener):
|
||||
''' Exists for backward compatibility. Use run_update(ydl) instead '''
|
||||
|
||||
@@ -87,13 +99,14 @@ def run_update(ydl):
|
||||
h.update(mv[:n])
|
||||
return h.hexdigest()
|
||||
|
||||
err = None
|
||||
if isinstance(globals().get('__loader__'), zipimporter):
|
||||
pass
|
||||
elif hasattr(sys, 'frozen'):
|
||||
pass
|
||||
else:
|
||||
err = 'It looks like you installed yt-dlp with a package manager, pip, setup.py or a tarball. Please use that to update'
|
||||
ERRORS = {
|
||||
'exe': None,
|
||||
'zip': None,
|
||||
'dir': 'Auto-update is not supported for unpackaged windows executable. Re-download the latest release',
|
||||
'source': 'You cannot update when running from source code',
|
||||
'unknown': 'It looks like you installed yt-dlp with a package manager, pip, setup.py or a tarball. Use that to update',
|
||||
}
|
||||
err = ERRORS.get(detect_variant(), ERRORS['unknown'])
|
||||
if err:
|
||||
return report_error(err, expected=True)
|
||||
|
||||
@@ -138,12 +151,7 @@ def run_update(ydl):
|
||||
if not urlh:
|
||||
return None
|
||||
hash_data = ydl._opener.open(urlh).read().decode('utf-8')
|
||||
if hash_data.startswith('version:'):
|
||||
# Old colon-separated hash file
|
||||
return dict(ln.split(':') for ln in hash_data.splitlines()).get(filename)
|
||||
else:
|
||||
# GNU-style hash file
|
||||
return dict(ln.split()[::-1] for ln in hash_data.splitlines()).get(filename)
|
||||
return dict(ln.split()[::-1] for ln in hash_data.splitlines()).get(filename)
|
||||
|
||||
if not os.access(filename, os.W_OK):
|
||||
return report_error('no write permissions on %s' % filename, expected=True)
|
||||
|
||||
+60
-10
@@ -16,6 +16,8 @@ import email.header
|
||||
import errno
|
||||
import functools
|
||||
import gzip
|
||||
import hashlib
|
||||
import hmac
|
||||
import imp
|
||||
import io
|
||||
import itertools
|
||||
@@ -1740,6 +1742,7 @@ DATE_FORMATS = (
|
||||
'%b %dth %Y %I:%M',
|
||||
'%Y %m %d',
|
||||
'%Y-%m-%d',
|
||||
'%Y.%m.%d.',
|
||||
'%Y/%m/%d',
|
||||
'%Y/%m/%d %H:%M',
|
||||
'%Y/%m/%d %H:%M:%S',
|
||||
@@ -1761,6 +1764,7 @@ DATE_FORMATS = (
|
||||
'%b %d %Y at %H:%M:%S',
|
||||
'%B %d %Y at %H:%M',
|
||||
'%B %d %Y at %H:%M:%S',
|
||||
'%H:%M %d-%b-%Y',
|
||||
)
|
||||
|
||||
DATE_FORMATS_DAY_FIRST = list(DATE_FORMATS)
|
||||
@@ -2408,7 +2412,7 @@ class ExtractorError(YoutubeDLError):
|
||||
if sys.exc_info()[0] in network_exceptions:
|
||||
expected = True
|
||||
|
||||
self.msg = msg
|
||||
self.msg = str(msg)
|
||||
self.traceback = tb
|
||||
self.expected = expected
|
||||
self.cause = cause
|
||||
@@ -2419,7 +2423,7 @@ class ExtractorError(YoutubeDLError):
|
||||
super(ExtractorError, self).__init__(''.join((
|
||||
format_field(ie, template='[%s] '),
|
||||
format_field(video_id, template='%s: '),
|
||||
msg,
|
||||
self.msg,
|
||||
format_field(cause, template=' (caused by %r)'),
|
||||
'' if expected else bug_reports_message())))
|
||||
|
||||
@@ -3033,8 +3037,16 @@ class YoutubeDLRedirectHandler(compat_urllib_request.HTTPRedirectHandler):
|
||||
|
||||
def extract_timezone(date_str):
|
||||
m = re.search(
|
||||
r'^.{8,}?(?P<tz>Z$| ?(?P<sign>\+|-)(?P<hours>[0-9]{2}):?(?P<minutes>[0-9]{2})$)',
|
||||
date_str)
|
||||
r'''(?x)
|
||||
^.{8,}? # >=8 char non-TZ prefix, if present
|
||||
(?P<tz>Z| # just the UTC Z, or
|
||||
(?:(?<=.\b\d{4}|\b\d{2}:\d\d)| # preceded by 4 digits or hh:mm or
|
||||
(?<!.\b[a-zA-Z]{3}|[a-zA-Z]{4}|..\b\d\d)) # not preceded by 3 alpha word or >= 4 alpha or 2 digits
|
||||
[ ]? # optional space
|
||||
(?P<sign>\+|-) # +/-
|
||||
(?P<hours>[0-9]{2}):?(?P<minutes>[0-9]{2}) # hh[:]mm
|
||||
$)
|
||||
''', date_str)
|
||||
if not m:
|
||||
timezone = datetime.timedelta()
|
||||
else:
|
||||
@@ -3280,6 +3292,14 @@ def platform_name():
|
||||
return res
|
||||
|
||||
|
||||
def get_windows_version():
|
||||
''' Get Windows version. None if it's not running on Windows '''
|
||||
if compat_os_name == 'nt':
|
||||
return version_tuple(platform.win32_ver()[1])
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _windows_write_string(s, out):
|
||||
""" Returns True if the string was written using special methods,
|
||||
False if it has yet to be written out."""
|
||||
@@ -3972,6 +3992,9 @@ class LazyList(collections.abc.Sequence):
|
||||
''' Lazy immutable list from an iterable
|
||||
Note that slices of a LazyList are lists and not LazyList'''
|
||||
|
||||
class IndexError(IndexError):
|
||||
pass
|
||||
|
||||
def __init__(self, iterable):
|
||||
self.__iterable = iter(iterable)
|
||||
self.__cache = []
|
||||
@@ -4015,22 +4038,28 @@ class LazyList(collections.abc.Sequence):
|
||||
or (stop is None and step > 0)):
|
||||
# We need to consume the entire iterable to be able to slice from the end
|
||||
# Obviously, never use this with infinite iterables
|
||||
return self.__exhaust()[idx]
|
||||
|
||||
self.__exhaust()
|
||||
try:
|
||||
return self.__cache[idx]
|
||||
except IndexError as e:
|
||||
raise self.IndexError(e) from e
|
||||
n = max(start or 0, stop or 0) - len(self.__cache) + 1
|
||||
if n > 0:
|
||||
self.__cache.extend(itertools.islice(self.__iterable, n))
|
||||
return self.__cache[idx]
|
||||
try:
|
||||
return self.__cache[idx]
|
||||
except IndexError as e:
|
||||
raise self.IndexError(e) from e
|
||||
|
||||
def __bool__(self):
|
||||
try:
|
||||
self[-1] if self.__reversed else self[0]
|
||||
except IndexError:
|
||||
except self.IndexError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __len__(self):
|
||||
self.exhaust()
|
||||
self.__exhaust()
|
||||
return len(self.__cache)
|
||||
|
||||
def reverse(self):
|
||||
@@ -4378,6 +4407,8 @@ def js_to_json(code, vars={}):
|
||||
v = m.group(0)
|
||||
if v in ('true', 'false', 'null'):
|
||||
return v
|
||||
elif v in ('undefined', 'void 0'):
|
||||
return 'null'
|
||||
elif v.startswith('/*') or v.startswith('//') or v.startswith('!') or v == ',':
|
||||
return ""
|
||||
|
||||
@@ -4404,7 +4435,7 @@ def js_to_json(code, vars={}):
|
||||
"(?:[^"\\]*(?:\\\\|\\['"nurtbfx/\n]))*[^"\\]*"|
|
||||
'(?:[^'\\]*(?:\\\\|\\['"nurtbfx/\n]))*[^'\\]*'|
|
||||
{comment}|,(?={skip}[\]}}])|
|
||||
(?:(?<![0-9])[eE]|[a-df-zA-DF-Z_])[.a-zA-Z_0-9]*|
|
||||
void\s0|(?:(?<![0-9])[eE]|[a-df-zA-DF-Z_$])[.a-zA-Z_$0-9]*|
|
||||
\b(?:0[xX][0-9a-fA-F]+|0+[0-7]+)(?:{skip}:)?|
|
||||
[0-9]+(?={skip}:)|
|
||||
!+
|
||||
@@ -6352,3 +6383,22 @@ def traverse_dict(dictn, keys, casesense=True):
|
||||
|
||||
def variadic(x, allowed_types=(str, bytes)):
|
||||
return x if isinstance(x, collections.abc.Iterable) and not isinstance(x, allowed_types) else (x,)
|
||||
|
||||
|
||||
# create a JSON Web Signature (jws) with HS256 algorithm
|
||||
# the resulting format is in JWS Compact Serialization
|
||||
# implemented following JWT https://www.rfc-editor.org/rfc/rfc7519.html
|
||||
# implemented following JWS https://www.rfc-editor.org/rfc/rfc7515.html
|
||||
def jwt_encode_hs256(payload_data, key, headers={}):
|
||||
header_data = {
|
||||
'alg': 'HS256',
|
||||
'typ': 'JWT',
|
||||
}
|
||||
if headers:
|
||||
header_data.update(headers)
|
||||
header_b64 = base64.b64encode(json.dumps(header_data).encode('utf-8'))
|
||||
payload_b64 = base64.b64encode(json.dumps(payload_data).encode('utf-8'))
|
||||
h = hmac.new(key.encode('utf-8'), header_b64 + b'.' + payload_b64, hashlib.sha256)
|
||||
signature_b64 = base64.b64encode(h.digest())
|
||||
token = header_b64 + b'.' + payload_b64 + b'.' + signature_b64
|
||||
return token
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
__version__ = '2021.08.10'
|
||||
__version__ = '2021.09.25'
|
||||
|
||||
+7
-2
@@ -89,8 +89,12 @@ class ParseError(Exception):
|
||||
))
|
||||
|
||||
|
||||
# While the specification <https://www.w3.org/TR/webvtt1/#webvtt-timestamp>
|
||||
# prescribes that hours must be *2 or more* digits, timestamps with a single
|
||||
# digit for the hour part has been seen in the wild.
|
||||
# See https://github.com/yt-dlp/yt-dlp/issues/921
|
||||
_REGEX_TS = re.compile(r'''(?x)
|
||||
(?:([0-9]{2,}):)?
|
||||
(?:([0-9]{1,}):)?
|
||||
([0-9]{2}):
|
||||
([0-9]{2})\.
|
||||
([0-9]{3})?
|
||||
@@ -172,6 +176,7 @@ class Magic(HeaderBlock):
|
||||
_REGEX_TSMAP = re.compile(r'X-TIMESTAMP-MAP=')
|
||||
_REGEX_TSMAP_LOCAL = re.compile(r'LOCAL:')
|
||||
_REGEX_TSMAP_MPEGTS = re.compile(r'MPEGTS:([0-9]+)')
|
||||
_REGEX_TSMAP_SEP = re.compile(r'[ \t]*,[ \t]*')
|
||||
|
||||
@classmethod
|
||||
def __parse_tsmap(cls, parser):
|
||||
@@ -194,7 +199,7 @@ class Magic(HeaderBlock):
|
||||
raise ParseError(parser)
|
||||
else:
|
||||
raise ParseError(parser)
|
||||
if parser.consume(','):
|
||||
if parser.consume(cls._REGEX_TSMAP_SEP):
|
||||
continue
|
||||
if parser.consume(_REGEX_NL):
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user