mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2024-11-26 22:56:51 +01:00
merge 'master'
This commit is contained in:
commit
8b5ba28892
8
.github/ISSUE_TEMPLATE/1_broken_site.yml
vendored
8
.github/ISSUE_TEMPLATE/1_broken_site.yml
vendored
@ -77,3 +77,11 @@ body:
|
|||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> [!CAUTION]
|
||||||
|
> ### GitHub is experiencing a high volume of malicious spam comments.
|
||||||
|
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
||||||
|
>
|
||||||
|
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
||||||
|
@ -89,3 +89,11 @@ body:
|
|||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> [!CAUTION]
|
||||||
|
> ### GitHub is experiencing a high volume of malicious spam comments.
|
||||||
|
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
||||||
|
>
|
||||||
|
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
||||||
|
@ -85,3 +85,11 @@ body:
|
|||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> [!CAUTION]
|
||||||
|
> ### GitHub is experiencing a high volume of malicious spam comments.
|
||||||
|
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
||||||
|
>
|
||||||
|
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
||||||
|
8
.github/ISSUE_TEMPLATE/4_bug_report.yml
vendored
8
.github/ISSUE_TEMPLATE/4_bug_report.yml
vendored
@ -70,3 +70,11 @@ body:
|
|||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> [!CAUTION]
|
||||||
|
> ### GitHub is experiencing a high volume of malicious spam comments.
|
||||||
|
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
||||||
|
>
|
||||||
|
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
||||||
|
8
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
8
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
@ -64,3 +64,11 @@ body:
|
|||||||
[youtube] Extracting URL: https://www.youtube.com/watch?v=BaW_jenozKc
|
[youtube] Extracting URL: https://www.youtube.com/watch?v=BaW_jenozKc
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> [!CAUTION]
|
||||||
|
> ### GitHub is experiencing a high volume of malicious spam comments.
|
||||||
|
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
||||||
|
>
|
||||||
|
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
||||||
|
8
.github/ISSUE_TEMPLATE/6_question.yml
vendored
8
.github/ISSUE_TEMPLATE/6_question.yml
vendored
@ -70,3 +70,11 @@ body:
|
|||||||
[youtube] Extracting URL: https://www.youtube.com/watch?v=BaW_jenozKc
|
[youtube] Extracting URL: https://www.youtube.com/watch?v=BaW_jenozKc
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> [!CAUTION]
|
||||||
|
> ### GitHub is experiencing a high volume of malicious spam comments.
|
||||||
|
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
||||||
|
>
|
||||||
|
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
||||||
|
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@ -240,7 +240,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: write # For cleaning up cache
|
actions: write # For cleaning up cache
|
||||||
runs-on: macos-12
|
runs-on: macos-13
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@ -266,7 +266,7 @@ jobs:
|
|||||||
# We need to ignore wheels otherwise we break universal2 builds
|
# We need to ignore wheels otherwise we break universal2 builds
|
||||||
python3 -m pip install -U --no-binary :all: -r requirements.txt
|
python3 -m pip install -U --no-binary :all: -r requirements.txt
|
||||||
# We need to fuse our own universal2 wheels for curl_cffi
|
# We need to fuse our own universal2 wheels for curl_cffi
|
||||||
python3 -m pip install -U delocate
|
python3 -m pip install -U 'delocate==0.11.0'
|
||||||
mkdir curl_cffi_whls curl_cffi_universal2
|
mkdir curl_cffi_whls curl_cffi_universal2
|
||||||
python3 devscripts/install_deps.py --print -o --include curl-cffi > requirements.txt
|
python3 devscripts/install_deps.py --print -o --include curl-cffi > requirements.txt
|
||||||
for platform in "macosx_11_0_arm64" "macosx_11_0_x86_64"; do
|
for platform in "macosx_11_0_arm64" "macosx_11_0_x86_64"; do
|
||||||
@ -346,7 +346,7 @@ jobs:
|
|||||||
macos_legacy:
|
macos_legacy:
|
||||||
needs: process
|
needs: process
|
||||||
if: inputs.macos_legacy
|
if: inputs.macos_legacy
|
||||||
runs-on: macos-12
|
runs-on: macos-13
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@ -409,7 +409,7 @@ jobs:
|
|||||||
run: | # Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
|
run: | # Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
|
||||||
python devscripts/install_deps.py -o --include build
|
python devscripts/install_deps.py -o --include build
|
||||||
python devscripts/install_deps.py --include curl-cffi
|
python devscripts/install_deps.py --include curl-cffi
|
||||||
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-6.7.0-py3-none-any.whl"
|
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-6.10.0-py3-none-any.whl"
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
@ -469,7 +469,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python devscripts/install_deps.py -o --include build
|
python devscripts/install_deps.py -o --include build
|
||||||
python devscripts/install_deps.py
|
python devscripts/install_deps.py
|
||||||
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-6.7.0-py3-none-any.whl"
|
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-6.10.0-py3-none-any.whl"
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
|
2
.github/workflows/core.yml
vendored
2
.github/workflows/core.yml
vendored
@ -59,4 +59,4 @@ jobs:
|
|||||||
continue-on-error: False
|
continue-on-error: False
|
||||||
run: |
|
run: |
|
||||||
python3 -m yt_dlp -v || true # Print debug head
|
python3 -m yt_dlp -v || true # Print debug head
|
||||||
python3 ./devscripts/run_tests.py core
|
python3 ./devscripts/run_tests.py --pytest-args '--reruns 2 --reruns-delay 3.0' core
|
||||||
|
21
.github/workflows/issue-lockdown.yml
vendored
Normal file
21
.github/workflows/issue-lockdown.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
name: Issue Lockdown
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lockdown:
|
||||||
|
name: Issue Lockdown
|
||||||
|
if: vars.ISSUE_LOCKDOWN
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: "Lock new issue"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
REPOSITORY: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
gh issue lock "${ISSUE_NUMBER}" -R "${REPOSITORY}"
|
4
.github/workflows/quick-test.yml
vendored
4
.github/workflows/quick-test.yml
vendored
@ -15,12 +15,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: '3.8'
|
python-version: '3.8'
|
||||||
- name: Install test requirements
|
- name: Install test requirements
|
||||||
run: python3 ./devscripts/install_deps.py --include test
|
run: python3 ./devscripts/install_deps.py -o --include test
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
run: |
|
run: |
|
||||||
python3 -m yt_dlp -v || true
|
python3 -m yt_dlp -v || true
|
||||||
python3 ./devscripts/run_tests.py core
|
python3 ./devscripts/run_tests.py --pytest-args '--reruns 2 --reruns-delay 3.0' core
|
||||||
check:
|
check:
|
||||||
name: Code check
|
name: Code check
|
||||||
if: "!contains(github.event.head_commit.message, 'ci skip all')"
|
if: "!contains(github.event.head_commit.message, 'ci skip all')"
|
||||||
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@ -204,7 +204,7 @@ jobs:
|
|||||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
git add -u
|
git add -u
|
||||||
git commit -m "Release ${{ env.version }}" \
|
git commit -m "Release ${{ env.version }}" \
|
||||||
-m "Created by: ${{ github.event.sender.login }}" -m ":ci skip all :ci run dl"
|
-m "Created by: ${{ github.event.sender.login }}" -m ":ci skip all"
|
||||||
git push origin --force ${{ github.event.ref }}:release
|
git push origin --force ${{ github.event.ref }}:release
|
||||||
|
|
||||||
- name: Get target commitish
|
- name: Get target commitish
|
||||||
@ -325,7 +325,7 @@ jobs:
|
|||||||
"(https://github.com/yt-dlp/yt-dlp-master-builds/releases/latest \"Master builds\")"' || '' }} > ./RELEASE_NOTES
|
"(https://github.com/yt-dlp/yt-dlp-master-builds/releases/latest \"Master builds\")"' || '' }} > ./RELEASE_NOTES
|
||||||
printf '\n\n' >> ./RELEASE_NOTES
|
printf '\n\n' >> ./RELEASE_NOTES
|
||||||
cat >> ./RELEASE_NOTES << EOF
|
cat >> ./RELEASE_NOTES << EOF
|
||||||
#### A description of the various files are in the [README](https://github.com/${{ github.repository }}#release-files)
|
#### A description of the various files is in the [README](https://github.com/${{ github.repository }}#release-files)
|
||||||
---
|
---
|
||||||
$(python ./devscripts/make_changelog.py -vv --collapsible)
|
$(python ./devscripts/make_changelog.py -vv --collapsible)
|
||||||
EOF
|
EOF
|
||||||
|
17
.github/workflows/sanitize-comment.yml
vendored
Normal file
17
.github/workflows/sanitize-comment.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
name: Sanitize comment
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created, edited]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sanitize-comment:
|
||||||
|
name: Sanitize comment
|
||||||
|
if: vars.SANITIZE_COMMENT && !github.event.issue.pull_request
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Sanitize comment
|
||||||
|
uses: yt-dlp/sanitize-comment@v1
|
21
CONTRIBUTORS
21
CONTRIBUTORS
@ -657,3 +657,24 @@ luvyana
|
|||||||
szantnerb
|
szantnerb
|
||||||
hugepower
|
hugepower
|
||||||
scribblemaniac
|
scribblemaniac
|
||||||
|
Codenade
|
||||||
|
Demon000
|
||||||
|
Deukhoofd
|
||||||
|
grqz
|
||||||
|
hibes
|
||||||
|
Khaoklong51
|
||||||
|
kieraneglin
|
||||||
|
lengzuo
|
||||||
|
naglis
|
||||||
|
ndyanx
|
||||||
|
otovalek
|
||||||
|
quad
|
||||||
|
rakslice
|
||||||
|
sahilsinghss73
|
||||||
|
tony-hn
|
||||||
|
xingchensong
|
||||||
|
BallzCrasher
|
||||||
|
coreywright
|
||||||
|
eric321
|
||||||
|
poyhen
|
||||||
|
tetra-fox
|
||||||
|
112
Changelog.md
112
Changelog.md
@ -4,6 +4,118 @@ # Changelog
|
|||||||
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
### 2024.10.07
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- **cookies**: [Fix cookie load error handling](https://github.com/yt-dlp/yt-dlp/commit/e59c82a74cda5139eb3928c75b0bd45484dbe7f0) ([#11140](https://github.com/yt-dlp/yt-dlp/issues/11140)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **applepodcasts**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/6328e2e67a4e126e08af382e6a387073082d5c5f) ([#10903](https://github.com/yt-dlp/yt-dlp/issues/10903)) by [coreywright](https://github.com/coreywright)
|
||||||
|
- **cwtv**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/4b7bec66d8100978b82bb24110ed44e2a7749931) ([#11135](https://github.com/yt-dlp/yt-dlp/issues/11135)) by [kclauhk](https://github.com/kclauhk)
|
||||||
|
- **instagram**
|
||||||
|
- [Do not hardcode user-agent](https://github.com/yt-dlp/yt-dlp/commit/079a7bc334281d3c13d347770ae5f9f2b7da471a) ([#11155](https://github.com/yt-dlp/yt-dlp/issues/11155)) by [poyhen](https://github.com/poyhen)
|
||||||
|
- [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/cf85cba5d9496bd2689e1070005b4d1b4cd3dc6d) ([#11156](https://github.com/yt-dlp/yt-dlp/issues/11156)) by [tetra-fox](https://github.com/tetra-fox)
|
||||||
|
- **noodlemagazine**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/ccb23e1bac9768d1c70535beb744e668ed4a2720) ([#11144](https://github.com/yt-dlp/yt-dlp/issues/11144)) by [BallzCrasher](https://github.com/BallzCrasher)
|
||||||
|
- **patreon**: [Extract all m3u8 formats for locked posts](https://github.com/yt-dlp/yt-dlp/commit/f91645aceaf13926cf35be2c1dfef61b3aab97fb) ([#11138](https://github.com/yt-dlp/yt-dlp/issues/11138)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **youtube**: [Change default player clients to `ios,mweb`](https://github.com/yt-dlp/yt-dlp/commit/de2062753a188060d76f587e45becce61fe399f9) ([#11190](https://github.com/yt-dlp/yt-dlp/issues/11190)) by [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
#### Postprocessor changes
|
||||||
|
- **xattrmetadata**: [Try to write each attribute](https://github.com/yt-dlp/yt-dlp/commit/3a193346eeb27ac2959ff30c370adb899ec94732) ([#11115](https://github.com/yt-dlp/yt-dlp/issues/11115)) by [eric321](https://github.com/eric321)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **ci**: [Rerun failed tests](https://github.com/yt-dlp/yt-dlp/commit/b31b81d85f00601710d4fac590c3e4efb4133283) ([#11143](https://github.com/yt-dlp/yt-dlp/issues/11143)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
- **cleanup**: Miscellaneous: [1a176d8](https://github.com/yt-dlp/yt-dlp/commit/1a176d874e6772cd898ce507379ea388e96ee3f7) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
### 2024.09.27
|
||||||
|
|
||||||
|
#### Important changes
|
||||||
|
- **The minimum *recommended* Python version has been raised to 3.9**
|
||||||
|
Since Python 3.8 will reach end-of-life in October 2024, support for it will be dropped soon. [Read more](https://github.com/yt-dlp/yt-dlp/issues/10086)
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- [Allow `none` arg to negate `--convert-subs` and `--convert-thumbnails`](https://github.com/yt-dlp/yt-dlp/commit/c08e0b20b5edd8957b8318716bc14e896d1b96f4) ([#11066](https://github.com/yt-dlp/yt-dlp/issues/11066)) by [kieraneglin](https://github.com/kieraneglin)
|
||||||
|
- [Fix format sorting bug with vp9.2 vcodec](https://github.com/yt-dlp/yt-dlp/commit/8f4ea14680c7865d8ffac10a9174205d1d84ada7) ([#10884](https://github.com/yt-dlp/yt-dlp/issues/10884)) by [rakslice](https://github.com/rakslice)
|
||||||
|
- [Raise minimum recommended Python version to 3.9](https://github.com/yt-dlp/yt-dlp/commit/cca534cd9e6850c70244f225a4a1895ef4bcdbec) ([#11098](https://github.com/yt-dlp/yt-dlp/issues/11098)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **cookies**: [Improve error message for Windows `--cookies-from-browser chrome` issue](https://github.com/yt-dlp/yt-dlp/commit/b397a64691421ace5df09457c2a764821a2dc6f2) ([#11090](https://github.com/yt-dlp/yt-dlp/issues/11090)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **utils**: `mimetype2ext`: [Recognize `aacp` as `aac`](https://github.com/yt-dlp/yt-dlp/commit/cc85596d5b59f0c14e9381b3675f619c1e12e597) ([#10860](https://github.com/yt-dlp/yt-dlp/issues/10860)) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- [Fix JW Player format parsing](https://github.com/yt-dlp/yt-dlp/commit/409f8e9e3b4bde81ef76fc563256f876d2ff8099) ([#10956](https://github.com/yt-dlp/yt-dlp/issues/10956)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- [Handle decode errors when reading responses](https://github.com/yt-dlp/yt-dlp/commit/325001317d97f4545d66fac44c4ba772c6f45f22) ([#10868](https://github.com/yt-dlp/yt-dlp/issues/10868)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **abc.net.au**: iview, showseries: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/7f909046f4dc0fba472b4963145aef6e0d42491b) ([#11101](https://github.com/yt-dlp/yt-dlp/issues/11101)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **adn**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/cc88a54bb1ef285154775f8a6a413335ce4c71ce) ([#10749](https://github.com/yt-dlp/yt-dlp/issues/10749)) by [infanf](https://github.com/infanf)
|
||||||
|
- **asobistage**: [Support redirected URLs](https://github.com/yt-dlp/yt-dlp/commit/a7d3235c84dac57a127cbe0ff38f7f7c2fdd8fa0) ([#10768](https://github.com/yt-dlp/yt-dlp/issues/10768)) by [pzhlkj6612](https://github.com/pzhlkj6612)
|
||||||
|
- **bandcamp**: user: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/5d0176547f16a3642cd71627126e9dfc24981e20) ([#10328](https://github.com/yt-dlp/yt-dlp/issues/10328)) by [bashonly](https://github.com/bashonly), [quad](https://github.com/quad)
|
||||||
|
- **beacon**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/b4760c778d0c92c6e3f2bc8346cd72c8f08595ae) ([#9901](https://github.com/yt-dlp/yt-dlp/issues/9901)) by [Deukhoofd](https://github.com/Deukhoofd)
|
||||||
|
- **bilibili**
|
||||||
|
- [Fix chapters and subtitles extraction](https://github.com/yt-dlp/yt-dlp/commit/a2000bc85730c950351d78bb818493dc39dca3cb) ([#11099](https://github.com/yt-dlp/yt-dlp/issues/11099)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Fix festival URL support](https://github.com/yt-dlp/yt-dlp/commit/b43bd864851f2862e26caa85461c5d825d49d463) ([#10740](https://github.com/yt-dlp/yt-dlp/issues/10740)) by [bashonly](https://github.com/bashonly), [grqz](https://github.com/grqz)
|
||||||
|
- **biliintl**: [Fix referer header](https://github.com/yt-dlp/yt-dlp/commit/a06bb586795ebab87a2356923acfc674d6f0e152) ([#11003](https://github.com/yt-dlp/yt-dlp/issues/11003)) by [Khaoklong51](https://github.com/Khaoklong51)
|
||||||
|
- **dropbox**: [Fix password-protected video support](https://github.com/yt-dlp/yt-dlp/commit/63da31b3b29af90062d8a72a905ffe4b5e499042) ([#10735](https://github.com/yt-dlp/yt-dlp/issues/10735)) by [ndyanx](https://github.com/ndyanx)
|
||||||
|
- **ertgr**: [Fix video extraction](https://github.com/yt-dlp/yt-dlp/commit/416686ed0cf792ec44ab059f3b229dd776077e14) ([#11091](https://github.com/yt-dlp/yt-dlp/issues/11091)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **eurosport**: [Support local URL variants](https://github.com/yt-dlp/yt-dlp/commit/f0bb28504c8c2b75ee3e5796aed50de2a7f90a1b) ([#10785](https://github.com/yt-dlp/yt-dlp/issues/10785)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **facebook**
|
||||||
|
- ads: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/d62fef7e07d454c0d2ba2d69fb96d691dba1ded0) ([#10704](https://github.com/yt-dlp/yt-dlp/issues/10704)) by [kclauhk](https://github.com/kclauhk)
|
||||||
|
- reel: [Improve metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/0e1b941c6b2caa688b0d3332e723d16dbafa4311) by [lengzuo](https://github.com/lengzuo)
|
||||||
|
- **germanupa**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/124f058b546d652a359c67025bb479789bfbef0b) ([#10538](https://github.com/yt-dlp/yt-dlp/issues/10538)) by [grqz](https://github.com/grqz)
|
||||||
|
- **hgtvde**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/a555389c9bb32e589e00b4664974423fb7b04dcd) ([#10992](https://github.com/yt-dlp/yt-dlp/issues/10992)) by [bashonly](https://github.com/bashonly), [rdamas](https://github.com/rdamas)
|
||||||
|
- **huya**: video: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/25c1cdaa2650563494d3bf00a38f72d0d9486bff) ([#10686](https://github.com/yt-dlp/yt-dlp/issues/10686)) by [hugepower](https://github.com/hugepower)
|
||||||
|
- **iprima**: [Fix zoom URL support](https://github.com/yt-dlp/yt-dlp/commit/4a27b8f092f7f7c10b7a334d3535c97c2af02f0a) ([#10959](https://github.com/yt-dlp/yt-dlp/issues/10959)) by [otovalek](https://github.com/otovalek)
|
||||||
|
- **khanacademy**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/0fba08485b6445b72b5b63ae23ca2a73fa5d967f) ([#10913](https://github.com/yt-dlp/yt-dlp/issues/10913)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **kick**
|
||||||
|
- clips: [Support new URL format](https://github.com/yt-dlp/yt-dlp/commit/0aa4426e9a35f7f8e184f1f2082b3b313c1448f7) ([#11107](https://github.com/yt-dlp/yt-dlp/issues/11107)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- vod: [Support new URL format](https://github.com/yt-dlp/yt-dlp/commit/173d54c151b987409e3eb09552d8d89ed8fc50f7) ([#10988](https://github.com/yt-dlp/yt-dlp/issues/10988)) by [bashonly](https://github.com/bashonly), [grqz](https://github.com/grqz)
|
||||||
|
- **kika**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/e6f48ca80821939c1fd11ec2a0cdbf2fba9b258a) ([#5788](https://github.com/yt-dlp/yt-dlp/issues/5788)) by [1100101](https://github.com/1100101)
|
||||||
|
- **lnkgo**: [Remove extractor](https://github.com/yt-dlp/yt-dlp/commit/fa83d0b36bc43d30fe9241c1e923f4614864b758) ([#10904](https://github.com/yt-dlp/yt-dlp/issues/10904)) by [naglis](https://github.com/naglis)
|
||||||
|
- **loom**: [Fix m3u8 formats extraction](https://github.com/yt-dlp/yt-dlp/commit/7509d692b37a7ec6230ea75bfe1e44a8de5eefce) ([#10760](https://github.com/yt-dlp/yt-dlp/issues/10760)) by [kclauhk](https://github.com/kclauhk)
|
||||||
|
- **mediaklikk**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/e2b3634e299be9c16a247ece3b1858d83889c324) ([#11083](https://github.com/yt-dlp/yt-dlp/issues/11083)) by [szantnerb](https://github.com/szantnerb)
|
||||||
|
- **mojevideo**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/28b0ecba2af5b4919f198474b3d00a76ef322c31) ([#11019](https://github.com/yt-dlp/yt-dlp/issues/11019)) by [04-pasha-04](https://github.com/04-pasha-04), [pzhlkj6612](https://github.com/pzhlkj6612)
|
||||||
|
- **niconico**: [Fix m3u8 formats extraction](https://github.com/yt-dlp/yt-dlp/commit/eabb4680fdb09ba1f48d174a700a2e3b43f82add) ([#11103](https://github.com/yt-dlp/yt-dlp/issues/11103)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **nzz**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/4a9bc8c3630378bc29f0266126b503f6190c0430) ([#10461](https://github.com/yt-dlp/yt-dlp/issues/10461)) by [1-Byte](https://github.com/1-Byte)
|
||||||
|
- **patreoncampaign**: [Support API URLs](https://github.com/yt-dlp/yt-dlp/commit/232e6db30c474d1b387e405342f34173ceeaf832) ([#10734](https://github.com/yt-dlp/yt-dlp/issues/10734)) by [bashonly](https://github.com/bashonly), [hibes](https://github.com/hibes)
|
||||||
|
- **pinterest**: [Extend `_VALID_URL`](https://github.com/yt-dlp/yt-dlp/commit/c8c078fe28b0ffc15ef9646346c00c592fe71a78) ([#10867](https://github.com/yt-dlp/yt-dlp/issues/10867)) by [bashonly](https://github.com/bashonly), [sahilsinghss73](https://github.com/sahilsinghss73)
|
||||||
|
- **radiko**: [Extract unique `id` values](https://github.com/yt-dlp/yt-dlp/commit/c8d096c5ce111411fbdbe2abb8fed54f317a6182) ([#10726](https://github.com/yt-dlp/yt-dlp/issues/10726)) by [garret1317](https://github.com/garret1317)
|
||||||
|
- **rtp**: [Support more subpages](https://github.com/yt-dlp/yt-dlp/commit/d02df303d8e49390599db9f34482697e4d1cf5b2) ([#10787](https://github.com/yt-dlp/yt-dlp/issues/10787)) by [Demon000](https://github.com/Demon000)
|
||||||
|
- **rumblechannel**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/ad0b857f459a6d390fbf124183916218c52f223a) ([#11049](https://github.com/yt-dlp/yt-dlp/issues/11049)) by [tony-hn](https://github.com/tony-hn)
|
||||||
|
- **rutube**: [Support livestreams](https://github.com/yt-dlp/yt-dlp/commit/41be32e78c3845000dbac188ffb90ea3ea7c4dfa) ([#10844](https://github.com/yt-dlp/yt-dlp/issues/10844)) by [pzhlkj6612](https://github.com/pzhlkj6612)
|
||||||
|
- **samplefocus**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/46f4c80bc363ee8116c33d37f65202e6c3470954) ([#10947](https://github.com/yt-dlp/yt-dlp/issues/10947)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **screenrec**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/36f9e602ad55679764bc75a4f67f7562b1d6adcf) ([#10917](https://github.com/yt-dlp/yt-dlp/issues/10917)) by [naglis](https://github.com/naglis)
|
||||||
|
- **sen**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/41a241ca6ffb95b3d9aaf4f42106ca8cba9af1a6) ([#10952](https://github.com/yt-dlp/yt-dlp/issues/10952)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **servus**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/300c91274f7ea5b1b0528fc5ee11cf1a61d4079e) ([#10944](https://github.com/yt-dlp/yt-dlp/issues/10944)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **snapchatspotlight**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/b37417e4f934fd8909788b493d017777155b0ae5) ([#11030](https://github.com/yt-dlp/yt-dlp/issues/11030)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **svtpage**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/5a8a05aebb49693e78e1123015837ed5e961ff76) ([#11010](https://github.com/yt-dlp/yt-dlp/issues/11010)) by [diman8](https://github.com/diman8)
|
||||||
|
- **tenplay**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/d8d473002b654ab0e7b97ead869f58b4361eeae1) ([#10928](https://github.com/yt-dlp/yt-dlp/issues/10928)) by [aarubui](https://github.com/aarubui)
|
||||||
|
- **tiktok**: [Fix web formats extraction](https://github.com/yt-dlp/yt-dlp/commit/3ad0b7f422d547204df687b6d0b2d9110fff3990) ([#11074](https://github.com/yt-dlp/yt-dlp/issues/11074)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **twitter**: spaces: [Support video spaces](https://github.com/yt-dlp/yt-dlp/commit/bef1d4d6fc9493fda7f75e2289c07c507d10092f) ([#10789](https://github.com/yt-dlp/yt-dlp/issues/10789)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **vidflex**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/e978c312d6550a6ae4c9df18001afb1b420cb72f) ([#10002](https://github.com/yt-dlp/yt-dlp/issues/10002)) by [pzhlkj6612](https://github.com/pzhlkj6612)
|
||||||
|
- **vimeo**
|
||||||
|
- [Always try to extract original format](https://github.com/yt-dlp/yt-dlp/commit/4115c24d157c5b5f63089d75c4e0f51d1f8b4489) ([#10721](https://github.com/yt-dlp/yt-dlp/issues/10721)) by [bashonly](https://github.com/bashonly) (With fixes in [e8e6a98](https://github.com/yt-dlp/yt-dlp/commit/e8e6a982a1b659eed434d225d7922f632bac6568) by [seproDev](https://github.com/seproDev))
|
||||||
|
- [Fix HLS audio format sorting](https://github.com/yt-dlp/yt-dlp/commit/a1b4ac2b8ed8e6eaa56044d439f1e0d00c2ba218) ([#11082](https://github.com/yt-dlp/yt-dlp/issues/11082)) by [fireattack](https://github.com/fireattack)
|
||||||
|
- **watchespn**: [Improve auth support](https://github.com/yt-dlp/yt-dlp/commit/7adff8caf152dcf96d03aff69ed8545c0a63567c) ([#10910](https://github.com/yt-dlp/yt-dlp/issues/10910)) by [ischmidt20](https://github.com/ischmidt20)
|
||||||
|
- **wistia**: [Support password-protected videos](https://github.com/yt-dlp/yt-dlp/commit/9f5c9a90898c5a1e672922d9cd799716c73cee34) ([#11100](https://github.com/yt-dlp/yt-dlp/issues/11100)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **ximalaya**: [Add VIP support](https://github.com/yt-dlp/yt-dlp/commit/3dfd720d098b4d49d69cfc77e6376f22bcd90934) ([#10832](https://github.com/yt-dlp/yt-dlp/issues/10832)) by [seproDev](https://github.com/seproDev), [xingchensong](https://github.com/xingchensong)
|
||||||
|
- **xinpianchang**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/3aa0156e05662923d130ddbc1c82596e38c01a00) ([#10950](https://github.com/yt-dlp/yt-dlp/issues/10950)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **yleareena**: [Support podcasts](https://github.com/yt-dlp/yt-dlp/commit/48d629d461e05b1b19f5e53dc959bb9ebe95da42) ([#11104](https://github.com/yt-dlp/yt-dlp/issues/11104)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **youtube**
|
||||||
|
- [Add `po_token`, `visitor_data`, `data_sync_id` extractor args](https://github.com/yt-dlp/yt-dlp/commit/3a3bd00037e9908e87da4fa9f2ad772aa34dc60e) ([#10648](https://github.com/yt-dlp/yt-dlp/issues/10648)) by [bashonly](https://github.com/bashonly), [coletdjnz](https://github.com/coletdjnz), [seproDev](https://github.com/seproDev) (With fixes in [fa2be9a](https://github.com/yt-dlp/yt-dlp/commit/fa2be9a7c63babede07480151363e54eee5702bd) by [bashonly](https://github.com/bashonly))
|
||||||
|
- [Support excluding `player_client`s in extractor-arg](https://github.com/yt-dlp/yt-dlp/commit/49f3741a820ed142f6866317c2e7d247b130960e) ([#10710](https://github.com/yt-dlp/yt-dlp/issues/10710)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- clip: [Prioritize `https` formats](https://github.com/yt-dlp/yt-dlp/commit/1d84b780cf33a1d84756825ac23f990a905703df) ([#11102](https://github.com/yt-dlp/yt-dlp/issues/11102)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- tab: [Fix shorts tab extraction](https://github.com/yt-dlp/yt-dlp/commit/9431777b4c37129a6093080c77ca59960afbb9d7) ([#10938](https://github.com/yt-dlp/yt-dlp/issues/10938)) by [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
#### Networking changes
|
||||||
|
- [Fix handler not being added to RequestError](https://github.com/yt-dlp/yt-dlp/commit/d1c4d88b2d912e8da5e76db455562ca63b1af690) ([#10955](https://github.com/yt-dlp/yt-dlp/issues/10955)) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
- [Pin `curl-cffi` version to < 0.7.2](https://github.com/yt-dlp/yt-dlp/commit/5bb1aa04dafce13ba9de707ea53169fab58b5207) ([#11092](https://github.com/yt-dlp/yt-dlp/issues/11092)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **Request Handler**: websockets: [Upgrade websockets to 13.0](https://github.com/yt-dlp/yt-dlp/commit/6f9e6537434562d513d0c9b68ced8a61ade94a64) ([#10815](https://github.com/yt-dlp/yt-dlp/issues/10815)) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **build**
|
||||||
|
- [Bump PyInstaller version pin to `>=6.10.0`](https://github.com/yt-dlp/yt-dlp/commit/fb8b7f226d251e521a89b23c415e249e5b788e5c) ([#10709](https://github.com/yt-dlp/yt-dlp/issues/10709)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Pin `delocate` version for `macos`](https://github.com/yt-dlp/yt-dlp/commit/7e41628ff523b3fe373b0981a5db441358980dab) ([#10901](https://github.com/yt-dlp/yt-dlp/issues/10901)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **ci**
|
||||||
|
- [Add comment sanitization workflow](https://github.com/yt-dlp/yt-dlp/commit/b6200bdcf3a9415ae36859188f9a57e3e461c696) ([#10915](https://github.com/yt-dlp/yt-dlp/issues/10915)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
|
||||||
|
- [Add issue tracker anti-spam protection](https://github.com/yt-dlp/yt-dlp/commit/ad9a8115aa29a1a95c961b16fcf129a228d98f50) ([#10861](https://github.com/yt-dlp/yt-dlp/issues/10861)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **cleanup**: Miscellaneous: [c6387ab](https://github.com/yt-dlp/yt-dlp/commit/c6387abc1af9842bb0541288a5610abba9b1ab51) by [bashonly](https://github.com/bashonly), [Codenade](https://github.com/Codenade), [coletdjnz](https://github.com/coletdjnz), [grqz](https://github.com/grqz), [Grub4K](https://github.com/Grub4K), [pzhlkj6612](https://github.com/pzhlkj6612), [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
### 2024.08.06
|
### 2024.08.06
|
||||||
|
|
||||||
#### Core changes
|
#### Core changes
|
||||||
|
47
README.md
47
README.md
@ -200,7 +200,7 @@ #### Impersonation
|
|||||||
|
|
||||||
The following provide support for impersonating browser requests. This may be required for some sites that employ TLS fingerprinting.
|
The following provide support for impersonating browser requests. This may be required for some sites that employ TLS fingerprinting.
|
||||||
|
|
||||||
* [**curl_cffi**](https://github.com/yifeikong/curl_cffi) (recommended) - Python binding for [curl-impersonate](https://github.com/lwthiker/curl-impersonate). Provides impersonation targets for Chrome, Edge and Safari. Licensed under [MIT](https://github.com/yifeikong/curl_cffi/blob/main/LICENSE)
|
* [**curl_cffi**](https://github.com/lexiforest/curl_cffi) (recommended) - Python binding for [curl-impersonate](https://github.com/lexiforest/curl-impersonate). Provides impersonation targets for Chrome, Edge and Safari. Licensed under [MIT](https://github.com/lexiforest/curl_cffi/blob/main/LICENSE)
|
||||||
* Can be installed with the `curl-cffi` group, e.g. `pip install "yt-dlp[default,curl-cffi]"`
|
* Can be installed with the `curl-cffi` group, e.g. `pip install "yt-dlp[default,curl-cffi]"`
|
||||||
* Currently included in `yt-dlp.exe`, `yt-dlp_linux` and `yt-dlp_macos` builds
|
* Currently included in `yt-dlp.exe`, `yt-dlp_linux` and `yt-dlp_macos` builds
|
||||||
|
|
||||||
@ -278,7 +278,7 @@ ### Related scripts
|
|||||||
* **`devscripts/update-version.py`** - Update the version number based on the current date.
|
* **`devscripts/update-version.py`** - Update the version number based on the current date.
|
||||||
* **`devscripts/set-variant.py`** - Set the build variant of the executable.
|
* **`devscripts/set-variant.py`** - Set the build variant of the executable.
|
||||||
* **`devscripts/make_changelog.py`** - Create a markdown changelog using short commit messages and update `CONTRIBUTORS` file.
|
* **`devscripts/make_changelog.py`** - Create a markdown changelog using short commit messages and update `CONTRIBUTORS` file.
|
||||||
* **`devscripts/make_lazy_extractors.py`** - Create lazy extractors. Running this before building the binaries (any variant) will improve their startup performance. Set the environment variable `YTDLP_NO_LAZY_EXTRACTORS=1` if you wish to forcefully disable lazy extractor loading.
|
* **`devscripts/make_lazy_extractors.py`** - Create lazy extractors. Running this before building the binaries (any variant) will improve their startup performance. Set the environment variable `YTDLP_NO_LAZY_EXTRACTORS` to something nonempty to forcefully disable lazy extractor loading.
|
||||||
|
|
||||||
Note: See their `--help` for more info.
|
Note: See their `--help` for more info.
|
||||||
|
|
||||||
@ -459,17 +459,17 @@ ## Video Selection:
|
|||||||
conditions. Use a "\" to escape "&" or
|
conditions. Use a "\" to escape "&" or
|
||||||
quotes if needed. If used multiple times,
|
quotes if needed. If used multiple times,
|
||||||
the filter matches if at least one of the
|
the filter matches if at least one of the
|
||||||
conditions is met. E.g. --match-filter
|
conditions is met. E.g. --match-filters
|
||||||
!is_live --match-filter "like_count>?100 &
|
!is_live --match-filters "like_count>?100 &
|
||||||
description~='(?i)\bcats \& dogs\b'" matches
|
description~='(?i)\bcats \& dogs\b'" matches
|
||||||
only videos that are not live OR those that
|
only videos that are not live OR those that
|
||||||
have a like count more than 100 (or the like
|
have a like count more than 100 (or the like
|
||||||
field is not available) and also has a
|
field is not available) and also has a
|
||||||
description that contains the phrase "cats &
|
description that contains the phrase "cats &
|
||||||
dogs" (caseless). Use "--match-filter -" to
|
dogs" (caseless). Use "--match-filters -" to
|
||||||
interactively ask whether to download each
|
interactively ask whether to download each
|
||||||
video
|
video
|
||||||
--no-match-filters Do not use any --match-filter (default)
|
--no-match-filters Do not use any --match-filters (default)
|
||||||
--break-match-filters FILTER Same as "--match-filters" but stops the
|
--break-match-filters FILTER Same as "--match-filters" but stops the
|
||||||
download process when a video is rejected
|
download process when a video is rejected
|
||||||
--no-break-match-filters Do not use any --break-match-filters (default)
|
--no-break-match-filters Do not use any --break-match-filters (default)
|
||||||
@ -490,7 +490,7 @@ ## Video Selection:
|
|||||||
encountering a file that is in the archive
|
encountering a file that is in the archive
|
||||||
(default)
|
(default)
|
||||||
--break-per-input Alters --max-downloads, --break-on-existing,
|
--break-per-input Alters --max-downloads, --break-on-existing,
|
||||||
--break-match-filter, and autonumber to
|
--break-match-filters, and autonumber to
|
||||||
reset per input URL
|
reset per input URL
|
||||||
--no-break-per-input --break-on-existing and similar options
|
--no-break-per-input --break-on-existing and similar options
|
||||||
terminates the entire download queue
|
terminates the entire download queue
|
||||||
@ -999,12 +999,16 @@ ## Post-Processing Options:
|
|||||||
be used multiple times
|
be used multiple times
|
||||||
--no-exec Remove any previously defined --exec
|
--no-exec Remove any previously defined --exec
|
||||||
--convert-subs FORMAT Convert the subtitles to another format
|
--convert-subs FORMAT Convert the subtitles to another format
|
||||||
(currently supported: ass, lrc, srt, vtt)
|
(currently supported: ass, lrc, srt, vtt).
|
||||||
(Alias: --convert-subtitles)
|
Use "--convert-subs none" to disable
|
||||||
|
conversion (default) (Alias: --convert-
|
||||||
|
subtitles)
|
||||||
--convert-thumbnails FORMAT Convert the thumbnails to another format
|
--convert-thumbnails FORMAT Convert the thumbnails to another format
|
||||||
(currently supported: jpg, png, webp). You
|
(currently supported: jpg, png, webp). You
|
||||||
can specify multiple rules using similar
|
can specify multiple rules using similar
|
||||||
syntax as --remux-video
|
syntax as "--remux-video". Use "--convert-
|
||||||
|
thumbnails none" to disable conversion
|
||||||
|
(default)
|
||||||
--split-chapters Split video into multiple files based on
|
--split-chapters Split video into multiple files based on
|
||||||
internal chapters. The "chapter:" prefix can
|
internal chapters. The "chapter:" prefix can
|
||||||
be used with "--paths" and "--output" to set
|
be used with "--paths" and "--output" to set
|
||||||
@ -1767,7 +1771,7 @@ # EXTRACTOR ARGUMENTS
|
|||||||
#### youtube
|
#### youtube
|
||||||
* `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube.py](https://github.com/yt-dlp/yt-dlp/blob/c26f9b991a0681fd3ea548d535919cec1fbbd430/yt_dlp/extractor/youtube.py#L381-L390) for list of supported content language codes
|
* `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube.py](https://github.com/yt-dlp/yt-dlp/blob/c26f9b991a0681fd3ea548d535919cec1fbbd430/yt_dlp/extractor/youtube.py#L381-L390) for list of supported content language codes
|
||||||
* `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively
|
* `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively
|
||||||
* `player_client`: Clients to extract video data from. The main clients are `web`, `ios` and `android`, with variants `_music` and `_creator` (e.g. `ios_creator`); and `mediaconnect`, `mweb`, `android_producer`, `android_testsuite`, `android_vr`, `web_safari`, `web_embedded`, `tv` and `tv_embedded` with no variants. By default, `ios,web_creator` is used, and `tv_embedded`, `web_creator` and `mediaconnect` are added as required for age-gated videos. Similarly, the music variants are added for `music.youtube.com` urls. Most `android` clients will be given lowest priority since their formats are broken. You can use `all` to use all the clients, and `default` for the default clients.
|
* `player_client`: Clients to extract video data from. The main clients are `web`, `ios` and `android`, with variants `_music` and `_creator` (e.g. `ios_creator`); and `mediaconnect`, `mweb`, `android_producer`, `android_testsuite`, `android_vr`, `web_safari`, `web_embedded`, `tv` and `tv_embedded` with no variants. By default, `ios,mweb` is used, and `tv_embedded`, `web_creator` and `mediaconnect` are added as required for age-gated videos. Similarly, the music variants are added for `music.youtube.com` urls. Most `android` clients will be given lowest priority since their formats are broken. You can use `all` to use all the clients, and `default` for the default clients. You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=all,-web`
|
||||||
* `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
|
* `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
|
||||||
* `player_params`: YouTube player parameters to use for player requests. Will overwrite any default ones set by yt-dlp.
|
* `player_params`: YouTube player parameters to use for player requests. Will overwrite any default ones set by yt-dlp.
|
||||||
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side)
|
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side)
|
||||||
@ -1777,6 +1781,9 @@ #### youtube
|
|||||||
* `innertube_host`: Innertube API host to use for all API requests; e.g. `studio.youtube.com`, `youtubei.googleapis.com`. Note that cookies exported from one subdomain will not work on others
|
* `innertube_host`: Innertube API host to use for all API requests; e.g. `studio.youtube.com`, `youtubei.googleapis.com`. Note that cookies exported from one subdomain will not work on others
|
||||||
* `innertube_key`: Innertube API key to use for all API requests. By default, no API key is used
|
* `innertube_key`: Innertube API key to use for all API requests. By default, no API key is used
|
||||||
* `raise_incomplete_data`: `Incomplete Data Received` raises an error instead of reporting a warning
|
* `raise_incomplete_data`: `Incomplete Data Received` raises an error instead of reporting a warning
|
||||||
|
* `data_sync_id`: Overrides the account Data Sync ID used in Innertube API requests. This may be needed if you are using an account with `youtube:player_skip=webpage,configs` or `youtubetab:skip=webpage`
|
||||||
|
* `visitor_data`: Overrides the Visitor Data used in Innertube API requests. This should be used with `player_skip=webpage,configs` and without cookies. Note: this may have adverse effects if used improperly. If a session from a browser is wanted, you should pass cookies instead (which contain the Visitor ID)
|
||||||
|
* `po_token`: Proof of Origin (PO) Token(s) to use for requesting video playback. Comma seperated list of PO Tokens in the format `CLIENT+PO_TOKEN`, e.g. `youtube:po_token=web+XXX,android+YYY`
|
||||||
|
|
||||||
#### youtubetab (YouTube playlists, channels, feeds, etc.)
|
#### youtubetab (YouTube playlists, channels, feeds, etc.)
|
||||||
* `skip`: One or more of `webpage` (skip initial webpage download), `authcheck` (allow the download of playlists requiring authentication when no initial webpage is downloaded. This may cause unwanted behavior, see [#1122](https://github.com/yt-dlp/yt-dlp/pull/1122) for more details)
|
* `skip`: One or more of `webpage` (skip initial webpage download), `authcheck` (allow the download of playlists requiring authentication when no initial webpage is downloaded. This may cause unwanted behavior, see [#1122](https://github.com/yt-dlp/yt-dlp/pull/1122) for more details)
|
||||||
@ -1788,6 +1795,7 @@ #### generic
|
|||||||
* `key_query`: Passthrough the master m3u8 URL query to its HLS AES-128 decryption key URI if no value is provided, or else apply the query string given as `key_query=VALUE`. Note that this will have no effect if the key URI is provided via the `hls_key` extractor-arg. Does not apply to ffmpeg
|
* `key_query`: Passthrough the master m3u8 URL query to its HLS AES-128 decryption key URI if no value is provided, or else apply the query string given as `key_query=VALUE`. Note that this will have no effect if the key URI is provided via the `hls_key` extractor-arg. Does not apply to ffmpeg
|
||||||
* `hls_key`: An HLS AES-128 key URI *or* key (as hex), and optionally the IV (as hex), in the form of `(URI|KEY)[,IV]`; e.g. `generic:hls_key=ABCDEF1234567980,0xFEDCBA0987654321`. Passing any of these values will force usage of the native HLS downloader and override the corresponding values found in the m3u8 playlist
|
* `hls_key`: An HLS AES-128 key URI *or* key (as hex), and optionally the IV (as hex), in the form of `(URI|KEY)[,IV]`; e.g. `generic:hls_key=ABCDEF1234567980,0xFEDCBA0987654321`. Passing any of these values will force usage of the native HLS downloader and override the corresponding values found in the m3u8 playlist
|
||||||
* `is_live`: Bypass live HLS detection and manually set `live_status` - a value of `false` will set `not_live`, any other value (or no value) will set `is_live`
|
* `is_live`: Bypass live HLS detection and manually set `live_status` - a value of `false` will set `not_live`, any other value (or no value) will set `is_live`
|
||||||
|
* `impersonate`: Target(s) to try and impersonate with the initial webpage request; e.g. `safari,chrome-110`. By default any available target will be used. Use `false` to disable impersonation
|
||||||
|
|
||||||
#### funimation
|
#### funimation
|
||||||
* `language`: Audio languages to extract, e.g. `funimation:language=english,japanese`
|
* `language`: Audio languages to extract, e.g. `funimation:language=english,japanese`
|
||||||
@ -1890,6 +1898,7 @@ # PLUGINS
|
|||||||
myplugin.py
|
myplugin.py
|
||||||
|
|
||||||
yt-dlp looks for these `yt_dlp_plugins` namespace folders in many locations (see below) and loads in plugins from **all** of them.
|
yt-dlp looks for these `yt_dlp_plugins` namespace folders in many locations (see below) and loads in plugins from **all** of them.
|
||||||
|
Set the environment variable `YTDLP_NO_PLUGINS` to something nonempty to disable loading plugins entirely.
|
||||||
|
|
||||||
See the [wiki for some known plugins](https://github.com/yt-dlp/yt-dlp/wiki/Plugins)
|
See the [wiki for some known plugins](https://github.com/yt-dlp/yt-dlp/wiki/Plugins)
|
||||||
|
|
||||||
@ -2177,9 +2186,9 @@ ### New features
|
|||||||
|
|
||||||
* **Output template improvements**: Output templates can now have date-time formatting, numeric offsets, object traversal etc. See [output template](#output-template) for details. Even more advanced operations can also be done with the help of `--parse-metadata` and `--replace-in-metadata`
|
* **Output template improvements**: Output templates can now have date-time formatting, numeric offsets, object traversal etc. See [output template](#output-template) for details. Even more advanced operations can also be done with the help of `--parse-metadata` and `--replace-in-metadata`
|
||||||
|
|
||||||
* **Other new options**: Many new options have been added such as `--alias`, `--print`, `--concat-playlist`, `--wait-for-video`, `--retry-sleep`, `--sleep-requests`, `--convert-thumbnails`, `--force-download-archive`, `--force-overwrites`, `--break-match-filter` etc
|
* **Other new options**: Many new options have been added such as `--alias`, `--print`, `--concat-playlist`, `--wait-for-video`, `--retry-sleep`, `--sleep-requests`, `--convert-thumbnails`, `--force-download-archive`, `--force-overwrites`, `--break-match-filters` etc
|
||||||
|
|
||||||
* **Improvements**: Regex and other operators in `--format`/`--match-filter`, multiple `--postprocessor-args` and `--downloader-args`, faster archive checking, more [format selection options](#format-selection), merge multi-video/audio, multiple `--config-locations`, `--exec` at different stages, etc
|
* **Improvements**: Regex and other operators in `--format`/`--match-filters`, multiple `--postprocessor-args` and `--downloader-args`, faster archive checking, more [format selection options](#format-selection), merge multi-video/audio, multiple `--config-locations`, `--exec` at different stages, etc
|
||||||
|
|
||||||
* **Plugins**: Extractors and PostProcessors can be loaded from an external file. See [plugins](#plugins) for details
|
* **Plugins**: Extractors and PostProcessors can be loaded from an external file. See [plugins](#plugins) for details
|
||||||
|
|
||||||
@ -2220,7 +2229,7 @@ ### Differences in default behavior
|
|||||||
* `certifi` will be used for SSL root certificates, if installed. If you want to use system certificates (e.g. self-signed), use `--compat-options no-certifi`
|
* `certifi` will be used for SSL root certificates, if installed. If you want to use system certificates (e.g. self-signed), use `--compat-options no-certifi`
|
||||||
* yt-dlp's sanitization of invalid characters in filenames is different/smarter than in youtube-dl. You can use `--compat-options filename-sanitization` to revert to youtube-dl's behavior
|
* yt-dlp's sanitization of invalid characters in filenames is different/smarter than in youtube-dl. You can use `--compat-options filename-sanitization` to revert to youtube-dl's behavior
|
||||||
* ~~yt-dlp tries to parse the external downloader outputs into the standard progress output if possible (Currently implemented: [aria2c](https://github.com/yt-dlp/yt-dlp/issues/5931)). You can use `--compat-options no-external-downloader-progress` to get the downloader output as-is~~
|
* ~~yt-dlp tries to parse the external downloader outputs into the standard progress output if possible (Currently implemented: [aria2c](https://github.com/yt-dlp/yt-dlp/issues/5931)). You can use `--compat-options no-external-downloader-progress` to get the downloader output as-is~~
|
||||||
* yt-dlp versions between 2021.09.01 and 2023.01.02 applies `--match-filter` to nested playlists. This was an unintentional side-effect of [8f18ac](https://github.com/yt-dlp/yt-dlp/commit/8f18aca8717bb0dd49054555af8d386e5eda3a88) and is fixed in [d7b460](https://github.com/yt-dlp/yt-dlp/commit/d7b460d0e5fc710950582baed2e3fc616ed98a80). Use `--compat-options playlist-match-filter` to revert this
|
* yt-dlp versions between 2021.09.01 and 2023.01.02 applies `--match-filters` to nested playlists. This was an unintentional side-effect of [8f18ac](https://github.com/yt-dlp/yt-dlp/commit/8f18aca8717bb0dd49054555af8d386e5eda3a88) and is fixed in [d7b460](https://github.com/yt-dlp/yt-dlp/commit/d7b460d0e5fc710950582baed2e3fc616ed98a80). Use `--compat-options playlist-match-filter` to revert this
|
||||||
* yt-dlp versions between 2021.11.10 and 2023.06.21 estimated `filesize_approx` values for fragmented/manifest formats. This was added for convenience in [f2fe69](https://github.com/yt-dlp/yt-dlp/commit/f2fe69c7b0d208bdb1f6292b4ae92bc1e1a7444a), but was reverted in [0dff8e](https://github.com/yt-dlp/yt-dlp/commit/0dff8e4d1e6e9fb938f4256ea9af7d81f42fd54f) due to the potentially extreme inaccuracy of the estimated values. Use `--compat-options manifest-filesize-approx` to keep extracting the estimated values
|
* yt-dlp versions between 2021.11.10 and 2023.06.21 estimated `filesize_approx` values for fragmented/manifest formats. This was added for convenience in [f2fe69](https://github.com/yt-dlp/yt-dlp/commit/f2fe69c7b0d208bdb1f6292b4ae92bc1e1a7444a), but was reverted in [0dff8e](https://github.com/yt-dlp/yt-dlp/commit/0dff8e4d1e6e9fb938f4256ea9af7d81f42fd54f) due to the potentially extreme inaccuracy of the estimated values. Use `--compat-options manifest-filesize-approx` to keep extracting the estimated values
|
||||||
* yt-dlp uses modern http client backends such as `requests`. Use `--compat-options prefer-legacy-http-handler` to prefer the legacy http handler (`urllib`) to be used for standard http requests.
|
* yt-dlp uses modern http client backends such as `requests`. Use `--compat-options prefer-legacy-http-handler` to prefer the legacy http handler (`urllib`) to be used for standard http requests.
|
||||||
* The sub-modules `swfinterp`, `casefold` are removed.
|
* The sub-modules `swfinterp`, `casefold` are removed.
|
||||||
@ -2266,11 +2275,11 @@ #### Redundant options
|
|||||||
--get-thumbnail --print thumbnail
|
--get-thumbnail --print thumbnail
|
||||||
-e, --get-title --print title
|
-e, --get-title --print title
|
||||||
-g, --get-url --print urls
|
-g, --get-url --print urls
|
||||||
--match-title REGEX --match-filter "title ~= (?i)REGEX"
|
--match-title REGEX --match-filters "title ~= (?i)REGEX"
|
||||||
--reject-title REGEX --match-filter "title !~= (?i)REGEX"
|
--reject-title REGEX --match-filters "title !~= (?i)REGEX"
|
||||||
--min-views COUNT --match-filter "view_count >=? COUNT"
|
--min-views COUNT --match-filters "view_count >=? COUNT"
|
||||||
--max-views COUNT --match-filter "view_count <=? COUNT"
|
--max-views COUNT --match-filters "view_count <=? COUNT"
|
||||||
--break-on-reject Use --break-match-filter
|
--break-on-reject Use --break-match-filters
|
||||||
--user-agent UA --add-header "User-Agent:UA"
|
--user-agent UA --add-header "User-Agent:UA"
|
||||||
--referer URL --add-header "Referer:URL"
|
--referer URL --add-header "Referer:URL"
|
||||||
--playlist-start NUMBER -I NUMBER:
|
--playlist-start NUMBER -I NUMBER:
|
||||||
|
@ -185,5 +185,16 @@
|
|||||||
"action": "add",
|
"action": "add",
|
||||||
"when": "6075a029dba70a89675ae1250e7cdfd91f0eba41",
|
"when": "6075a029dba70a89675ae1250e7cdfd91f0eba41",
|
||||||
"short": "[priority] Security: [[ie/douyutv] Do not use dangerous javascript source/URL](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-3v33-3wmw-3785)\n - A dependency on potentially malicious third-party JavaScript code has been removed from the Douyu extractors"
|
"short": "[priority] Security: [[ie/douyutv] Do not use dangerous javascript source/URL](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-3v33-3wmw-3785)\n - A dependency on potentially malicious third-party JavaScript code has been removed from the Douyu extractors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "add",
|
||||||
|
"when": "fb8b7f226d251e521a89b23c415e249e5b788e5c",
|
||||||
|
"short": "[priority] **The minimum *recommended* Python version has been raised to 3.9**\nSince Python 3.8 will reach end-of-life in October 2024, support for it will be dropped soon. [Read more](https://github.com/yt-dlp/yt-dlp/issues/10086)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "change",
|
||||||
|
"when": "b31b81d85f00601710d4fac590c3e4efb4133283",
|
||||||
|
"short": "[ci] Rerun failed tests (#11143)",
|
||||||
|
"authors": ["Grub4K"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -46,6 +46,14 @@
|
|||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> [!CAUTION]
|
||||||
|
> ### GitHub is experiencing a high volume of malicious spam comments.
|
||||||
|
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
||||||
|
>
|
||||||
|
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
||||||
'''.strip()
|
'''.strip()
|
||||||
|
|
||||||
NO_SKIP = '''
|
NO_SKIP = '''
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
# Allow direct execution
|
# Allow direct execution
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
@ -34,18 +33,14 @@ class {name}({bases}):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
os.environ['YTDLP_NO_PLUGINS'] = 'true'
|
||||||
|
os.environ['YTDLP_NO_LAZY_EXTRACTORS'] = 'true'
|
||||||
|
|
||||||
lazy_extractors_filename = get_filename_args(default_outfile='yt_dlp/extractor/lazy_extractors.py')
|
lazy_extractors_filename = get_filename_args(default_outfile='yt_dlp/extractor/lazy_extractors.py')
|
||||||
if os.path.exists(lazy_extractors_filename):
|
|
||||||
os.remove(lazy_extractors_filename)
|
|
||||||
|
|
||||||
_ALL_CLASSES = get_all_ies() # Must be before import
|
from yt_dlp.extractor.extractors import _ALL_CLASSES
|
||||||
|
|
||||||
import yt_dlp.plugins
|
|
||||||
from yt_dlp.extractor.common import InfoExtractor, SearchInfoExtractor
|
from yt_dlp.extractor.common import InfoExtractor, SearchInfoExtractor
|
||||||
|
|
||||||
# Filter out plugins
|
|
||||||
_ALL_CLASSES = [cls for cls in _ALL_CLASSES if not cls.__module__.startswith(f'{yt_dlp.plugins.PACKAGE_NAME}.')]
|
|
||||||
|
|
||||||
DummyInfoExtractor = type('InfoExtractor', (InfoExtractor,), {'IE_NAME': NO_ATTR})
|
DummyInfoExtractor = type('InfoExtractor', (InfoExtractor,), {'IE_NAME': NO_ATTR})
|
||||||
module_src = '\n'.join((
|
module_src = '\n'.join((
|
||||||
MODULE_TEMPLATE,
|
MODULE_TEMPLATE,
|
||||||
@ -58,20 +53,6 @@ def main():
|
|||||||
write_file(lazy_extractors_filename, f'{module_src}\n')
|
write_file(lazy_extractors_filename, f'{module_src}\n')
|
||||||
|
|
||||||
|
|
||||||
def get_all_ies():
|
|
||||||
PLUGINS_DIRNAME = 'ytdlp_plugins'
|
|
||||||
BLOCKED_DIRNAME = f'{PLUGINS_DIRNAME}_blocked'
|
|
||||||
if os.path.exists(PLUGINS_DIRNAME):
|
|
||||||
# os.rename cannot be used, e.g. in Docker. See https://github.com/yt-dlp/yt-dlp/pull/4958
|
|
||||||
shutil.move(PLUGINS_DIRNAME, BLOCKED_DIRNAME)
|
|
||||||
try:
|
|
||||||
from yt_dlp.extractor.extractors import _ALL_CLASSES
|
|
||||||
finally:
|
|
||||||
if os.path.exists(BLOCKED_DIRNAME):
|
|
||||||
shutil.move(BLOCKED_DIRNAME, PLUGINS_DIRNAME)
|
|
||||||
return _ALL_CLASSES
|
|
||||||
|
|
||||||
|
|
||||||
def extra_ie_code(ie, base=None):
|
def extra_ie_code(ie, base=None):
|
||||||
for var in STATIC_CLASS_PROPERTIES:
|
for var in STATIC_CLASS_PROPERTIES:
|
||||||
val = getattr(ie, var)
|
val = getattr(ie, var)
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
def parse_args():
|
def parse_args():
|
||||||
parser = argparse.ArgumentParser(description='Run selected yt-dlp tests')
|
parser = argparse.ArgumentParser(description='Run selected yt-dlp tests')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'test', help='a extractor tests, or one of "core" or "download"', nargs='*')
|
'test', help='an extractor test, test path, or one of "core" or "download"', nargs='*')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-k', help='run a test matching EXPRESSION. Same as "pytest -k"', metavar='EXPRESSION')
|
'-k', help='run a test matching EXPRESSION. Same as "pytest -k"', metavar='EXPRESSION')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -27,7 +27,6 @@ def parse_args():
|
|||||||
def run_tests(*tests, pattern=None, ci=False):
|
def run_tests(*tests, pattern=None, ci=False):
|
||||||
run_core = 'core' in tests or (not pattern and not tests)
|
run_core = 'core' in tests or (not pattern and not tests)
|
||||||
run_download = 'download' in tests
|
run_download = 'download' in tests
|
||||||
tests = list(map(fix_test_name, tests))
|
|
||||||
|
|
||||||
pytest_args = args.pytest_args or os.getenv('HATCH_TEST_ARGS', '')
|
pytest_args = args.pytest_args or os.getenv('HATCH_TEST_ARGS', '')
|
||||||
arguments = ['pytest', '-Werror', '--tb=short', *shlex.split(pytest_args)]
|
arguments = ['pytest', '-Werror', '--tb=short', *shlex.split(pytest_args)]
|
||||||
@ -41,7 +40,9 @@ def run_tests(*tests, pattern=None, ci=False):
|
|||||||
arguments.extend(['-m', 'download'])
|
arguments.extend(['-m', 'download'])
|
||||||
else:
|
else:
|
||||||
arguments.extend(
|
arguments.extend(
|
||||||
f'test/test_download.py::TestDownload::test_{test}' for test in tests)
|
test if '/' in test
|
||||||
|
else f'test/test_download.py::TestDownload::test_{fix_test_name(test)}'
|
||||||
|
for test in tests)
|
||||||
|
|
||||||
print(f'Running {arguments}', flush=True)
|
print(f'Running {arguments}', flush=True)
|
||||||
try:
|
try:
|
||||||
|
@ -56,7 +56,7 @@ dependencies = [
|
|||||||
default = []
|
default = []
|
||||||
curl-cffi = [
|
curl-cffi = [
|
||||||
"curl-cffi==0.5.10; os_name=='nt' and implementation_name=='cpython'",
|
"curl-cffi==0.5.10; os_name=='nt' and implementation_name=='cpython'",
|
||||||
"curl-cffi>=0.5.10,!=0.6.*,<0.8; os_name!='nt' and implementation_name=='cpython'",
|
"curl-cffi>=0.5.10,!=0.6.*,<0.7.2; os_name!='nt' and implementation_name=='cpython'",
|
||||||
]
|
]
|
||||||
secretstorage = [
|
secretstorage = [
|
||||||
"cffi",
|
"cffi",
|
||||||
@ -76,13 +76,14 @@ dev = [
|
|||||||
]
|
]
|
||||||
static-analysis = [
|
static-analysis = [
|
||||||
"autopep8~=2.0",
|
"autopep8~=2.0",
|
||||||
"ruff~=0.5.0",
|
"ruff~=0.6.0",
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
"pytest~=8.1",
|
"pytest~=8.1",
|
||||||
|
"pytest-rerunfailures~=14.0",
|
||||||
]
|
]
|
||||||
pyinstaller = [
|
pyinstaller = [
|
||||||
"pyinstaller>=6.7.0", # for compat with setuptools>=70
|
"pyinstaller>=6.10.0", # Windows temp cleanup fixed in 6.10.0
|
||||||
]
|
]
|
||||||
py2exe = [
|
py2exe = [
|
||||||
"py2exe>=0.12",
|
"py2exe>=0.12",
|
||||||
@ -162,7 +163,6 @@ lint-fix = "ruff check --fix {args:.}"
|
|||||||
features = ["test"]
|
features = ["test"]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pytest-randomly~=3.15",
|
"pytest-randomly~=3.15",
|
||||||
"pytest-rerunfailures~=14.0",
|
|
||||||
"pytest-xdist[psutil]~=3.5",
|
"pytest-xdist[psutil]~=3.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -143,6 +143,7 @@ # Supported sites
|
|||||||
- **BBVTV**: [*bbvtv*](## "netrc machine")
|
- **BBVTV**: [*bbvtv*](## "netrc machine")
|
||||||
- **BBVTVLive**: [*bbvtv*](## "netrc machine")
|
- **BBVTVLive**: [*bbvtv*](## "netrc machine")
|
||||||
- **BBVTVRecordings**: [*bbvtv*](## "netrc machine")
|
- **BBVTVRecordings**: [*bbvtv*](## "netrc machine")
|
||||||
|
- **BeaconTv**
|
||||||
- **BeatBumpPlaylist**
|
- **BeatBumpPlaylist**
|
||||||
- **BeatBumpVideo**
|
- **BeatBumpVideo**
|
||||||
- **Beatport**
|
- **Beatport**
|
||||||
@ -505,6 +506,7 @@ # Supported sites
|
|||||||
- **gem.cbc.ca:playlist**
|
- **gem.cbc.ca:playlist**
|
||||||
- **Genius**
|
- **Genius**
|
||||||
- **GeniusLyrics**
|
- **GeniusLyrics**
|
||||||
|
- **Germanupa**: germanupa.de
|
||||||
- **GetCourseRu**: [*getcourseru*](## "netrc machine")
|
- **GetCourseRu**: [*getcourseru*](## "netrc machine")
|
||||||
- **GetCourseRuPlayer**
|
- **GetCourseRuPlayer**
|
||||||
- **Gettr**
|
- **Gettr**
|
||||||
@ -580,6 +582,7 @@ # Supported sites
|
|||||||
- **HungamaAlbumPlaylist**
|
- **HungamaAlbumPlaylist**
|
||||||
- **HungamaSong**
|
- **HungamaSong**
|
||||||
- **huya:live**: huya.com
|
- **huya:live**: huya.com
|
||||||
|
- **huya:video**: 虎牙视频
|
||||||
- **Hypem**
|
- **Hypem**
|
||||||
- **Hytale**
|
- **Hytale**
|
||||||
- **Icareus**
|
- **Icareus**
|
||||||
@ -660,6 +663,7 @@ # Supported sites
|
|||||||
- **kick:vod**
|
- **kick:vod**
|
||||||
- **Kicker**
|
- **Kicker**
|
||||||
- **KickStarter**
|
- **KickStarter**
|
||||||
|
- **Kika**: KiKA.de
|
||||||
- **kinja:embed**
|
- **kinja:embed**
|
||||||
- **KinoPoisk**
|
- **KinoPoisk**
|
||||||
- **Kommunetv**
|
- **Kommunetv**
|
||||||
@ -722,7 +726,6 @@ # Supported sites
|
|||||||
- **livestream:original**
|
- **livestream:original**
|
||||||
- **Livestreamfails**
|
- **Livestreamfails**
|
||||||
- **Lnk**
|
- **Lnk**
|
||||||
- **LnkGo**
|
|
||||||
- **loc**: Library of Congress
|
- **loc**: Library of Congress
|
||||||
- **loom**
|
- **loom**
|
||||||
- **loom:folder**
|
- **loom:folder**
|
||||||
@ -756,7 +759,7 @@ # Supported sites
|
|||||||
- **Masters**
|
- **Masters**
|
||||||
- **MatchTV**
|
- **MatchTV**
|
||||||
- **MBN**: mbn.co.kr (매일방송)
|
- **MBN**: mbn.co.kr (매일방송)
|
||||||
- **MDR**: MDR.DE and KiKA
|
- **MDR**: MDR.DE
|
||||||
- **MedalTV**
|
- **MedalTV**
|
||||||
- **media.ccc.de**
|
- **media.ccc.de**
|
||||||
- **media.ccc.de:lists**
|
- **media.ccc.de:lists**
|
||||||
@ -811,6 +814,7 @@ # Supported sites
|
|||||||
- **MNetTVLive**: [*mnettv*](## "netrc machine")
|
- **MNetTVLive**: [*mnettv*](## "netrc machine")
|
||||||
- **MNetTVRecordings**: [*mnettv*](## "netrc machine")
|
- **MNetTVRecordings**: [*mnettv*](## "netrc machine")
|
||||||
- **MochaVideo**
|
- **MochaVideo**
|
||||||
|
- **Mojevideo**: mojevideo.sk
|
||||||
- **Mojvideo**
|
- **Mojvideo**
|
||||||
- **Monstercat**
|
- **Monstercat**
|
||||||
- **MonsterSirenHypergryphMusic**
|
- **MonsterSirenHypergryphMusic**
|
||||||
@ -1285,12 +1289,14 @@ # Supported sites
|
|||||||
- **Screencast**
|
- **Screencast**
|
||||||
- **Screencastify**
|
- **Screencastify**
|
||||||
- **ScreencastOMatic**
|
- **ScreencastOMatic**
|
||||||
|
- **ScreenRec**
|
||||||
- **ScrippsNetworks**
|
- **ScrippsNetworks**
|
||||||
- **scrippsnetworks:watch**
|
- **scrippsnetworks:watch**
|
||||||
- **Scrolller**
|
- **Scrolller**
|
||||||
- **SCTE**: [*scte*](## "netrc machine") (**Currently broken**)
|
- **SCTE**: [*scte*](## "netrc machine") (**Currently broken**)
|
||||||
- **SCTECourse**: [*scte*](## "netrc machine") (**Currently broken**)
|
- **SCTECourse**: [*scte*](## "netrc machine") (**Currently broken**)
|
||||||
- **sejm**
|
- **sejm**
|
||||||
|
- **Sen**
|
||||||
- **SenalColombiaLive**: (**Currently broken**)
|
- **SenalColombiaLive**: (**Currently broken**)
|
||||||
- **SenateGov**
|
- **SenateGov**
|
||||||
- **SenateISVP**
|
- **SenateISVP**
|
||||||
@ -1327,6 +1333,7 @@ # Supported sites
|
|||||||
- **SlidesLive**
|
- **SlidesLive**
|
||||||
- **Slutload**
|
- **Slutload**
|
||||||
- **Smotrim**
|
- **Smotrim**
|
||||||
|
- **SnapchatSpotlight**
|
||||||
- **Snotr**
|
- **Snotr**
|
||||||
- **Sohu**
|
- **Sohu**
|
||||||
- **SohuV**
|
- **SohuV**
|
||||||
@ -1608,6 +1615,7 @@ # Supported sites
|
|||||||
- **videomore:season**
|
- **videomore:season**
|
||||||
- **videomore:video**
|
- **videomore:video**
|
||||||
- **VideoPress**
|
- **VideoPress**
|
||||||
|
- **Vidflex**
|
||||||
- **Vidio**: [*vidio*](## "netrc machine")
|
- **Vidio**: [*vidio*](## "netrc machine")
|
||||||
- **VidioLive**: [*vidio*](## "netrc machine")
|
- **VidioLive**: [*vidio*](## "netrc machine")
|
||||||
- **VidioPremier**: [*vidio*](## "netrc machine")
|
- **VidioPremier**: [*vidio*](## "netrc machine")
|
||||||
@ -1736,7 +1744,7 @@ # Supported sites
|
|||||||
- **XiaoHongShu**: 小红书
|
- **XiaoHongShu**: 小红书
|
||||||
- **ximalaya**: 喜马拉雅FM
|
- **ximalaya**: 喜马拉雅FM
|
||||||
- **ximalaya:album**: 喜马拉雅FM 专辑
|
- **ximalaya:album**: 喜马拉雅FM 专辑
|
||||||
- **xinpianchang**: xinpianchang.com (**Currently broken**)
|
- **Xinpianchang**: 新片场
|
||||||
- **XMinus**: (**Currently broken**)
|
- **XMinus**: (**Currently broken**)
|
||||||
- **XNXX**
|
- **XNXX**
|
||||||
- **Xstream**
|
- **Xstream**
|
||||||
|
@ -236,6 +236,35 @@ def test_format_selection_video(self):
|
|||||||
downloaded = ydl.downloaded_info_dicts[0]
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
self.assertEqual(downloaded['format_id'], 'vid-vcodec-dot')
|
self.assertEqual(downloaded['format_id'], 'vid-vcodec-dot')
|
||||||
|
|
||||||
|
def test_format_selection_by_vcodec_sort(self):
|
||||||
|
formats = [
|
||||||
|
{'format_id': 'av1-format', 'ext': 'mp4', 'vcodec': 'av1', 'acodec': 'none', 'url': TEST_URL},
|
||||||
|
{'format_id': 'vp9-hdr-format', 'ext': 'mp4', 'vcodec': 'vp09.02.50.10.01.09.18.09.00', 'acodec': 'none', 'url': TEST_URL},
|
||||||
|
{'format_id': 'vp9-sdr-format', 'ext': 'mp4', 'vcodec': 'vp09.00.50.08', 'acodec': 'none', 'url': TEST_URL},
|
||||||
|
{'format_id': 'h265-format', 'ext': 'mp4', 'vcodec': 'h265', 'acodec': 'none', 'url': TEST_URL},
|
||||||
|
]
|
||||||
|
info_dict = _make_result(formats)
|
||||||
|
|
||||||
|
ydl = YDL({'format': 'bestvideo', 'format_sort': ['vcodec:vp9.2']})
|
||||||
|
ydl.process_ie_result(info_dict.copy())
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(downloaded['format_id'], 'vp9-hdr-format')
|
||||||
|
|
||||||
|
ydl = YDL({'format': 'bestvideo', 'format_sort': ['vcodec:vp9']})
|
||||||
|
ydl.process_ie_result(info_dict.copy())
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(downloaded['format_id'], 'vp9-sdr-format')
|
||||||
|
|
||||||
|
ydl = YDL({'format': 'bestvideo', 'format_sort': ['+vcodec:vp9.2']})
|
||||||
|
ydl.process_ie_result(info_dict.copy())
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(downloaded['format_id'], 'vp9-hdr-format')
|
||||||
|
|
||||||
|
ydl = YDL({'format': 'bestvideo', 'format_sort': ['+vcodec:vp9']})
|
||||||
|
ydl.process_ie_result(info_dict.copy())
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(downloaded['format_id'], 'vp9-sdr-format')
|
||||||
|
|
||||||
def test_format_selection_string_ops(self):
|
def test_format_selection_string_ops(self):
|
||||||
formats = [
|
formats = [
|
||||||
{'format_id': 'abc-cba', 'ext': 'mp4', 'url': TEST_URL},
|
{'format_id': 'abc-cba', 'ext': 'mp4', 'url': TEST_URL},
|
||||||
|
@ -822,6 +822,24 @@ def test_remove_logging_handler(self, handler, logger_name):
|
|||||||
rh.close()
|
rh.close()
|
||||||
assert len(logging_handlers) == before_count
|
assert len(logging_handlers) == before_count
|
||||||
|
|
||||||
|
def test_wrap_request_errors(self):
|
||||||
|
class TestRequestHandler(RequestHandler):
|
||||||
|
def _validate(self, request):
|
||||||
|
if request.headers.get('x-fail'):
|
||||||
|
raise UnsupportedRequest('test error')
|
||||||
|
|
||||||
|
def _send(self, request: Request):
|
||||||
|
raise RequestError('test error')
|
||||||
|
|
||||||
|
with TestRequestHandler(logger=FakeLogger()) as rh:
|
||||||
|
with pytest.raises(UnsupportedRequest, match='test error') as exc_info:
|
||||||
|
rh.validate(Request('http://example.com', headers={'x-fail': '1'}))
|
||||||
|
assert exc_info.value.handler is rh
|
||||||
|
|
||||||
|
with pytest.raises(RequestError, match='test error') as exc_info:
|
||||||
|
rh.send(Request('http://example.com'))
|
||||||
|
assert exc_info.value.handler is rh
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('handler', ['Urllib'], indirect=True)
|
@pytest.mark.parametrize('handler', ['Urllib'], indirect=True)
|
||||||
class TestUrllibRequestHandler(TestRequestHandlerBase):
|
class TestUrllibRequestHandler(TestRequestHandlerBase):
|
||||||
|
@ -4,8 +4,18 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from yt_dlp.utils import dict_get, int_or_none, str_or_none
|
from yt_dlp.utils import (
|
||||||
from yt_dlp.utils.traversal import traverse_obj
|
ExtractorError,
|
||||||
|
determine_ext,
|
||||||
|
dict_get,
|
||||||
|
int_or_none,
|
||||||
|
str_or_none,
|
||||||
|
)
|
||||||
|
from yt_dlp.utils.traversal import (
|
||||||
|
traverse_obj,
|
||||||
|
require,
|
||||||
|
subs_list_to_dict,
|
||||||
|
)
|
||||||
|
|
||||||
_TEST_DATA = {
|
_TEST_DATA = {
|
||||||
100: 100,
|
100: 100,
|
||||||
@ -420,6 +430,71 @@ def test_traversal_morsel(self):
|
|||||||
assert traverse_obj(morsel, [(None,), any]) == morsel, \
|
assert traverse_obj(morsel, [(None,), any]) == morsel, \
|
||||||
'Morsel should not be implicitly changed to dict on usage'
|
'Morsel should not be implicitly changed to dict on usage'
|
||||||
|
|
||||||
|
def test_traversal_filter(self):
|
||||||
|
data = [None, False, True, 0, 1, 0.0, 1.1, '', 'str', {}, {0: 0}, [], [1]]
|
||||||
|
|
||||||
|
assert traverse_obj(data, [..., filter]) == [True, 1, 1.1, 'str', {0: 0}, [1]], \
|
||||||
|
'`filter` should filter falsy values'
|
||||||
|
|
||||||
|
|
||||||
|
class TestTraversalHelpers:
|
||||||
|
def test_traversal_require(self):
|
||||||
|
with pytest.raises(ExtractorError):
|
||||||
|
traverse_obj(_TEST_DATA, ['None', {require('value')}])
|
||||||
|
assert traverse_obj(_TEST_DATA, ['str', {require('value')}]) == 'str', \
|
||||||
|
'`require` should pass through non `None` values'
|
||||||
|
|
||||||
|
def test_subs_list_to_dict(self):
|
||||||
|
assert traverse_obj([
|
||||||
|
{'name': 'de', 'url': 'https://example.com/subs/de.vtt'},
|
||||||
|
{'name': 'en', 'url': 'https://example.com/subs/en1.ass'},
|
||||||
|
{'name': 'en', 'url': 'https://example.com/subs/en2.ass'},
|
||||||
|
], [..., {
|
||||||
|
'id': 'name',
|
||||||
|
'url': 'url',
|
||||||
|
}, all, {subs_list_to_dict}]) == {
|
||||||
|
'de': [{'url': 'https://example.com/subs/de.vtt'}],
|
||||||
|
'en': [
|
||||||
|
{'url': 'https://example.com/subs/en1.ass'},
|
||||||
|
{'url': 'https://example.com/subs/en2.ass'},
|
||||||
|
],
|
||||||
|
}, 'function should build subtitle dict from list of subtitles'
|
||||||
|
assert traverse_obj([
|
||||||
|
{'name': 'de', 'url': 'https://example.com/subs/de.ass'},
|
||||||
|
{'name': 'de'},
|
||||||
|
{'name': 'en', 'content': 'content'},
|
||||||
|
{'url': 'https://example.com/subs/en'},
|
||||||
|
], [..., {
|
||||||
|
'id': 'name',
|
||||||
|
'data': 'content',
|
||||||
|
'url': 'url',
|
||||||
|
}, all, {subs_list_to_dict}]) == {
|
||||||
|
'de': [{'url': 'https://example.com/subs/de.ass'}],
|
||||||
|
'en': [{'data': 'content'}],
|
||||||
|
}, 'subs with mandatory items missing should be filtered'
|
||||||
|
assert traverse_obj([
|
||||||
|
{'url': 'https://example.com/subs/de.ass', 'name': 'de'},
|
||||||
|
{'url': 'https://example.com/subs/en', 'name': 'en'},
|
||||||
|
], [..., {
|
||||||
|
'id': 'name',
|
||||||
|
'ext': ['url', {lambda x: determine_ext(x, default_ext=None)}],
|
||||||
|
'url': 'url',
|
||||||
|
}, all, {subs_list_to_dict(ext='ext')}]) == {
|
||||||
|
'de': [{'url': 'https://example.com/subs/de.ass', 'ext': 'ass'}],
|
||||||
|
'en': [{'url': 'https://example.com/subs/en', 'ext': 'ext'}],
|
||||||
|
}, '`ext` should set default ext but leave existing value untouched'
|
||||||
|
assert traverse_obj([
|
||||||
|
{'name': 'en', 'url': 'https://example.com/subs/en2', 'prio': True},
|
||||||
|
{'name': 'en', 'url': 'https://example.com/subs/en1', 'prio': False},
|
||||||
|
], [..., {
|
||||||
|
'id': 'name',
|
||||||
|
'quality': ['prio', {int}],
|
||||||
|
'url': 'url',
|
||||||
|
}, all, {subs_list_to_dict(ext='ext')}]) == {'en': [
|
||||||
|
{'url': 'https://example.com/subs/en1', 'ext': 'ext'},
|
||||||
|
{'url': 'https://example.com/subs/en2', 'ext': 'ext'},
|
||||||
|
]}, '`quality` key should sort subtitle list accordingly'
|
||||||
|
|
||||||
|
|
||||||
class TestDictGet:
|
class TestDictGet:
|
||||||
def test_dict_get(self):
|
def test_dict_get(self):
|
||||||
|
@ -221,9 +221,10 @@ def test_sanitize_ids(self):
|
|||||||
self.assertEqual(sanitize_filename('N0Y__7-UOdI', is_id=True), 'N0Y__7-UOdI')
|
self.assertEqual(sanitize_filename('N0Y__7-UOdI', is_id=True), 'N0Y__7-UOdI')
|
||||||
|
|
||||||
def test_sanitize_path(self):
|
def test_sanitize_path(self):
|
||||||
if sys.platform != 'win32':
|
with unittest.mock.patch('sys.platform', 'win32'):
|
||||||
return
|
self._test_sanitize_path()
|
||||||
|
|
||||||
|
def _test_sanitize_path(self):
|
||||||
self.assertEqual(sanitize_path('abc'), 'abc')
|
self.assertEqual(sanitize_path('abc'), 'abc')
|
||||||
self.assertEqual(sanitize_path('abc/def'), 'abc\\def')
|
self.assertEqual(sanitize_path('abc/def'), 'abc\\def')
|
||||||
self.assertEqual(sanitize_path('abc\\def'), 'abc\\def')
|
self.assertEqual(sanitize_path('abc\\def'), 'abc\\def')
|
||||||
@ -256,6 +257,11 @@ def test_sanitize_path(self):
|
|||||||
self.assertEqual(sanitize_path('./abc'), 'abc')
|
self.assertEqual(sanitize_path('./abc'), 'abc')
|
||||||
self.assertEqual(sanitize_path('./../abc'), '..\\abc')
|
self.assertEqual(sanitize_path('./../abc'), '..\\abc')
|
||||||
|
|
||||||
|
self.assertEqual(sanitize_path('\\abc'), '\\abc')
|
||||||
|
self.assertEqual(sanitize_path('C:abc'), 'C:abc')
|
||||||
|
self.assertEqual(sanitize_path('C:abc\\..\\'), 'C:..')
|
||||||
|
self.assertEqual(sanitize_path('C:\\abc:%(title)s.%(ext)s'), 'C:\\abc#%(title)s.%(ext)s')
|
||||||
|
|
||||||
def test_sanitize_url(self):
|
def test_sanitize_url(self):
|
||||||
self.assertEqual(sanitize_url('//foo.bar'), 'http://foo.bar')
|
self.assertEqual(sanitize_url('//foo.bar'), 'http://foo.bar')
|
||||||
self.assertEqual(sanitize_url('httpss://foo.bar'), 'https://foo.bar')
|
self.assertEqual(sanitize_url('httpss://foo.bar'), 'https://foo.bar')
|
||||||
@ -921,6 +927,11 @@ def test_parse_codecs(self):
|
|||||||
'acodec': 'none',
|
'acodec': 'none',
|
||||||
'dynamic_range': 'HDR10',
|
'dynamic_range': 'HDR10',
|
||||||
})
|
})
|
||||||
|
self.assertEqual(parse_codecs('vp09.02.50.10.01.09.18.09.00'), {
|
||||||
|
'vcodec': 'vp09.02.50.10.01.09.18.09.00',
|
||||||
|
'acodec': 'none',
|
||||||
|
'dynamic_range': 'HDR10',
|
||||||
|
})
|
||||||
self.assertEqual(parse_codecs('av01.0.12M.10.0.110.09.16.09.0'), {
|
self.assertEqual(parse_codecs('av01.0.12M.10.0.110.09.16.09.0'), {
|
||||||
'vcodec': 'av01.0.12M.10.0.110.09.16.09.0',
|
'vcodec': 'av01.0.12M.10.0.110.09.16.09.0',
|
||||||
'acodec': 'none',
|
'acodec': 'none',
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
from .cache import Cache
|
from .cache import Cache
|
||||||
from .compat import urllib # isort: split
|
from .compat import urllib # isort: split
|
||||||
from .compat import compat_os_name, urllib_req_to_req
|
from .compat import compat_os_name, urllib_req_to_req
|
||||||
from .cookies import LenientSimpleCookie, load_cookies
|
from .cookies import CookieLoadError, LenientSimpleCookie, load_cookies
|
||||||
from .downloader import FFmpegFD, get_suitable_downloader, shorten_protocol_name
|
from .downloader import FFmpegFD, get_suitable_downloader, shorten_protocol_name
|
||||||
from .downloader.rtmp import rtmpdump_version
|
from .downloader.rtmp import rtmpdump_version
|
||||||
from .extractor import gen_extractor_classes, get_info_extractor
|
from .extractor import gen_extractor_classes, get_info_extractor
|
||||||
@ -1624,7 +1624,7 @@ def wrapper(self, *args, **kwargs):
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
except (DownloadCancelled, LazyList.IndexError, PagedList.IndexError):
|
except (CookieLoadError, DownloadCancelled, LazyList.IndexError, PagedList.IndexError):
|
||||||
raise
|
raise
|
||||||
except ReExtractInfo as e:
|
except ReExtractInfo as e:
|
||||||
if e.expected:
|
if e.expected:
|
||||||
@ -3580,6 +3580,8 @@ def __download_wrapper(self, func):
|
|||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
res = func(*args, **kwargs)
|
res = func(*args, **kwargs)
|
||||||
|
except CookieLoadError:
|
||||||
|
raise
|
||||||
except UnavailableVideoError as e:
|
except UnavailableVideoError as e:
|
||||||
self.report_error(e)
|
self.report_error(e)
|
||||||
except DownloadCancelled as e:
|
except DownloadCancelled as e:
|
||||||
@ -4068,6 +4070,10 @@ def get_encoding(stream):
|
|||||||
|
|
||||||
write_debug(f'Proxy map: {self.proxies}')
|
write_debug(f'Proxy map: {self.proxies}')
|
||||||
write_debug(f'Request Handlers: {", ".join(rh.RH_NAME for rh in self._request_director.handlers.values())}')
|
write_debug(f'Request Handlers: {", ".join(rh.RH_NAME for rh in self._request_director.handlers.values())}')
|
||||||
|
if os.environ.get('YTDLP_NO_PLUGINS'):
|
||||||
|
write_debug('Plugins are forcibly disabled')
|
||||||
|
return
|
||||||
|
|
||||||
for plugin_type, plugins in {'Extractor': plugin_ies, 'Post-Processor': plugin_pps}.items():
|
for plugin_type, plugins in {'Extractor': plugin_ies, 'Post-Processor': plugin_pps}.items():
|
||||||
display_list = ['{}{}'.format(
|
display_list = ['{}{}'.format(
|
||||||
klass.__name__, '' if klass.__name__ == name else f' as {name}')
|
klass.__name__, '' if klass.__name__ == name else f' as {name}')
|
||||||
@ -4113,8 +4119,14 @@ def proxies(self):
|
|||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
def cookiejar(self):
|
def cookiejar(self):
|
||||||
"""Global cookiejar instance"""
|
"""Global cookiejar instance"""
|
||||||
return load_cookies(
|
try:
|
||||||
self.params.get('cookiefile'), self.params.get('cookiesfrombrowser'), self)
|
return load_cookies(
|
||||||
|
self.params.get('cookiefile'), self.params.get('cookiesfrombrowser'), self)
|
||||||
|
except CookieLoadError as error:
|
||||||
|
cause = error.__context__
|
||||||
|
# compat: <=py3.9: `traceback.format_exception` has a different signature
|
||||||
|
self.report_error(str(cause), tb=''.join(traceback.format_exception(None, cause, cause.__traceback__)))
|
||||||
|
raise
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _opener(self):
|
def _opener(self):
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from .compat import compat_os_name
|
from .compat import compat_os_name
|
||||||
from .cookies import SUPPORTED_BROWSERS, SUPPORTED_KEYRINGS
|
from .cookies import SUPPORTED_BROWSERS, SUPPORTED_KEYRINGS, CookieLoadError
|
||||||
from .downloader.external import get_external_downloader
|
from .downloader.external import get_external_downloader
|
||||||
from .extractor import list_extractor_classes
|
from .extractor import list_extractor_classes
|
||||||
from .extractor.adobepass import MSO_INFO
|
from .extractor.adobepass import MSO_INFO
|
||||||
@ -235,6 +235,11 @@ def validate_minmax(min_val, max_val, min_name, max_name=None):
|
|||||||
validate_regex('format sorting', f, FormatSorter.regex)
|
validate_regex('format sorting', f, FormatSorter.regex)
|
||||||
|
|
||||||
# Postprocessor formats
|
# Postprocessor formats
|
||||||
|
if opts.convertsubtitles == 'none':
|
||||||
|
opts.convertsubtitles = None
|
||||||
|
if opts.convertthumbnails == 'none':
|
||||||
|
opts.convertthumbnails = None
|
||||||
|
|
||||||
validate_regex('merge output format', opts.merge_output_format,
|
validate_regex('merge output format', opts.merge_output_format,
|
||||||
r'({0})(/({0}))*'.format('|'.join(map(re.escape, FFmpegMergerPP.SUPPORTED_EXTS))))
|
r'({0})(/({0}))*'.format('|'.join(map(re.escape, FFmpegMergerPP.SUPPORTED_EXTS))))
|
||||||
validate_regex('audio format', opts.audioformat, FFmpegExtractAudioPP.FORMAT_RE)
|
validate_regex('audio format', opts.audioformat, FFmpegExtractAudioPP.FORMAT_RE)
|
||||||
@ -1079,7 +1084,7 @@ def main(argv=None):
|
|||||||
_IN_CLI = True
|
_IN_CLI = True
|
||||||
try:
|
try:
|
||||||
_exit(*variadic(_real_main(argv)))
|
_exit(*variadic(_real_main(argv)))
|
||||||
except DownloadError:
|
except (CookieLoadError, DownloadError):
|
||||||
_exit(1)
|
_exit(1)
|
||||||
except SameFileError as e:
|
except SameFileError as e:
|
||||||
_exit(f'ERROR: {e}')
|
_exit(f'ERROR: {e}')
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
from .minicurses import MultilinePrinter, QuietMultilinePrinter
|
from .minicurses import MultilinePrinter, QuietMultilinePrinter
|
||||||
from .utils import (
|
from .utils import (
|
||||||
DownloadError,
|
DownloadError,
|
||||||
|
YoutubeDLError,
|
||||||
Popen,
|
Popen,
|
||||||
error_to_str,
|
error_to_str,
|
||||||
expand_path,
|
expand_path,
|
||||||
@ -86,24 +87,31 @@ def _create_progress_bar(logger):
|
|||||||
return printer
|
return printer
|
||||||
|
|
||||||
|
|
||||||
|
class CookieLoadError(YoutubeDLError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def load_cookies(cookie_file, browser_specification, ydl):
|
def load_cookies(cookie_file, browser_specification, ydl):
|
||||||
cookie_jars = []
|
try:
|
||||||
if browser_specification is not None:
|
cookie_jars = []
|
||||||
browser_name, profile, keyring, container = _parse_browser_specification(*browser_specification)
|
if browser_specification is not None:
|
||||||
cookie_jars.append(
|
browser_name, profile, keyring, container = _parse_browser_specification(*browser_specification)
|
||||||
extract_cookies_from_browser(browser_name, profile, YDLLogger(ydl), keyring=keyring, container=container))
|
cookie_jars.append(
|
||||||
|
extract_cookies_from_browser(browser_name, profile, YDLLogger(ydl), keyring=keyring, container=container))
|
||||||
|
|
||||||
if cookie_file is not None:
|
if cookie_file is not None:
|
||||||
is_filename = is_path_like(cookie_file)
|
is_filename = is_path_like(cookie_file)
|
||||||
if is_filename:
|
if is_filename:
|
||||||
cookie_file = expand_path(cookie_file)
|
cookie_file = expand_path(cookie_file)
|
||||||
|
|
||||||
jar = YoutubeDLCookieJar(cookie_file)
|
jar = YoutubeDLCookieJar(cookie_file)
|
||||||
if not is_filename or os.access(cookie_file, os.R_OK):
|
if not is_filename or os.access(cookie_file, os.R_OK):
|
||||||
jar.load()
|
jar.load()
|
||||||
cookie_jars.append(jar)
|
cookie_jars.append(jar)
|
||||||
|
|
||||||
return _merge_cookie_jars(cookie_jars)
|
return _merge_cookie_jars(cookie_jars)
|
||||||
|
except Exception:
|
||||||
|
raise CookieLoadError('failed to load cookies')
|
||||||
|
|
||||||
|
|
||||||
def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger(), *, keyring=None, container=None):
|
def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger(), *, keyring=None, container=None):
|
||||||
@ -1053,8 +1061,9 @@ class DATA_BLOB(ctypes.Structure):
|
|||||||
ctypes.byref(blob_out), # pDataOut
|
ctypes.byref(blob_out), # pDataOut
|
||||||
)
|
)
|
||||||
if not ret:
|
if not ret:
|
||||||
logger.warning('failed to decrypt with DPAPI', only_once=True)
|
message = 'Failed to decrypt with DPAPI. See https://github.com/yt-dlp/yt-dlp/issues/10927 for more info'
|
||||||
return None
|
logger.error(message)
|
||||||
|
raise DownloadError(message) # force exit
|
||||||
|
|
||||||
result = ctypes.string_at(blob_out.pbData, blob_out.cbData)
|
result = ctypes.string_at(blob_out.pbData, blob_out.cbData)
|
||||||
ctypes.windll.kernel32.LocalFree(blob_out.pbData)
|
ctypes.windll.kernel32.LocalFree(blob_out.pbData)
|
||||||
|
@ -508,7 +508,7 @@ def _call_downloader(self, tmpfilename, info_dict):
|
|||||||
env = None
|
env = None
|
||||||
proxy = self.params.get('proxy')
|
proxy = self.params.get('proxy')
|
||||||
if proxy:
|
if proxy:
|
||||||
if not re.match(r'^[\da-zA-Z]+://', proxy):
|
if not re.match(r'[\da-zA-Z]+://', proxy):
|
||||||
proxy = f'http://{proxy}'
|
proxy = f'http://{proxy}'
|
||||||
|
|
||||||
if proxy.startswith('socks'):
|
if proxy.startswith('socks'):
|
||||||
@ -559,7 +559,7 @@ def _call_downloader(self, tmpfilename, info_dict):
|
|||||||
|
|
||||||
selected_formats = info_dict.get('requested_formats') or [info_dict]
|
selected_formats = info_dict.get('requested_formats') or [info_dict]
|
||||||
for i, fmt in enumerate(selected_formats):
|
for i, fmt in enumerate(selected_formats):
|
||||||
is_http = re.match(r'^https?://', fmt['url'])
|
is_http = re.match(r'https?://', fmt['url'])
|
||||||
cookies = self.ydl.cookiejar.get_cookies_for_url(fmt['url']) if is_http else []
|
cookies = self.ydl.cookiejar.get_cookies_for_url(fmt['url']) if is_http else []
|
||||||
if cookies:
|
if cookies:
|
||||||
args.extend(['-cookies', ''.join(
|
args.extend(['-cookies', ''.join(
|
||||||
|
@ -217,6 +217,7 @@
|
|||||||
BBCCoUkIPlayerGroupIE,
|
BBCCoUkIPlayerGroupIE,
|
||||||
BBCCoUkPlaylistIE,
|
BBCCoUkPlaylistIE,
|
||||||
)
|
)
|
||||||
|
from .beacon import BeaconTvIE
|
||||||
from .beatbump import (
|
from .beatbump import (
|
||||||
BeatBumpPlaylistIE,
|
BeatBumpPlaylistIE,
|
||||||
BeatBumpVideoIE,
|
BeatBumpVideoIE,
|
||||||
@ -730,6 +731,7 @@
|
|||||||
GeniusIE,
|
GeniusIE,
|
||||||
GeniusLyricsIE,
|
GeniusLyricsIE,
|
||||||
)
|
)
|
||||||
|
from .germanupa import GermanupaIE
|
||||||
from .getcourseru import (
|
from .getcourseru import (
|
||||||
GetCourseRuIE,
|
GetCourseRuIE,
|
||||||
GetCourseRuPlayerIE,
|
GetCourseRuPlayerIE,
|
||||||
@ -823,7 +825,10 @@
|
|||||||
HungamaIE,
|
HungamaIE,
|
||||||
HungamaSongIE,
|
HungamaSongIE,
|
||||||
)
|
)
|
||||||
from .huya import HuyaLiveIE
|
from .huya import (
|
||||||
|
HuyaLiveIE,
|
||||||
|
HuyaVideoIE,
|
||||||
|
)
|
||||||
from .hypem import HypemIE
|
from .hypem import HypemIE
|
||||||
from .hypergryph import MonsterSirenHypergryphMusicIE
|
from .hypergryph import MonsterSirenHypergryphMusicIE
|
||||||
from .hytale import HytaleIE
|
from .hytale import HytaleIE
|
||||||
@ -946,6 +951,7 @@
|
|||||||
)
|
)
|
||||||
from .kicker import KickerIE
|
from .kicker import KickerIE
|
||||||
from .kickstarter import KickStarterIE
|
from .kickstarter import KickStarterIE
|
||||||
|
from .kika import KikaIE
|
||||||
from .kinja import KinjaEmbedIE
|
from .kinja import KinjaEmbedIE
|
||||||
from .kinopoisk import KinoPoiskIE
|
from .kinopoisk import KinoPoiskIE
|
||||||
from .kommunetv import KommunetvIE
|
from .kommunetv import KommunetvIE
|
||||||
@ -1037,10 +1043,7 @@
|
|||||||
LivestreamShortenerIE,
|
LivestreamShortenerIE,
|
||||||
)
|
)
|
||||||
from .livestreamfails import LivestreamfailsIE
|
from .livestreamfails import LivestreamfailsIE
|
||||||
from .lnkgo import (
|
from .lnk import LnkIE
|
||||||
LnkGoIE,
|
|
||||||
LnkIE,
|
|
||||||
)
|
|
||||||
from .loom import (
|
from .loom import (
|
||||||
LoomFolderIE,
|
LoomFolderIE,
|
||||||
LoomIE,
|
LoomIE,
|
||||||
@ -1165,6 +1168,7 @@
|
|||||||
)
|
)
|
||||||
from .mlssoccer import MLSSoccerIE
|
from .mlssoccer import MLSSoccerIE
|
||||||
from .mocha import MochaVideoIE
|
from .mocha import MochaVideoIE
|
||||||
|
from .mojevideo import MojevideoIE
|
||||||
from .mojvideo import MojvideoIE
|
from .mojvideo import MojvideoIE
|
||||||
from .monstercat import MonstercatIE
|
from .monstercat import MonstercatIE
|
||||||
from .motherless import (
|
from .motherless import (
|
||||||
@ -1811,6 +1815,7 @@
|
|||||||
from .screencast import ScreencastIE
|
from .screencast import ScreencastIE
|
||||||
from .screencastify import ScreencastifyIE
|
from .screencastify import ScreencastifyIE
|
||||||
from .screencastomatic import ScreencastOMaticIE
|
from .screencastomatic import ScreencastOMaticIE
|
||||||
|
from .screenrec import ScreenRecIE
|
||||||
from .scrippsnetworks import (
|
from .scrippsnetworks import (
|
||||||
ScrippsNetworksIE,
|
ScrippsNetworksIE,
|
||||||
ScrippsNetworksWatchIE,
|
ScrippsNetworksWatchIE,
|
||||||
@ -1821,6 +1826,7 @@
|
|||||||
SCTECourseIE,
|
SCTECourseIE,
|
||||||
)
|
)
|
||||||
from .sejmpl import SejmIE
|
from .sejmpl import SejmIE
|
||||||
|
from .sen import SenIE
|
||||||
from .senalcolombia import SenalColombiaLiveIE
|
from .senalcolombia import SenalColombiaLiveIE
|
||||||
from .senategov import (
|
from .senategov import (
|
||||||
SenateGovIE,
|
SenateGovIE,
|
||||||
@ -1876,6 +1882,7 @@
|
|||||||
from .slideslive import SlidesLiveIE
|
from .slideslive import SlidesLiveIE
|
||||||
from .slutload import SlutloadIE
|
from .slutload import SlutloadIE
|
||||||
from .smotrim import SmotrimIE
|
from .smotrim import SmotrimIE
|
||||||
|
from .snapchat import SnapchatSpotlightIE
|
||||||
from .snotr import SnotrIE
|
from .snotr import SnotrIE
|
||||||
from .sohu import (
|
from .sohu import (
|
||||||
SohuIE,
|
SohuIE,
|
||||||
@ -2312,6 +2319,7 @@
|
|||||||
VideomoreVideoIE,
|
VideomoreVideoIE,
|
||||||
)
|
)
|
||||||
from .videopress import VideoPressIE
|
from .videopress import VideoPressIE
|
||||||
|
from .vidflex import VidflexIE
|
||||||
from .vidio import (
|
from .vidio import (
|
||||||
VidioIE,
|
VidioIE,
|
||||||
VidioLiveIE,
|
VidioLiveIE,
|
||||||
|
@ -387,17 +387,27 @@ class ABCIViewShowSeriesIE(InfoExtractor):
|
|||||||
'thumbnail': r're:^https?://cdn\.iview\.abc\.net\.au/thumbs/.*\.jpg$',
|
'thumbnail': r're:^https?://cdn\.iview\.abc\.net\.au/thumbs/.*\.jpg$',
|
||||||
},
|
},
|
||||||
'playlist_count': 15,
|
'playlist_count': 15,
|
||||||
|
'skip': 'This program is not currently available in ABC iview',
|
||||||
|
}, {
|
||||||
|
'url': 'https://iview.abc.net.au/show/inbestigators',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '175343-1',
|
||||||
|
'title': 'Series 1',
|
||||||
|
'description': 'md5:b9976935a6450e5b78ce2a940a755685',
|
||||||
|
'series': 'The Inbestigators',
|
||||||
|
'season': 'Series 1',
|
||||||
|
'thumbnail': r're:^https?://cdn\.iview\.abc\.net\.au/thumbs/.+\.jpg',
|
||||||
|
},
|
||||||
|
'playlist_count': 17,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
show_id = self._match_id(url)
|
show_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, show_id)
|
webpage = self._download_webpage(url, show_id)
|
||||||
webpage_data = self._search_regex(
|
video_data = self._search_json(
|
||||||
r'window\.__INITIAL_STATE__\s*=\s*[\'"](.+?)[\'"]\s*;',
|
r'window\.__INITIAL_STATE__\s*=\s*[\'"]', webpage, 'initial state', show_id,
|
||||||
webpage, 'initial state')
|
transform_source=lambda x: x.encode().decode('unicode_escape'),
|
||||||
video_data = self._parse_json(
|
end_pattern=r'[\'"]\s*;')['route']['pageData']['_embedded']
|
||||||
unescapeHTML(webpage_data).encode().decode('unicode_escape'), show_id)
|
|
||||||
video_data = video_data['route']['pageData']['_embedded']
|
|
||||||
|
|
||||||
highlight = try_get(video_data, lambda x: x['highlightVideo']['shareUrl'])
|
highlight = try_get(video_data, lambda x: x['highlightVideo']['shareUrl'])
|
||||||
if not self._yes_playlist(show_id, bool(highlight), video_label='highlight video'):
|
if not self._yes_playlist(show_id, bool(highlight), video_label='highlight video'):
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
|
|
||||||
class AcademicEarthCourseIE(InfoExtractor):
|
class AcademicEarthCourseIE(InfoExtractor):
|
||||||
_VALID_URL = r'^https?://(?:www\.)?academicearth\.org/playlists/(?P<id>[^?#/]+)'
|
_VALID_URL = r'https?://(?:www\.)?academicearth\.org/playlists/(?P<id>[^?#/]+)'
|
||||||
IE_NAME = 'AcademicEarth:Course'
|
IE_NAME = 'AcademicEarth:Course'
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://academicearth.org/playlists/laws-of-nature/',
|
'url': 'http://academicearth.org/playlists/laws-of-nature/',
|
||||||
|
@ -1355,6 +1355,7 @@
|
|||||||
class AdobePassIE(InfoExtractor): # XXX: Conventionally, base classes should end with BaseIE/InfoExtractor
|
class AdobePassIE(InfoExtractor): # XXX: Conventionally, base classes should end with BaseIE/InfoExtractor
|
||||||
_SERVICE_PROVIDER_TEMPLATE = 'https://sp.auth.adobe.com/adobe-services/%s'
|
_SERVICE_PROVIDER_TEMPLATE = 'https://sp.auth.adobe.com/adobe-services/%s'
|
||||||
_USER_AGENT = 'Mozilla/5.0 (X11; Linux i686; rv:47.0) Gecko/20100101 Firefox/47.0'
|
_USER_AGENT = 'Mozilla/5.0 (X11; Linux i686; rv:47.0) Gecko/20100101 Firefox/47.0'
|
||||||
|
_MODERN_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; rv:131.0) Gecko/20100101 Firefox/131.0'
|
||||||
_MVPD_CACHE = 'ap-mvpd'
|
_MVPD_CACHE = 'ap-mvpd'
|
||||||
|
|
||||||
_DOWNLOADING_LOGIN_PAGE = 'Downloading Provider Login Page'
|
_DOWNLOADING_LOGIN_PAGE = 'Downloading Provider Login Page'
|
||||||
@ -1454,7 +1455,11 @@ def extract_redirect_url(html, url=None, fatal=False):
|
|||||||
'no_iframe': 'false',
|
'no_iframe': 'false',
|
||||||
'domain_name': 'adobe.com',
|
'domain_name': 'adobe.com',
|
||||||
'redirect_url': url,
|
'redirect_url': url,
|
||||||
})
|
}, headers={
|
||||||
|
# yt-dlp's default user-agent is usually too old for Comcast_SSO
|
||||||
|
# See: https://github.com/yt-dlp/yt-dlp/issues/10848
|
||||||
|
'User-Agent': self._MODERN_USER_AGENT,
|
||||||
|
} if mso_id == 'Comcast_SSO' else None)
|
||||||
elif not self._cookies_passed:
|
elif not self._cookies_passed:
|
||||||
raise_mvpd_required()
|
raise_mvpd_required()
|
||||||
|
|
||||||
|
@ -1,27 +1,42 @@
|
|||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
clean_html,
|
|
||||||
clean_podcast_url,
|
clean_podcast_url,
|
||||||
get_element_by_class,
|
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
try_get,
|
|
||||||
)
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class ApplePodcastsIE(InfoExtractor):
|
class ApplePodcastsIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://podcasts\.apple\.com/(?:[^/]+/)?podcast(?:/[^/]+){1,2}.*?\bi=(?P<id>\d+)'
|
_VALID_URL = r'https?://podcasts\.apple\.com/(?:[^/]+/)?podcast(?:/[^/]+){1,2}.*?\bi=(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://podcasts.apple.com/us/podcast/ferreck-dawn-to-the-break-of-dawn-117/id1625658232?i=1000665010654',
|
||||||
|
'md5': '82cc219b8cc1dcf8bfc5a5e99b23b172',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1000665010654',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': 'Ferreck Dawn - To The Break of Dawn 117',
|
||||||
|
'episode': 'Ferreck Dawn - To The Break of Dawn 117',
|
||||||
|
'description': 'md5:1fc571102f79dbd0a77bfd71ffda23bc',
|
||||||
|
'upload_date': '20240812',
|
||||||
|
'timestamp': 1723449600,
|
||||||
|
'duration': 3596,
|
||||||
|
'series': 'Ferreck Dawn - To The Break of Dawn',
|
||||||
|
'thumbnail': 're:.+[.](png|jpe?g|webp)',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'https://podcasts.apple.com/us/podcast/207-whitney-webb-returns/id1135137367?i=1000482637777',
|
'url': 'https://podcasts.apple.com/us/podcast/207-whitney-webb-returns/id1135137367?i=1000482637777',
|
||||||
'md5': '41dc31cd650143e530d9423b6b5a344f',
|
'md5': 'baf8a6b8b8aa6062dbb4639ed73d0052',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1000482637777',
|
'id': '1000482637777',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': '207 - Whitney Webb Returns',
|
'title': '207 - Whitney Webb Returns',
|
||||||
|
'episode': '207 - Whitney Webb Returns',
|
||||||
|
'episode_number': 207,
|
||||||
'description': 'md5:75ef4316031df7b41ced4e7b987f79c6',
|
'description': 'md5:75ef4316031df7b41ced4e7b987f79c6',
|
||||||
'upload_date': '20200705',
|
'upload_date': '20200705',
|
||||||
'timestamp': 1593932400,
|
'timestamp': 1593932400,
|
||||||
'duration': 6454,
|
'duration': 5369,
|
||||||
'series': 'The Tim Dillon Show',
|
'series': 'The Tim Dillon Show',
|
||||||
'thumbnail': 're:.+[.](png|jpe?g|webp)',
|
'thumbnail': 're:.+[.](png|jpe?g|webp)',
|
||||||
},
|
},
|
||||||
@ -39,47 +54,24 @@ class ApplePodcastsIE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
episode_id = self._match_id(url)
|
episode_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, episode_id)
|
webpage = self._download_webpage(url, episode_id)
|
||||||
episode_data = {}
|
server_data = self._search_json(
|
||||||
ember_data = {}
|
r'<script [^>]*\bid=["\']serialized-server-data["\'][^>]*>', webpage,
|
||||||
# new page type 2021-11
|
'server data', episode_id, contains_pattern=r'\[{(?s:.+)}\]')[0]['data']
|
||||||
amp_data = self._parse_json(self._search_regex(
|
model_data = traverse_obj(server_data, (
|
||||||
r'(?s)id="shoebox-media-api-cache-amp-podcasts"[^>]*>\s*({.+?})\s*<',
|
'headerButtonItems', lambda _, v: v['$kind'] == 'bookmark' and v['modelType'] == 'EpisodeOffer',
|
||||||
webpage, 'AMP data', default='{}'), episode_id, fatal=False) or {}
|
'model', {dict}, any))
|
||||||
amp_data = try_get(amp_data,
|
|
||||||
lambda a: self._parse_json(
|
|
||||||
next(a[x] for x in iter(a) if episode_id in x),
|
|
||||||
episode_id),
|
|
||||||
dict) or {}
|
|
||||||
amp_data = amp_data.get('d') or []
|
|
||||||
episode_data = try_get(
|
|
||||||
amp_data,
|
|
||||||
lambda a: next(x for x in a
|
|
||||||
if x['type'] == 'podcast-episodes' and x['id'] == episode_id),
|
|
||||||
dict)
|
|
||||||
if not episode_data:
|
|
||||||
# try pre 2021-11 page type: TODO: consider deleting if no longer used
|
|
||||||
ember_data = self._parse_json(self._search_regex(
|
|
||||||
r'(?s)id="shoebox-ember-data-store"[^>]*>\s*({.+?})\s*<',
|
|
||||||
webpage, 'ember data'), episode_id) or {}
|
|
||||||
ember_data = ember_data.get(episode_id) or ember_data
|
|
||||||
episode_data = try_get(ember_data, lambda x: x['data'], dict)
|
|
||||||
episode = episode_data['attributes']
|
|
||||||
description = episode.get('description') or {}
|
|
||||||
|
|
||||||
series = None
|
|
||||||
for inc in (amp_data or ember_data.get('included') or []):
|
|
||||||
if inc.get('type') == 'media/podcast':
|
|
||||||
series = try_get(inc, lambda x: x['attributes']['name'])
|
|
||||||
series = series or clean_html(get_element_by_class('podcast-header__identity', webpage))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': episode_id,
|
'id': episode_id,
|
||||||
'title': episode.get('name'),
|
**self._json_ld(
|
||||||
'url': clean_podcast_url(episode['assetUrl']),
|
traverse_obj(server_data, ('seoData', 'schemaContent', {dict}))
|
||||||
'description': description.get('standard') or description.get('short'),
|
or self._yield_json_ld(webpage, episode_id, fatal=False), episode_id, fatal=False),
|
||||||
'timestamp': parse_iso8601(episode.get('releaseDateTime')),
|
**traverse_obj(model_data, {
|
||||||
'duration': int_or_none(episode.get('durationInMilliseconds'), 1000),
|
'title': ('title', {str}),
|
||||||
'series': series,
|
'url': ('streamUrl', {clean_podcast_url}),
|
||||||
|
'timestamp': ('releaseDate', {parse_iso8601}),
|
||||||
|
'duration': ('duration', {int_or_none}),
|
||||||
|
}),
|
||||||
'thumbnail': self._og_search_thumbnail(webpage),
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
'vcodec': 'none',
|
'vcodec': 'none',
|
||||||
}
|
}
|
||||||
|
@ -231,7 +231,7 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
class ARDBetaMediathekIE(InfoExtractor):
|
class ARDBetaMediathekIE(InfoExtractor):
|
||||||
IE_NAME = 'ARDMediathek'
|
IE_NAME = 'ARDMediathek'
|
||||||
_VALID_URL = r'''(?x)https://
|
_VALID_URL = r'''(?x)https?://
|
||||||
(?:(?:beta|www)\.)?ardmediathek\.de/
|
(?:(?:beta|www)\.)?ardmediathek\.de/
|
||||||
(?:[^/]+/)?
|
(?:[^/]+/)?
|
||||||
(?:player|live|video)/
|
(?:player|live|video)/
|
||||||
@ -470,7 +470,7 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
|
|
||||||
class ARDMediathekCollectionIE(InfoExtractor):
|
class ARDMediathekCollectionIE(InfoExtractor):
|
||||||
_VALID_URL = r'''(?x)https://
|
_VALID_URL = r'''(?x)https?://
|
||||||
(?:(?:beta|www)\.)?ardmediathek\.de/
|
(?:(?:beta|www)\.)?ardmediathek\.de/
|
||||||
(?:[^/?#]+/)?
|
(?:[^/?#]+/)?
|
||||||
(?P<playlist>sendung|serie|sammlung)/
|
(?P<playlist>sendung|serie|sammlung)/
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import functools
|
||||||
|
import json
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@ -6,7 +8,9 @@
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
KNOWN_EXTENSIONS,
|
KNOWN_EXTENSIONS,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
extract_attributes,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
|
get_element_html_by_id,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_filesize,
|
parse_filesize,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
@ -17,6 +21,7 @@
|
|||||||
url_or_none,
|
url_or_none,
|
||||||
urljoin,
|
urljoin,
|
||||||
)
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class BandcampIE(InfoExtractor):
|
class BandcampIE(InfoExtractor):
|
||||||
@ -459,7 +464,7 @@ class BandcampUserIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://coldworldofficial.bandcamp.com/music',
|
'url': 'https://coldworldofficial.bandcamp.com/music',
|
||||||
'playlist_mincount': 10,
|
'playlist_mincount': 7,
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'coldworldofficial',
|
'id': 'coldworldofficial',
|
||||||
'title': 'Discography of coldworldofficial',
|
'title': 'Discography of coldworldofficial',
|
||||||
@ -473,12 +478,19 @@ class BandcampUserIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
def _yield_items(self, webpage):
|
||||||
|
yield from (
|
||||||
|
re.findall(r'<li data-item-id=["\'][^>]+>\s*<a href=["\'](?![^"\'/]*?/merch)([^"\']+)', webpage)
|
||||||
|
or re.findall(r'<div[^>]+trackTitle["\'][^"\']+["\']([^"\']+)', webpage))
|
||||||
|
|
||||||
|
yield from traverse_obj(webpage, (
|
||||||
|
{functools.partial(get_element_html_by_id, 'music-grid')}, {extract_attributes},
|
||||||
|
'data-client-items', {json.loads}, ..., 'page_url', {str}))
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
uploader = self._match_id(url)
|
uploader = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, uploader)
|
webpage = self._download_webpage(url, uploader)
|
||||||
|
|
||||||
discography_data = (re.findall(r'<li data-item-id=["\'][^>]+>\s*<a href=["\'](?![^"\'/]*?/merch)([^"\']+)', webpage)
|
|
||||||
or re.findall(r'<div[^>]+trackTitle["\'][^"\']+["\']([^"\']+)', webpage))
|
|
||||||
|
|
||||||
return self.playlist_from_matches(
|
return self.playlist_from_matches(
|
||||||
discography_data, uploader, f'Discography of {uploader}', getter=lambda x: urljoin(url, x))
|
self._yield_items(webpage), uploader, f'Discography of {uploader}',
|
||||||
|
getter=functools.partial(urljoin, url))
|
||||||
|
68
yt_dlp/extractor/beacon.py
Normal file
68
yt_dlp/extractor/beacon.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
parse_iso8601,
|
||||||
|
traverse_obj,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BeaconTvIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?beacon\.tv/content/(?P<id>[\w-]+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://beacon.tv/content/welcome-to-beacon',
|
||||||
|
'md5': 'b3f5932d437f288e662f10f3bfc5bd04',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'welcome-to-beacon',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'upload_date': '20240509',
|
||||||
|
'description': 'md5:ea2bd32e71acf3f9fca6937412cc3563',
|
||||||
|
'thumbnail': 'https://cdn.jwplayer.com/v2/media/I4CkkEvN/poster.jpg?width=720',
|
||||||
|
'title': 'Your home for Critical Role!',
|
||||||
|
'timestamp': 1715227200,
|
||||||
|
'duration': 105.494,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://beacon.tv/content/re-slayers-take-trailer',
|
||||||
|
'md5': 'd879b091485dbed2245094c8152afd89',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 're-slayers-take-trailer',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'The Re-Slayer’s Take | Official Trailer',
|
||||||
|
'timestamp': 1715189040,
|
||||||
|
'upload_date': '20240508',
|
||||||
|
'duration': 53.249,
|
||||||
|
'thumbnail': 'https://cdn.jwplayer.com/v2/media/PW5ApIw3/poster.jpg?width=720',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
content_data = traverse_obj(self._search_nextjs_data(webpage, video_id), (
|
||||||
|
'props', 'pageProps', '__APOLLO_STATE__',
|
||||||
|
lambda k, v: k.startswith('Content:') and v['slug'] == video_id, any))
|
||||||
|
if not content_data:
|
||||||
|
raise ExtractorError('Failed to extract content data')
|
||||||
|
|
||||||
|
jwplayer_data = traverse_obj(content_data, (
|
||||||
|
(('contentVideo', 'video', 'videoData'),
|
||||||
|
('contentPodcast', 'podcast', 'audioData')), {json.loads}, {dict}, any))
|
||||||
|
if not jwplayer_data:
|
||||||
|
if content_data.get('contentType') not in ('videoPodcast', 'video', 'podcast'):
|
||||||
|
raise ExtractorError('Content is not a video/podcast', expected=True)
|
||||||
|
if traverse_obj(content_data, ('contentTier', '__ref')) != 'MemberTier:65b258d178f89be87b4dc0a4':
|
||||||
|
self.raise_login_required('This video/podcast is for members only')
|
||||||
|
raise ExtractorError('Failed to extract content')
|
||||||
|
|
||||||
|
return {
|
||||||
|
**self._parse_jwplayer_data(jwplayer_data, video_id),
|
||||||
|
**traverse_obj(content_data, {
|
||||||
|
'title': ('title', {str}),
|
||||||
|
'description': ('description', {str}),
|
||||||
|
'timestamp': ('publishedAt', {parse_iso8601}),
|
||||||
|
}),
|
||||||
|
}
|
@ -46,6 +46,7 @@
|
|||||||
|
|
||||||
|
|
||||||
class BilibiliBaseIE(InfoExtractor):
|
class BilibiliBaseIE(InfoExtractor):
|
||||||
|
_HEADERS = {'Referer': 'https://www.bilibili.com/'}
|
||||||
_FORMAT_ID_RE = re.compile(r'-(\d+)\.m4s\?')
|
_FORMAT_ID_RE = re.compile(r'-(\d+)\.m4s\?')
|
||||||
_WBI_KEY_CACHE_TIMEOUT = 30 # exact expire timeout is unclear, use 30s for one session
|
_WBI_KEY_CACHE_TIMEOUT = 30 # exact expire timeout is unclear, use 30s for one session
|
||||||
_wbi_key_cache = {}
|
_wbi_key_cache = {}
|
||||||
@ -192,7 +193,7 @@ def _get_subtitles(self, video_id, cid, aid=None):
|
|||||||
video_info = self._download_json(
|
video_info = self._download_json(
|
||||||
'https://api.bilibili.com/x/player/v2', video_id,
|
'https://api.bilibili.com/x/player/v2', video_id,
|
||||||
query={'aid': aid, 'cid': cid} if aid else {'bvid': video_id, 'cid': cid},
|
query={'aid': aid, 'cid': cid} if aid else {'bvid': video_id, 'cid': cid},
|
||||||
note=f'Extracting subtitle info {cid}')
|
note=f'Extracting subtitle info {cid}', headers=self._HEADERS)
|
||||||
if traverse_obj(video_info, ('data', 'need_login_subtitle')):
|
if traverse_obj(video_info, ('data', 'need_login_subtitle')):
|
||||||
self.report_warning(
|
self.report_warning(
|
||||||
f'Subtitles are only available when logged in. {self._login_hint()}', only_once=True)
|
f'Subtitles are only available when logged in. {self._login_hint()}', only_once=True)
|
||||||
@ -207,7 +208,7 @@ def _get_subtitles(self, video_id, cid, aid=None):
|
|||||||
def _get_chapters(self, aid, cid):
|
def _get_chapters(self, aid, cid):
|
||||||
chapters = aid and cid and self._download_json(
|
chapters = aid and cid and self._download_json(
|
||||||
'https://api.bilibili.com/x/player/v2', aid, query={'aid': aid, 'cid': cid},
|
'https://api.bilibili.com/x/player/v2', aid, query={'aid': aid, 'cid': cid},
|
||||||
note='Extracting chapters', fatal=False)
|
note='Extracting chapters', fatal=False, headers=self._HEADERS)
|
||||||
return traverse_obj(chapters, ('data', 'view_points', ..., {
|
return traverse_obj(chapters, ('data', 'view_points', ..., {
|
||||||
'title': 'content',
|
'title': 'content',
|
||||||
'start_time': 'from',
|
'start_time': 'from',
|
||||||
@ -1021,8 +1022,6 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
|
|
||||||
class BilibiliCheeseBaseIE(BilibiliBaseIE):
|
class BilibiliCheeseBaseIE(BilibiliBaseIE):
|
||||||
_HEADERS = {'Referer': 'https://www.bilibili.com/'}
|
|
||||||
|
|
||||||
def _extract_episode(self, season_info, ep_id):
|
def _extract_episode(self, season_info, ep_id):
|
||||||
episode_info = traverse_obj(season_info, (
|
episode_info = traverse_obj(season_info, (
|
||||||
'episodes', lambda _, v: v['id'] == int(ep_id)), get_all=False)
|
'episodes', lambda _, v: v['id'] == int(ep_id)), get_all=False)
|
||||||
@ -1852,7 +1851,7 @@ def _real_extract(self, url):
|
|||||||
class BiliIntlBaseIE(InfoExtractor):
|
class BiliIntlBaseIE(InfoExtractor):
|
||||||
_API_URL = 'https://api.bilibili.tv/intl/gateway'
|
_API_URL = 'https://api.bilibili.tv/intl/gateway'
|
||||||
_NETRC_MACHINE = 'biliintl'
|
_NETRC_MACHINE = 'biliintl'
|
||||||
_HEADERS = {'Referer': 'https://www.bilibili.com/'}
|
_HEADERS = {'Referer': 'https://www.bilibili.tv/'}
|
||||||
|
|
||||||
def _call_api(self, endpoint, *args, **kwargs):
|
def _call_api(self, endpoint, *args, **kwargs):
|
||||||
json = self._download_json(self._API_URL + endpoint, *args, **kwargs)
|
json = self._download_json(self._API_URL + endpoint, *args, **kwargs)
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
class CallinIE(InfoExtractor):
|
class CallinIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?callin\.com/(episode)/(?P<id>[-a-zA-Z]+)'
|
_VALID_URL = r'https?://(?:www\.)?callin\.com/episode/(?P<id>[-a-zA-Z]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.callin.com/episode/the-title-ix-regime-and-the-long-march-through-EBfXYSrsjc',
|
'url': 'https://www.callin.com/episode/the-title-ix-regime-and-the-long-march-through-EBfXYSrsjc',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
from ..networking.exceptions import (
|
from ..networking.exceptions import (
|
||||||
HTTPError,
|
HTTPError,
|
||||||
IncompleteRead,
|
IncompleteRead,
|
||||||
|
TransportError,
|
||||||
network_exceptions,
|
network_exceptions,
|
||||||
)
|
)
|
||||||
from ..networking.impersonate import ImpersonateTarget
|
from ..networking.impersonate import ImpersonateTarget
|
||||||
@ -572,13 +573,13 @@ class InfoExtractor:
|
|||||||
|
|
||||||
def _login_hint(self, method=NO_DEFAULT, netrc=None):
|
def _login_hint(self, method=NO_DEFAULT, netrc=None):
|
||||||
password_hint = f'--username and --password, --netrc-cmd, or --netrc ({netrc or self._NETRC_MACHINE}) to provide account credentials'
|
password_hint = f'--username and --password, --netrc-cmd, or --netrc ({netrc or self._NETRC_MACHINE}) to provide account credentials'
|
||||||
|
cookies_hint = 'See https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp for how to manually pass cookies'
|
||||||
return {
|
return {
|
||||||
None: '',
|
None: '',
|
||||||
'any': f'Use --cookies, --cookies-from-browser, {password_hint}',
|
'any': f'Use --cookies, --cookies-from-browser, {password_hint}. {cookies_hint}',
|
||||||
'password': f'Use {password_hint}',
|
'password': f'Use {password_hint}',
|
||||||
'cookies': (
|
'cookies': f'Use --cookies-from-browser or --cookies for the authentication. {cookies_hint}',
|
||||||
'Use --cookies-from-browser or --cookies for the authentication. '
|
'session_cookies': f'Use --cookies for the authentication (--cookies-from-browser might not work). {cookies_hint}',
|
||||||
'See https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp for how to manually pass cookies'),
|
|
||||||
}[method if method is not NO_DEFAULT else 'any' if self.supports_login() else 'cookies']
|
}[method if method is not NO_DEFAULT else 'any' if self.supports_login() else 'cookies']
|
||||||
|
|
||||||
def __init__(self, downloader=None):
|
def __init__(self, downloader=None):
|
||||||
@ -965,6 +966,9 @@ def _download_webpage_handle(self, url_or_request, video_id, note=None, errnote=
|
|||||||
return False
|
return False
|
||||||
content = self._webpage_read_content(urlh, url_or_request, video_id, note, errnote, fatal,
|
content = self._webpage_read_content(urlh, url_or_request, video_id, note, errnote, fatal,
|
||||||
encoding=encoding, data=data)
|
encoding=encoding, data=data)
|
||||||
|
if content is False:
|
||||||
|
assert not fatal
|
||||||
|
return False
|
||||||
return (content, urlh)
|
return (content, urlh)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -1039,7 +1043,15 @@ def __decode_webpage(self, webpage_bytes, encoding, headers):
|
|||||||
|
|
||||||
def _webpage_read_content(self, urlh, url_or_request, video_id, note=None, errnote=None, fatal=True,
|
def _webpage_read_content(self, urlh, url_or_request, video_id, note=None, errnote=None, fatal=True,
|
||||||
prefix=None, encoding=None, data=None):
|
prefix=None, encoding=None, data=None):
|
||||||
webpage_bytes = urlh.read()
|
try:
|
||||||
|
webpage_bytes = urlh.read()
|
||||||
|
except TransportError as err:
|
||||||
|
errmsg = f'{video_id}: Error reading response: {err.msg}'
|
||||||
|
if fatal:
|
||||||
|
raise ExtractorError(errmsg, cause=err)
|
||||||
|
self.report_warning(errmsg)
|
||||||
|
return False
|
||||||
|
|
||||||
if prefix is not None:
|
if prefix is not None:
|
||||||
webpage_bytes = prefix + webpage_bytes
|
webpage_bytes = prefix + webpage_bytes
|
||||||
if self.get_param('dump_intermediate_pages', False):
|
if self.get_param('dump_intermediate_pages', False):
|
||||||
@ -1698,7 +1710,7 @@ def traverse_json_ld(json_ld, at_top_level=True):
|
|||||||
rating = traverse_obj(e, ('aggregateRating', 'ratingValue'), expected_type=float_or_none)
|
rating = traverse_obj(e, ('aggregateRating', 'ratingValue'), expected_type=float_or_none)
|
||||||
if rating is not None:
|
if rating is not None:
|
||||||
info['average_rating'] = rating
|
info['average_rating'] = rating
|
||||||
if is_type(e, 'TVEpisode', 'Episode'):
|
if is_type(e, 'TVEpisode', 'Episode', 'PodcastEpisode'):
|
||||||
episode_name = unescapeHTML(e.get('name'))
|
episode_name = unescapeHTML(e.get('name'))
|
||||||
info.update({
|
info.update({
|
||||||
'episode': episode_name,
|
'episode': episode_name,
|
||||||
@ -2065,7 +2077,7 @@ def _parse_m3u8_formats_and_subtitles(
|
|||||||
has_drm = HlsFD._has_drm(m3u8_doc)
|
has_drm = HlsFD._has_drm(m3u8_doc)
|
||||||
|
|
||||||
def format_url(url):
|
def format_url(url):
|
||||||
return url if re.match(r'^https?://', url) else urllib.parse.urljoin(m3u8_url, url)
|
return url if re.match(r'https?://', url) else urllib.parse.urljoin(m3u8_url, url)
|
||||||
|
|
||||||
if self.get_param('hls_split_discontinuity', False):
|
if self.get_param('hls_split_discontinuity', False):
|
||||||
def _extract_m3u8_playlist_indices(manifest_url=None, m3u8_doc=None):
|
def _extract_m3u8_playlist_indices(manifest_url=None, m3u8_doc=None):
|
||||||
@ -2800,11 +2812,11 @@ def extract_Initialization(source):
|
|||||||
base_url_e = element.find(_add_ns('BaseURL'))
|
base_url_e = element.find(_add_ns('BaseURL'))
|
||||||
if try_call(lambda: base_url_e.text) is not None:
|
if try_call(lambda: base_url_e.text) is not None:
|
||||||
base_url = base_url_e.text + base_url
|
base_url = base_url_e.text + base_url
|
||||||
if re.match(r'^https?://', base_url):
|
if re.match(r'https?://', base_url):
|
||||||
break
|
break
|
||||||
if mpd_base_url and base_url.startswith('/'):
|
if mpd_base_url and base_url.startswith('/'):
|
||||||
base_url = urllib.parse.urljoin(mpd_base_url, base_url)
|
base_url = urllib.parse.urljoin(mpd_base_url, base_url)
|
||||||
elif mpd_base_url and not re.match(r'^https?://', base_url):
|
elif mpd_base_url and not re.match(r'https?://', base_url):
|
||||||
if not mpd_base_url.endswith('/'):
|
if not mpd_base_url.endswith('/'):
|
||||||
mpd_base_url += '/'
|
mpd_base_url += '/'
|
||||||
base_url = mpd_base_url + base_url
|
base_url = mpd_base_url + base_url
|
||||||
@ -2894,7 +2906,7 @@ def prepare_template(template_name, identifiers):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def location_key(location):
|
def location_key(location):
|
||||||
return 'url' if re.match(r'^https?://', location) else 'path'
|
return 'url' if re.match(r'https?://', location) else 'path'
|
||||||
|
|
||||||
if 'segment_urls' not in representation_ms_info and 'media' in representation_ms_info:
|
if 'segment_urls' not in representation_ms_info and 'media' in representation_ms_info:
|
||||||
|
|
||||||
@ -3489,7 +3501,7 @@ def _parse_jwplayer_formats(self, jwplayer_sources_data, video_id=None,
|
|||||||
continue
|
continue
|
||||||
urls.add(source_url)
|
urls.add(source_url)
|
||||||
source_type = source.get('type') or ''
|
source_type = source.get('type') or ''
|
||||||
ext = mimetype2ext(source_type) or determine_ext(source_url)
|
ext = determine_ext(source_url, default_ext=mimetype2ext(source_type))
|
||||||
if source_type == 'hls' or ext == 'm3u8' or 'format=m3u8-aapl' in source_url:
|
if source_type == 'hls' or ext == 'm3u8' or 'format=m3u8-aapl' in source_url:
|
||||||
formats.extend(self._extract_m3u8_formats(
|
formats.extend(self._extract_m3u8_formats(
|
||||||
source_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
source_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
|
@ -6,12 +6,37 @@
|
|||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
|
update_url_query,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CWTVIE(InfoExtractor):
|
class CWTVIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?cw(?:tv(?:pr)?|seed)\.com/(?:shows/)?(?:[^/]+/)+[^?]*\?.*\b(?:play|watch)=(?P<id>[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})'
|
_VALID_URL = r'https?://(?:www\.)?cw(?:tv(?:pr)?|seed)\.com/(?:shows/)?(?:[^/]+/)+[^?]*\?.*\b(?:play|watch)=(?P<id>[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://www.cwtv.com/shows/all-american-homecoming/ready-or-not/?play=d848488f-f62a-40fd-af1f-6440b1821aab',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'd848488f-f62a-40fd-af1f-6440b1821aab',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Ready Or Not',
|
||||||
|
'description': 'Simone is concerned about changes taking place at Bringston; JR makes a decision about his future.',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpe?g$',
|
||||||
|
'duration': 2547,
|
||||||
|
'timestamp': 1720519200,
|
||||||
|
'uploader': 'CWTV',
|
||||||
|
'chapters': 'count:6',
|
||||||
|
'series': 'All American: Homecoming',
|
||||||
|
'season_number': 3,
|
||||||
|
'episode_number': 1,
|
||||||
|
'age_limit': 0,
|
||||||
|
'upload_date': '20240709',
|
||||||
|
'season': 'Season 3',
|
||||||
|
'episode': 'Episode 1',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'http://cwtv.com/shows/arrow/legends-of-yesterday/?play=6b15e985-9345-4f60-baf8-56e96be57c63',
|
'url': 'http://cwtv.com/shows/arrow/legends-of-yesterday/?play=6b15e985-9345-4f60-baf8-56e96be57c63',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '6b15e985-9345-4f60-baf8-56e96be57c63',
|
'id': '6b15e985-9345-4f60-baf8-56e96be57c63',
|
||||||
@ -69,13 +94,14 @@ class CWTVIE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
data = self._download_json(
|
data = self._download_json(
|
||||||
'http://images.cwtv.com/feed/mobileapp/video-meta/apiversion_8/guid_' + video_id,
|
f'https://images.cwtv.com/feed/mobileapp/video-meta/apiversion_12/guid_{video_id}', video_id)
|
||||||
video_id)
|
|
||||||
if data.get('result') != 'ok':
|
if data.get('result') != 'ok':
|
||||||
raise ExtractorError(data['msg'], expected=True)
|
raise ExtractorError(data['msg'], expected=True)
|
||||||
video_data = data['video']
|
video_data = data['video']
|
||||||
title = video_data['title']
|
title = video_data['title']
|
||||||
mpx_url = video_data.get('mpx_url') or f'http://link.theplatform.com/s/cwtv/media/guid/2703454149/{video_id}?formats=M3U'
|
mpx_url = update_url_query(
|
||||||
|
video_data.get('mpx_url') or f'https://link.theplatform.com/s/cwtv/media/guid/2703454149/{video_id}',
|
||||||
|
{'formats': 'M3U+none'})
|
||||||
|
|
||||||
season = str_or_none(video_data.get('season'))
|
season = str_or_none(video_data.get('season'))
|
||||||
episode = str_or_none(video_data.get('episode'))
|
episode = str_or_none(video_data.get('episode'))
|
||||||
|
@ -319,32 +319,6 @@ def _real_extract(self, url):
|
|||||||
url, display_id, host, 'dplay' + country, country, domain)
|
url, display_id, host, 'dplay' + country, country, domain)
|
||||||
|
|
||||||
|
|
||||||
class HGTVDeIE(DPlayBaseIE):
|
|
||||||
_VALID_URL = r'https?://de\.hgtv\.com/sendungen' + DPlayBaseIE._PATH_REGEX
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://de.hgtv.com/sendungen/tiny-house-klein-aber-oho/wer-braucht-schon-eine-toilette/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '151205',
|
|
||||||
'display_id': 'tiny-house-klein-aber-oho/wer-braucht-schon-eine-toilette',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Wer braucht schon eine Toilette',
|
|
||||||
'description': 'md5:05b40a27e7aed2c9172de34d459134e2',
|
|
||||||
'duration': 1177.024,
|
|
||||||
'timestamp': 1595705400,
|
|
||||||
'upload_date': '20200725',
|
|
||||||
'creator': 'HGTV',
|
|
||||||
'series': 'Tiny House - klein, aber oho',
|
|
||||||
'season_number': 3,
|
|
||||||
'episode_number': 3,
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
display_id = self._match_id(url)
|
|
||||||
return self._get_disco_api_info(
|
|
||||||
url, display_id, 'eu1-prod.disco-api.com', 'hgtv', 'de')
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryPlusBaseIE(DPlayBaseIE):
|
class DiscoveryPlusBaseIE(DPlayBaseIE):
|
||||||
"""Subclasses must set _PRODUCT, _DISCO_API_PARAMS"""
|
"""Subclasses must set _PRODUCT, _DISCO_API_PARAMS"""
|
||||||
|
|
||||||
@ -373,6 +347,45 @@ def _real_extract(self, url):
|
|||||||
return self._get_disco_api_info(url, self._match_id(url), **self._DISCO_API_PARAMS)
|
return self._get_disco_api_info(url, self._match_id(url), **self._DISCO_API_PARAMS)
|
||||||
|
|
||||||
|
|
||||||
|
class HGTVDeIE(DiscoveryPlusBaseIE):
|
||||||
|
_VALID_URL = r'https?://de\.hgtv\.com/sendungen' + DPlayBaseIE._PATH_REGEX
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://de.hgtv.com/sendungen/mein-kleinstadt-traumhaus/vom-landleben-ins-loft',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '7332936',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'display_id': 'mein-kleinstadt-traumhaus/vom-landleben-ins-loft',
|
||||||
|
'title': 'Vom Landleben ins Loft',
|
||||||
|
'description': 'md5:e5f72c02c853970796dd3818f2e25745',
|
||||||
|
'episode': 'Episode 7',
|
||||||
|
'episode_number': 7,
|
||||||
|
'season': 'Season 7',
|
||||||
|
'season_number': 7,
|
||||||
|
'series': 'Mein Kleinstadt-Traumhaus',
|
||||||
|
'duration': 2645.0,
|
||||||
|
'timestamp': 1725998100,
|
||||||
|
'upload_date': '20240910',
|
||||||
|
'creators': ['HGTV'],
|
||||||
|
'tags': [],
|
||||||
|
'thumbnail': 'https://eu1-prod-images.disco-api.com/2024/08/09/82a386b9-c688-32c7-b9ff-0b13865f0bae.jpeg',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
_PRODUCT = 'hgtv'
|
||||||
|
_DISCO_API_PARAMS = {
|
||||||
|
'disco_host': 'eu1-prod.disco-api.com',
|
||||||
|
'realm': 'hgtv',
|
||||||
|
'country': 'de',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
||||||
|
headers.update({
|
||||||
|
'x-disco-params': f'realm={realm}',
|
||||||
|
'x-disco-client': 'Alps:HyogaPlayer:0.0.0',
|
||||||
|
'Authorization': self._get_auth(disco_base, display_id, realm),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class GoDiscoveryIE(DiscoveryPlusBaseIE):
|
class GoDiscoveryIE(DiscoveryPlusBaseIE):
|
||||||
_VALID_URL = r'https?://(?:go\.)?discovery\.com/video' + DPlayBaseIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:go\.)?discovery\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
@ -6,8 +6,10 @@
|
|||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
update_url,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
url_basename,
|
url_basename,
|
||||||
|
urlencode_postdata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -36,43 +38,58 @@ class DropboxIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def _yield_decoded_parts(self, webpage):
|
||||||
|
for encoded in reversed(re.findall(r'registerStreamedPrefetch\s*\(\s*"[\w/+=]+"\s*,\s*"([\w/+=]+)"', webpage)):
|
||||||
|
yield base64.b64decode(encoded).decode('utf-8', 'ignore')
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = self._match_valid_url(url)
|
mobj = self._match_valid_url(url)
|
||||||
video_id = mobj.group('id')
|
video_id = mobj.group('id')
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
fn = urllib.parse.unquote(url_basename(url))
|
fn = urllib.parse.unquote(url_basename(url))
|
||||||
title = os.path.splitext(fn)[0]
|
title = os.path.splitext(fn)[0]
|
||||||
|
|
||||||
password = self.get_param('videopassword')
|
password = self.get_param('videopassword')
|
||||||
if (self._og_search_title(webpage) == 'Dropbox - Password Required'
|
|
||||||
or 'Enter the password for this link' in webpage):
|
|
||||||
|
|
||||||
|
for part in self._yield_decoded_parts(webpage):
|
||||||
|
if '/sm/password' in part:
|
||||||
|
webpage = self._download_webpage(
|
||||||
|
update_url('https://www.dropbox.com/sm/password', query=part.partition('?')[2]), video_id)
|
||||||
|
break
|
||||||
|
|
||||||
|
if (self._og_search_title(webpage, default=None) == 'Dropbox - Password Required'
|
||||||
|
or 'Enter the password for this link' in webpage):
|
||||||
if password:
|
if password:
|
||||||
content_id = self._search_regex(r'content_id=(.*?)["\']', webpage, 'content_id')
|
|
||||||
payload = f'is_xhr=true&t={self._get_cookies("https://www.dropbox.com").get("t").value}&content_id={content_id}&password={password}&url={url}'
|
|
||||||
response = self._download_json(
|
response = self._download_json(
|
||||||
'https://www.dropbox.com/sm/auth', video_id, 'POSTing video password', data=payload.encode(),
|
'https://www.dropbox.com/sm/auth', video_id, 'POSTing video password',
|
||||||
headers={'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'})
|
headers={'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'},
|
||||||
|
data=urlencode_postdata({
|
||||||
|
'is_xhr': 'true',
|
||||||
|
't': self._get_cookies('https://www.dropbox.com')['t'].value,
|
||||||
|
'content_id': self._search_regex(r'content_id=([\w.+=/-]+)["\']', webpage, 'content id'),
|
||||||
|
'password': password,
|
||||||
|
'url': url,
|
||||||
|
}))
|
||||||
|
|
||||||
if response.get('status') != 'authed':
|
if response.get('status') != 'authed':
|
||||||
raise ExtractorError('Authentication failed!', expected=True)
|
raise ExtractorError('Invalid password', expected=True)
|
||||||
webpage = self._download_webpage(url, video_id)
|
elif not self._get_cookies('https://dropbox.com').get('sm_auth'):
|
||||||
elif self._get_cookies('https://dropbox.com').get('sm_auth'):
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
|
||||||
else:
|
|
||||||
raise ExtractorError('Password protected video, use --video-password <password>', expected=True)
|
raise ExtractorError('Password protected video, use --video-password <password>', expected=True)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
formats, subtitles, has_anonymous_download = [], {}, False
|
formats, subtitles = [], {}
|
||||||
for encoded in reversed(re.findall(r'registerStreamedPrefetch\s*\(\s*"[\w/+=]+"\s*,\s*"([\w/+=]+)"', webpage)):
|
has_anonymous_download = False
|
||||||
decoded = base64.b64decode(encoded).decode('utf-8', 'ignore')
|
thumbnail = None
|
||||||
|
for part in self._yield_decoded_parts(webpage):
|
||||||
if not has_anonymous_download:
|
if not has_anonymous_download:
|
||||||
has_anonymous_download = self._search_regex(
|
has_anonymous_download = self._search_regex(
|
||||||
r'(anonymous:\tanonymous)', decoded, 'anonymous', default=False)
|
r'(anonymous:\tanonymous)', part, 'anonymous', default=False)
|
||||||
transcode_url = self._search_regex(
|
transcode_url = self._search_regex(
|
||||||
r'\n.(https://[^\x03\x08\x12\n]+\.m3u8)', decoded, 'transcode url', default=None)
|
r'\n.(https://[^\x03\x08\x12\n]+\.m3u8)', part, 'transcode url', default=None)
|
||||||
if not transcode_url:
|
if not transcode_url:
|
||||||
continue
|
continue
|
||||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(transcode_url, video_id, 'mp4')
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(transcode_url, video_id, 'mp4')
|
||||||
|
thumbnail = self._search_regex(
|
||||||
|
r'(https://www\.dropbox\.com/temp_thumb_from_token/[\w/?&=]+)', part, 'thumbnail', default=None)
|
||||||
break
|
break
|
||||||
|
|
||||||
# downloads enabled we can get the original file
|
# downloads enabled we can get the original file
|
||||||
@ -89,4 +106,5 @@ def _real_extract(self, url):
|
|||||||
'title': title,
|
'title': title,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
}
|
}
|
||||||
|
@ -139,12 +139,11 @@ def _real_initialize(self):
|
|||||||
return
|
return
|
||||||
|
|
||||||
token_response = self._download_json(
|
token_response = self._download_json(
|
||||||
'https://production.dr-massive.com/api/authorization/anonymous-sso', None,
|
'https://isl.dr-massive.com/api/authorization/anonymous-sso', None,
|
||||||
note='Downloading anonymous token', headers={
|
note='Downloading anonymous token', headers={
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
}, query={
|
}, query={
|
||||||
'device': 'web_browser',
|
'device': 'phone_android',
|
||||||
'ff': 'idp,ldp,rpt',
|
|
||||||
'lang': 'da',
|
'lang': 'da',
|
||||||
'supportFallbackToken': 'true',
|
'supportFallbackToken': 'true',
|
||||||
}, data=json.dumps({
|
}, data=json.dumps({
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
url_or_none,
|
url_or_none,
|
||||||
variadic,
|
variadic,
|
||||||
)
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class ERTFlixBaseIE(InfoExtractor):
|
class ERTFlixBaseIE(InfoExtractor):
|
||||||
@ -74,29 +75,28 @@ class ERTFlixCodenameIE(ERTFlixBaseIE):
|
|||||||
|
|
||||||
def _extract_formats_and_subs(self, video_id):
|
def _extract_formats_and_subs(self, video_id):
|
||||||
media_info = self._call_api(video_id, codename=video_id)
|
media_info = self._call_api(video_id, codename=video_id)
|
||||||
formats, subs = [], {}
|
formats, subtitles = [], {}
|
||||||
for media_file in try_get(media_info, lambda x: x['MediaFiles'], list) or []:
|
for media in traverse_obj(media_info, (
|
||||||
for media in try_get(media_file, lambda x: x['Formats'], list) or []:
|
'MediaFiles', lambda _, v: v['RoleCodename'] == 'main',
|
||||||
fmt_url = url_or_none(try_get(media, lambda x: x['Url']))
|
'Formats', lambda _, v: url_or_none(v['Url']))):
|
||||||
if not fmt_url:
|
fmt_url = media['Url']
|
||||||
continue
|
ext = determine_ext(fmt_url)
|
||||||
ext = determine_ext(fmt_url)
|
if ext == 'm3u8':
|
||||||
if ext == 'm3u8':
|
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
||||||
formats_, subs_ = self._extract_m3u8_formats_and_subtitles(
|
fmt_url, video_id, m3u8_id='hls', ext='mp4', fatal=False)
|
||||||
fmt_url, video_id, m3u8_id='hls', ext='mp4', fatal=False)
|
elif ext == 'mpd':
|
||||||
elif ext == 'mpd':
|
fmts, subs = self._extract_mpd_formats_and_subtitles(
|
||||||
formats_, subs_ = self._extract_mpd_formats_and_subtitles(
|
fmt_url, video_id, mpd_id='dash', fatal=False)
|
||||||
fmt_url, video_id, mpd_id='dash', fatal=False)
|
else:
|
||||||
else:
|
formats.append({
|
||||||
formats.append({
|
'url': fmt_url,
|
||||||
'url': fmt_url,
|
'format_id': str_or_none(media.get('Id')),
|
||||||
'format_id': str_or_none(media.get('Id')),
|
})
|
||||||
})
|
continue
|
||||||
continue
|
formats.extend(fmts)
|
||||||
formats.extend(formats_)
|
self._merge_subtitles(subs, target=subtitles)
|
||||||
self._merge_subtitles(subs_, target=subs)
|
|
||||||
|
|
||||||
return formats, subs
|
return formats, subtitles
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
@ -294,37 +294,37 @@ def _real_extract(self, url):
|
|||||||
class WatchESPNIE(AdobePassIE):
|
class WatchESPNIE(AdobePassIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?espn\.com/(?:watch|espnplus)/player/_/id/(?P<id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'
|
_VALID_URL = r'https?://(?:www\.)?espn\.com/(?:watch|espnplus)/player/_/id/(?P<id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.espn.com/watch/player/_/id/dbbc6b1d-c084-4b47-9878-5f13c56ce309',
|
'url': 'https://www.espn.com/watch/player/_/id/11ce417a-6ac9-42b6-8a15-46aeb9ad5710',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'dbbc6b1d-c084-4b47-9878-5f13c56ce309',
|
'id': '11ce417a-6ac9-42b6-8a15-46aeb9ad5710',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Huddersfield vs. Burnley',
|
'title': 'Abilene Chrstn vs. Texas Tech',
|
||||||
'duration': 7500,
|
'duration': 14166,
|
||||||
'thumbnail': 'https://artwork.api.espn.com/artwork/collections/media/dbbc6b1d-c084-4b47-9878-5f13c56ce309/default?width=640&apikey=1ngjw23osgcis1i1vbj96lmfqs',
|
'thumbnail': 'https://s.secure.espncdn.com/stitcher/artwork/collections/media/11ce417a-6ac9-42b6-8a15-46aeb9ad5710/16x9.jpg?timestamp=202407252343&showBadge=true&cb=12&package=ESPN_PLUS',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.espn.com/watch/player/_/id/a049a56e-a7ce-477e-aef3-c7e48ef8221c',
|
'url': 'https://www.espn.com/watch/player/_/id/90a2c85d-75e0-4b1e-a878-8e428a3cb2f3',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'a049a56e-a7ce-477e-aef3-c7e48ef8221c',
|
'id': '90a2c85d-75e0-4b1e-a878-8e428a3cb2f3',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Dynamo Dresden vs. VfB Stuttgart (Round #1) (German Cup)',
|
'title': 'UC Davis vs. California',
|
||||||
'duration': 8335,
|
'duration': 9547,
|
||||||
'thumbnail': 'https://s.secure.espncdn.com/stitcher/artwork/collections/media/bd1f3d12-0654-47d9-852e-71b85ea695c7/16x9.jpg?timestamp=202201112217&showBadge=true&cb=12&package=ESPN_PLUS',
|
'thumbnail': 'https://artwork.api.espn.com/artwork/collections/media/90a2c85d-75e0-4b1e-a878-8e428a3cb2f3/default?width=640&apikey=1ngjw23osgcis1i1vbj96lmfqs',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.espn.com/espnplus/player/_/id/317f5fd1-c78a-4ebe-824a-129e0d348421',
|
'url': 'https://www.espn.com/watch/player/_/id/c4313bbe-95b5-4bb8-b251-ac143ea0fc54',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '317f5fd1-c78a-4ebe-824a-129e0d348421',
|
'id': 'c4313bbe-95b5-4bb8-b251-ac143ea0fc54',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'The Wheel - Episode 10',
|
'title': 'The College Football Show',
|
||||||
'duration': 3352,
|
'duration': 3639,
|
||||||
'thumbnail': 'https://s.secure.espncdn.com/stitcher/artwork/collections/media/317f5fd1-c78a-4ebe-824a-129e0d348421/16x9.jpg?timestamp=202205031523&showBadge=true&cb=12&package=ESPN_PLUS',
|
'thumbnail': 'https://artwork.api.espn.com/artwork/collections/media/c4313bbe-95b5-4bb8-b251-ac143ea0fc54/default?width=640&apikey=1ngjw23osgcis1i1vbj96lmfqs',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
@ -353,6 +353,13 @@ def _real_extract(self, url):
|
|||||||
if not cookie:
|
if not cookie:
|
||||||
self.raise_login_required(method='cookies')
|
self.raise_login_required(method='cookies')
|
||||||
|
|
||||||
|
jwt = self._search_regex(r'=([^|]+)\|', cookie.value, 'cookie jwt')
|
||||||
|
id_token = self._download_json(
|
||||||
|
'https://registerdisney.go.com/jgc/v6/client/ESPN-ONESITE.WEB-PROD/guest/refresh-auth',
|
||||||
|
None, 'Refreshing token', headers={'Content-Type': 'application/json'}, data=json.dumps({
|
||||||
|
'refreshToken': json.loads(base64.urlsafe_b64decode(f'{jwt}==='))['refresh_token'],
|
||||||
|
}).encode())['data']['token']['id_token']
|
||||||
|
|
||||||
assertion = self._call_bamgrid_api(
|
assertion = self._call_bamgrid_api(
|
||||||
'devices', video_id,
|
'devices', video_id,
|
||||||
headers={'Content-Type': 'application/json; charset=UTF-8'},
|
headers={'Content-Type': 'application/json; charset=UTF-8'},
|
||||||
@ -371,7 +378,7 @@ def _real_extract(self, url):
|
|||||||
})['access_token']
|
})['access_token']
|
||||||
|
|
||||||
assertion = self._call_bamgrid_api(
|
assertion = self._call_bamgrid_api(
|
||||||
'accounts/grant', video_id, payload={'id_token': cookie.value.split('|')[1]},
|
'accounts/grant', video_id, payload={'id_token': id_token},
|
||||||
headers={
|
headers={
|
||||||
'Authorization': token,
|
'Authorization': token,
|
||||||
'Content-Type': 'application/json; charset=UTF-8',
|
'Content-Type': 'application/json; charset=UTF-8',
|
||||||
|
@ -84,7 +84,7 @@ class FacebookIE(InfoExtractor):
|
|||||||
'timestamp': 1692346159,
|
'timestamp': 1692346159,
|
||||||
'thumbnail': r're:^https?://.*',
|
'thumbnail': r're:^https?://.*',
|
||||||
'uploader_id': '100063551323670',
|
'uploader_id': '100063551323670',
|
||||||
'duration': 3132.184,
|
'duration': 3133.583,
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'concurrent_view_count': 0,
|
'concurrent_view_count': 0,
|
||||||
},
|
},
|
||||||
@ -112,9 +112,10 @@ class FacebookIE(InfoExtractor):
|
|||||||
'upload_date': '20140506',
|
'upload_date': '20140506',
|
||||||
'timestamp': 1399398998,
|
'timestamp': 1399398998,
|
||||||
'thumbnail': r're:^https?://.*',
|
'thumbnail': r're:^https?://.*',
|
||||||
'uploader_id': 'pfbid028wxorhX2ErLFJ578N6P3crHD3PHmXTCqCvfBpsnbSLmbokwSY75p5hWBjHGkG4zxl',
|
'uploader_id': 'pfbid05AzrFTXgY37tqwaSgbFTTEpCLBjjEJHkigogwGiRPtKEpAsJYJpzE94H1RxYXWEtl',
|
||||||
'duration': 131.03,
|
'duration': 131.03,
|
||||||
'concurrent_view_count': int,
|
'concurrent_view_count': int,
|
||||||
|
'view_count': int,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'note': 'Video with DASH manifest',
|
'note': 'Video with DASH manifest',
|
||||||
@ -167,7 +168,7 @@ class FacebookIE(InfoExtractor):
|
|||||||
# have 1080P, but only up to 720p in swf params
|
# have 1080P, but only up to 720p in swf params
|
||||||
# data.video.story.attachments[].media
|
# data.video.story.attachments[].media
|
||||||
'url': 'https://www.facebook.com/cnn/videos/10155529876156509/',
|
'url': 'https://www.facebook.com/cnn/videos/10155529876156509/',
|
||||||
'md5': 'ca63897a90c9452efee5f8c40d080e25',
|
'md5': '1659aa21fb3dd1585874f668e81a72c8',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '10155529876156509',
|
'id': '10155529876156509',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@ -180,9 +181,10 @@ class FacebookIE(InfoExtractor):
|
|||||||
'view_count': int,
|
'view_count': int,
|
||||||
'uploader_id': '100059479812265',
|
'uploader_id': '100059479812265',
|
||||||
'concurrent_view_count': int,
|
'concurrent_view_count': int,
|
||||||
'duration': 44.478,
|
'duration': 44.181,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
|
# FIXME: unable to extract uploader, no formats found
|
||||||
# bigPipe.onPageletArrive ... onPageletArrive pagelet_group_mall
|
# bigPipe.onPageletArrive ... onPageletArrive pagelet_group_mall
|
||||||
# data.node.comet_sections.content.story.attachments[].style_type_renderer.attachment.media
|
# data.node.comet_sections.content.story.attachments[].style_type_renderer.attachment.media
|
||||||
'url': 'https://www.facebook.com/yaroslav.korpan/videos/1417995061575415/',
|
'url': 'https://www.facebook.com/yaroslav.korpan/videos/1417995061575415/',
|
||||||
@ -241,9 +243,9 @@ class FacebookIE(InfoExtractor):
|
|||||||
'timestamp': 1511548260,
|
'timestamp': 1511548260,
|
||||||
'upload_date': '20171124',
|
'upload_date': '20171124',
|
||||||
'uploader': 'Vickie Gentry',
|
'uploader': 'Vickie Gentry',
|
||||||
'uploader_id': 'pfbid0FuZhHCeWDAxWxEbr3yKPFaRstXvRxgsp9uCPG6GjD4J2AitB35NUAuJ4Q75KcjiDl',
|
'uploader_id': 'pfbid0FkkycT95ySNNyfCw4Cho6u5G7WbbZEcxT496Hq8rtx1K3LcTCATpR3wnyYhmyGC5l',
|
||||||
'thumbnail': r're:^https?://.*',
|
'thumbnail': r're:^https?://.*',
|
||||||
'duration': 148.435,
|
'duration': 148.224,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# data.node.comet_sections.content.story.attachments[].styles.attachment.media
|
# data.node.comet_sections.content.story.attachments[].styles.attachment.media
|
||||||
@ -271,7 +273,7 @@ class FacebookIE(InfoExtractor):
|
|||||||
'description': 'Today Makkovik\'s own Pilot Mandy Smith made her inaugural landing on the airstrip in her hometown. What a proud moment as we all cheered and...',
|
'description': 'Today Makkovik\'s own Pilot Mandy Smith made her inaugural landing on the airstrip in her hometown. What a proud moment as we all cheered and...',
|
||||||
'thumbnail': r're:^https?://.*',
|
'thumbnail': r're:^https?://.*',
|
||||||
'uploader': 'Lela Evans',
|
'uploader': 'Lela Evans',
|
||||||
'uploader_id': 'pfbid0shZJipuigyy5mqrUJn9ub5LJFWNHvan5prtyi3LrDuuuJ4NwrURgnQHYR9fywBepl',
|
'uploader_id': 'pfbid0swT2y7t6TAsZVBvcyeYPdhTMefGaS26mzUwML3vd1ma6ndGZKxsyS4Ssu3jitZLXl',
|
||||||
'upload_date': '20231228',
|
'upload_date': '20231228',
|
||||||
'timestamp': 1703804085,
|
'timestamp': 1703804085,
|
||||||
'duration': 394.347,
|
'duration': 394.347,
|
||||||
@ -322,7 +324,7 @@ class FacebookIE(InfoExtractor):
|
|||||||
'upload_date': '20180523',
|
'upload_date': '20180523',
|
||||||
'uploader': 'ESL One Dota 2',
|
'uploader': 'ESL One Dota 2',
|
||||||
'uploader_id': '100066514874195',
|
'uploader_id': '100066514874195',
|
||||||
'duration': 4524.212,
|
'duration': 4524.001,
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'thumbnail': r're:^https?://.*',
|
'thumbnail': r're:^https?://.*',
|
||||||
'concurrent_view_count': int,
|
'concurrent_view_count': int,
|
||||||
@ -339,9 +341,9 @@ class FacebookIE(InfoExtractor):
|
|||||||
'title': 'Josef',
|
'title': 'Josef',
|
||||||
'thumbnail': r're:^https?://.*',
|
'thumbnail': r're:^https?://.*',
|
||||||
'concurrent_view_count': int,
|
'concurrent_view_count': int,
|
||||||
'uploader_id': 'pfbid0cibUN6tV7DYgdbJdsUFN46wc4jKpVSPAvJQhFofGqBGmVn3V3JtAs2tfUwziw2hUl',
|
'uploader_id': 'pfbid02gpfwRM2XvdEJfsERupwQiNmBiDArc38RMRYZnap372q6Vs7MtFTVy72mmFWpJBTKl',
|
||||||
'timestamp': 1549275572,
|
'timestamp': 1549275572,
|
||||||
'duration': 3.413,
|
'duration': 3.283,
|
||||||
'uploader': 'Josef Novak',
|
'uploader': 'Josef Novak',
|
||||||
'description': '',
|
'description': '',
|
||||||
'upload_date': '20190204',
|
'upload_date': '20190204',
|
||||||
@ -396,6 +398,7 @@ class FacebookIE(InfoExtractor):
|
|||||||
'playlist_count': 1,
|
'playlist_count': 1,
|
||||||
'skip': 'Requires logging in',
|
'skip': 'Requires logging in',
|
||||||
}, {
|
}, {
|
||||||
|
# FIXME: Cannot parse data error
|
||||||
# data.event.cover_media_renderer.cover_video
|
# data.event.cover_media_renderer.cover_video
|
||||||
'url': 'https://m.facebook.com/events/1509582499515440',
|
'url': 'https://m.facebook.com/events/1509582499515440',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@ -498,7 +501,8 @@ def extract_metadata(webpage):
|
|||||||
or get_first(post, ('video', 'creation_story', 'attachments', ..., 'media', lambda k, v: k == 'owner' and v['name']))
|
or get_first(post, ('video', 'creation_story', 'attachments', ..., 'media', lambda k, v: k == 'owner' and v['name']))
|
||||||
or get_first(post, (..., 'video', lambda k, v: k == 'owner' and v['name']))
|
or get_first(post, (..., 'video', lambda k, v: k == 'owner' and v['name']))
|
||||||
or get_first(post, ('node', 'actors', ..., {dict}))
|
or get_first(post, ('node', 'actors', ..., {dict}))
|
||||||
or get_first(post, ('event', 'event_creator', {dict})) or {})
|
or get_first(post, ('event', 'event_creator', {dict}))
|
||||||
|
or get_first(post, ('video', 'creation_story', 'short_form_video_context', 'video_owner', {dict})) or {})
|
||||||
uploader = uploader_data.get('name') or (
|
uploader = uploader_data.get('name') or (
|
||||||
clean_html(get_element_by_id('fbPhotoPageAuthorName', webpage))
|
clean_html(get_element_by_id('fbPhotoPageAuthorName', webpage))
|
||||||
or self._search_regex(
|
or self._search_regex(
|
||||||
@ -524,6 +528,11 @@ def extract_metadata(webpage):
|
|||||||
webpage, 'view count', default=None)),
|
webpage, 'view count', default=None)),
|
||||||
'concurrent_view_count': get_first(post, (
|
'concurrent_view_count': get_first(post, (
|
||||||
('video', (..., ..., 'attachments', ..., 'media')), 'liveViewerCount', {int_or_none})),
|
('video', (..., ..., 'attachments', ..., 'media')), 'liveViewerCount', {int_or_none})),
|
||||||
|
**traverse_obj(post, (lambda _, v: video_id in v['url'], 'feedback', {
|
||||||
|
'like_count': ('likers', 'count', {int}),
|
||||||
|
'comment_count': ('total_comment_count', {int}),
|
||||||
|
'repost_count': ('share_count_reduced', {parse_count}),
|
||||||
|
}), get_all=False),
|
||||||
}
|
}
|
||||||
|
|
||||||
info_json_ld = self._search_json_ld(webpage, video_id, default={})
|
info_json_ld = self._search_json_ld(webpage, video_id, default={})
|
||||||
@ -932,18 +941,21 @@ class FacebookReelIE(InfoExtractor):
|
|||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.facebook.com/reel/1195289147628387',
|
'url': 'https://www.facebook.com/reel/1195289147628387',
|
||||||
'md5': 'f13dd37f2633595982db5ed8765474d3',
|
'md5': 'a53256d10fc2105441fe0c4212ed8cea',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1195289147628387',
|
'id': '1195289147628387',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'md5:b05800b5b1ad56c0ca78bd3807b6a61e',
|
'title': r're:9\.6K views · 355 reactions .+ Let the “Slapathon” commence!! .+ LL COOL J · Mama Said Knock You Out$',
|
||||||
'description': 'md5:22f03309b216ac84720183961441d8db',
|
'description': r're:When your trying to help your partner .+ LL COOL J · Mama Said Knock You Out$',
|
||||||
'uploader': 'md5:723e6cb3091241160f20b3c5dc282af1',
|
'uploader': 'Beast Camp Training',
|
||||||
'uploader_id': '100040874179269',
|
'uploader_id': '100040874179269',
|
||||||
'duration': 9.579,
|
'duration': 9.579,
|
||||||
'timestamp': 1637502609,
|
'timestamp': 1637502609,
|
||||||
'upload_date': '20211121',
|
'upload_date': '20211121',
|
||||||
'thumbnail': r're:^https?://.*',
|
'thumbnail': r're:^https?://.*',
|
||||||
|
'like_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'repost_count': int,
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
|
|
||||||
class FC2IE(InfoExtractor):
|
class FC2IE(InfoExtractor):
|
||||||
_VALID_URL = r'^(?:https?://video\.fc2\.com/(?:[^/]+/)*content/|fc2:)(?P<id>[^/]+)'
|
_VALID_URL = r'(?:https?://video\.fc2\.com/(?:[^/]+/)*content/|fc2:)(?P<id>[^/]+)'
|
||||||
IE_NAME = 'fc2'
|
IE_NAME = 'fc2'
|
||||||
_NETRC_MACHINE = 'fc2'
|
_NETRC_MACHINE = 'fc2'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
from .commonprotocols import RtmpIE
|
from .commonprotocols import RtmpIE
|
||||||
from .youtube import YoutubeIE
|
from .youtube import YoutubeIE
|
||||||
from ..compat import compat_etree_fromstring
|
from ..compat import compat_etree_fromstring
|
||||||
|
from ..networking.impersonate import ImpersonateTarget
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
KNOWN_EXTENSIONS,
|
KNOWN_EXTENSIONS,
|
||||||
MEDIA_EXTENSIONS,
|
MEDIA_EXTENSIONS,
|
||||||
@ -2340,7 +2341,7 @@ def _real_extract(self, url):
|
|||||||
default_search = 'fixup_error'
|
default_search = 'fixup_error'
|
||||||
|
|
||||||
if default_search in ('auto', 'auto_warning', 'fixup_error'):
|
if default_search in ('auto', 'auto_warning', 'fixup_error'):
|
||||||
if re.match(r'^[^\s/]+\.[^\s/]+/', url):
|
if re.match(r'[^\s/]+\.[^\s/]+/', url):
|
||||||
self.report_warning('The url doesn\'t specify the protocol, trying with http')
|
self.report_warning('The url doesn\'t specify the protocol, trying with http')
|
||||||
return self.url_result('http://' + url)
|
return self.url_result('http://' + url)
|
||||||
elif default_search != 'fixup_error':
|
elif default_search != 'fixup_error':
|
||||||
@ -2373,6 +2374,12 @@ def _real_extract(self, url):
|
|||||||
else:
|
else:
|
||||||
video_id = self._generic_id(url)
|
video_id = self._generic_id(url)
|
||||||
|
|
||||||
|
# Try to impersonate a web-browser by default if possible
|
||||||
|
# Skip impersonation if not available to omit the warning
|
||||||
|
impersonate = self._configuration_arg('impersonate', [''])
|
||||||
|
if 'false' in impersonate or not self._downloader._impersonate_target_available(ImpersonateTarget()):
|
||||||
|
impersonate = None
|
||||||
|
|
||||||
# Some webservers may serve compressed content of rather big size (e.g. gzipped flac)
|
# Some webservers may serve compressed content of rather big size (e.g. gzipped flac)
|
||||||
# making it impossible to download only chunk of the file (yet we need only 512kB to
|
# making it impossible to download only chunk of the file (yet we need only 512kB to
|
||||||
# test whether it's HTML or not). According to yt-dlp default Accept-Encoding
|
# test whether it's HTML or not). According to yt-dlp default Accept-Encoding
|
||||||
@ -2384,7 +2391,7 @@ def _real_extract(self, url):
|
|||||||
full_response = self._request_webpage(url, video_id, headers=filter_dict({
|
full_response = self._request_webpage(url, video_id, headers=filter_dict({
|
||||||
'Accept-Encoding': 'identity',
|
'Accept-Encoding': 'identity',
|
||||||
'Referer': smuggled_data.get('referer'),
|
'Referer': smuggled_data.get('referer'),
|
||||||
}))
|
}), impersonate=impersonate)
|
||||||
new_url = full_response.url
|
new_url = full_response.url
|
||||||
if new_url != extract_basic_auth(url)[0]:
|
if new_url != extract_basic_auth(url)[0]:
|
||||||
self.report_following_redirect(new_url)
|
self.report_following_redirect(new_url)
|
||||||
@ -2400,7 +2407,7 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
# Check for direct link to a video
|
# Check for direct link to a video
|
||||||
content_type = full_response.headers.get('Content-Type', '').lower()
|
content_type = full_response.headers.get('Content-Type', '').lower()
|
||||||
m = re.match(r'^(?P<type>audio|video|application(?=/(?:ogg$|(?:vnd\.apple\.|x-)?mpegurl)))/(?P<format_id>[^;\s]+)', content_type)
|
m = re.match(r'(?P<type>audio|video|application(?=/(?:ogg$|(?:vnd\.apple\.|x-)?mpegurl)))/(?P<format_id>[^;\s]+)', content_type)
|
||||||
if m:
|
if m:
|
||||||
self.report_detected('direct video link')
|
self.report_detected('direct video link')
|
||||||
headers = filter_dict({'Referer': smuggled_data.get('referer')})
|
headers = filter_dict({'Referer': smuggled_data.get('referer')})
|
||||||
|
91
yt_dlp/extractor/germanupa.py
Normal file
91
yt_dlp/extractor/germanupa.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
from .common import InfoExtractor
|
||||||
|
from .vimeo import VimeoIE
|
||||||
|
from ..utils import (
|
||||||
|
parse_qs,
|
||||||
|
traverse_obj,
|
||||||
|
url_or_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GermanupaIE(InfoExtractor):
|
||||||
|
IE_DESC = 'germanupa.de'
|
||||||
|
_VALID_URL = r'https?://germanupa\.de/mediathek/(?P<id>[\w-]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://germanupa.de/mediathek/4-figma-beratung-deine-sprechstunde-fuer-figma-fragen',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '909179246',
|
||||||
|
'title': 'Tutorial: #4 Figma Beratung - Deine Sprechstunde für Figma-Fragen',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'uploader': 'German UPA',
|
||||||
|
'uploader_id': 'germanupa',
|
||||||
|
'thumbnail': 'https://i.vimeocdn.com/video/1792564420-7415283ccef8bf8702dab8c6b7515555ceeb7a1c11371ffcc133b8e887dbf70e-d_1280',
|
||||||
|
'uploader_url': 'https://vimeo.com/germanupa',
|
||||||
|
'duration': 3987,
|
||||||
|
},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
}, {
|
||||||
|
'note': 'audio, uses GenericIE',
|
||||||
|
'url': 'https://germanupa.de/mediathek/live-vom-ux-festival-neuigkeiten-von-figma-jobmarkt-agenturszene-interview-zu-sustainable',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1867346676',
|
||||||
|
'title': 'Live vom UX Festival: Neuigkeiten von Figma, Jobmarkt, Agenturszene & Interview zu Sustainable UX',
|
||||||
|
'ext': 'opus',
|
||||||
|
'timestamp': 1720545088,
|
||||||
|
'upload_date': '20240709',
|
||||||
|
'duration': 3910.557,
|
||||||
|
'like_count': int,
|
||||||
|
'description': 'md5:db2aed5ff131e177a7b33901e9a8db05',
|
||||||
|
'uploader': 'German UPA',
|
||||||
|
'repost_count': int,
|
||||||
|
'genres': ['Science'],
|
||||||
|
'license': 'all-rights-reserved',
|
||||||
|
'uploader_url': 'https://soundcloud.com/user-80097677',
|
||||||
|
'uploader_id': '471579486',
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'thumbnail': 'https://i1.sndcdn.com/artworks-oCti2e9GhaZFWBqY-48ybGw-original.jpg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'note': 'Nur für Mitglieder/Just for members',
|
||||||
|
'url': 'https://germanupa.de/mediathek/ux-festival-2024-usability-tests-und-ai',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '986994430',
|
||||||
|
'title': 'UX Festival 2024 "Usability Tests und AI" von Lennart Weber',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'release_date': '20240719',
|
||||||
|
'uploader_url': 'https://vimeo.com/germanupa',
|
||||||
|
'timestamp': 1721373980,
|
||||||
|
'license': 'by-sa',
|
||||||
|
'like_count': int,
|
||||||
|
'thumbnail': 'https://i.vimeocdn.com/video/1904187064-2a672630c30f9ad787bd390bff3f51d7506a3e8416763ba6dbf465732b165c5c-d_1280',
|
||||||
|
'duration': 2146,
|
||||||
|
'release_timestamp': 1721373980,
|
||||||
|
'uploader': 'German UPA',
|
||||||
|
'uploader_id': 'germanupa',
|
||||||
|
'upload_date': '20240719',
|
||||||
|
'comment_count': int,
|
||||||
|
},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
|
'skip': 'login required',
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
param_url = traverse_obj(
|
||||||
|
self._search_regex(
|
||||||
|
r'<iframe[^>]+data-src\s*?=\s*?([\'"])(?P<url>https://germanupa\.de/media/oembed\?url=(?:(?!\1).)+)\1',
|
||||||
|
webpage, 'embedded video', default=None, group='url'),
|
||||||
|
({parse_qs}, 'url', 0, {url_or_none}))
|
||||||
|
|
||||||
|
if not param_url:
|
||||||
|
if self._search_regex(
|
||||||
|
r'<div[^>]+class\s*?=\s*?([\'"])(?:(?!\1).)*login-wrapper(?:(?!\1).)*\1',
|
||||||
|
webpage, 'login wrapper', default=None):
|
||||||
|
self.raise_login_required('This video is only available for members')
|
||||||
|
return self.url_result(url, 'Generic') # Fall back to generic to extract audio
|
||||||
|
|
||||||
|
real_url = param_url.replace('https://vimeo.com/', 'https://player.vimeo.com/video/')
|
||||||
|
return self.url_result(VimeoIE._smuggle_referrer(real_url, url), VimeoIE, video_id)
|
@ -52,7 +52,7 @@ class GetCourseRuIE(InfoExtractor):
|
|||||||
_BASE_URL_RE = rf'https?://(?:(?!player02\.)[^.]+\.getcourse\.(?:ru|io)|{"|".join(map(re.escape, _DOMAINS))})'
|
_BASE_URL_RE = rf'https?://(?:(?!player02\.)[^.]+\.getcourse\.(?:ru|io)|{"|".join(map(re.escape, _DOMAINS))})'
|
||||||
_VALID_URL = [
|
_VALID_URL = [
|
||||||
rf'{_BASE_URL_RE}/(?!pl/|teach/)(?P<id>[^?#]+)',
|
rf'{_BASE_URL_RE}/(?!pl/|teach/)(?P<id>[^?#]+)',
|
||||||
rf'{_BASE_URL_RE}/(:?pl/)?teach/control/lesson/view\?(?:[^#]+&)?id=(?P<id>\d+)',
|
rf'{_BASE_URL_RE}/(?:pl/)?teach/control/lesson/view\?(?:[^#]+&)?id=(?P<id>\d+)',
|
||||||
]
|
]
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://academymel.online/3video_1',
|
'url': 'http://academymel.online/3video_1',
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
|
|
||||||
class GolemIE(InfoExtractor):
|
class GolemIE(InfoExtractor):
|
||||||
_VALID_URL = r'^https?://video\.golem\.de/.+?/(?P<id>.+?)/'
|
_VALID_URL = r'https?://video\.golem\.de/.+?/(?P<id>.+?)/'
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://video.golem.de/handy/14095/iphone-6-und-6-plus-test.html',
|
'url': 'http://video.golem.de/handy/14095/iphone-6-und-6-plus-test.html',
|
||||||
'md5': 'c1a2c0a3c863319651c7c992c5ee29bf',
|
'md5': 'c1a2c0a3c863319651c7c992c5ee29bf',
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
class HRFernsehenIE(InfoExtractor):
|
class HRFernsehenIE(InfoExtractor):
|
||||||
IE_NAME = 'hrfernsehen'
|
IE_NAME = 'hrfernsehen'
|
||||||
_VALID_URL = r'^https?://www\.(?:hr-fernsehen|hessenschau)\.de/.*,video-(?P<id>[0-9]{6})\.html'
|
_VALID_URL = r'https?://www\.(?:hr-fernsehen|hessenschau)\.de/.*,video-(?P<id>[0-9]{6})\.html'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.hessenschau.de/tv-sendung/hessenschau-vom-26082020,video-130546.html',
|
'url': 'https://www.hessenschau.de/tv-sendung/hessenschau-vom-26082020,video-130546.html',
|
||||||
'md5': '5c4e0ba94677c516a2f65a84110fc536',
|
'md5': '5c4e0ba94677c516a2f65a84110fc536',
|
||||||
|
@ -8,15 +8,19 @@
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
parse_duration,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
try_get,
|
try_get,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
|
unified_strdate,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class HuyaLiveIE(InfoExtractor):
|
class HuyaLiveIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.|m\.)?huya\.com/(?P<id>[^/#?&]+)(?:\D|$)'
|
_VALID_URL = r'https?://(?:www\.|m\.)?huya\.com/(?!(?:video/play/))(?P<id>[^/#?&]+)(?:\D|$)'
|
||||||
IE_NAME = 'huya:live'
|
IE_NAME = 'huya:live'
|
||||||
IE_DESC = 'huya.com'
|
IE_DESC = 'huya.com'
|
||||||
TESTS = [{
|
TESTS = [{
|
||||||
@ -24,6 +28,7 @@ class HuyaLiveIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '572329',
|
'id': '572329',
|
||||||
'title': str,
|
'title': str,
|
||||||
|
'ext': 'flv',
|
||||||
'description': str,
|
'description': str,
|
||||||
'is_live': True,
|
'is_live': True,
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
@ -131,3 +136,76 @@ def encrypt(self, params, stream_info, stream_name):
|
|||||||
fm = base64.b64decode(params['fm']).decode().split('_', 1)[0]
|
fm = base64.b64decode(params['fm']).decode().split('_', 1)[0]
|
||||||
ss = hashlib.md5('|'.join([params['seqid'], params['ctype'], params['t']]))
|
ss = hashlib.md5('|'.join([params['seqid'], params['ctype'], params['t']]))
|
||||||
return fm, ss
|
return fm, ss
|
||||||
|
|
||||||
|
|
||||||
|
class HuyaVideoIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?huya\.com/video/play/(?P<id>\d+)\.html'
|
||||||
|
IE_NAME = 'huya:video'
|
||||||
|
IE_DESC = '虎牙视频'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.huya.com/video/play/1002412640.html',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1002412640',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '8月3日',
|
||||||
|
'thumbnail': r're:https?://.*\.jpg',
|
||||||
|
'duration': 14,
|
||||||
|
'uploader': '虎牙-ATS欧卡车队青木',
|
||||||
|
'uploader_id': '1564376151',
|
||||||
|
'upload_date': '20240803',
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://www.huya.com/video/play/556054543.html',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '556054543',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '我不挑事 也不怕事',
|
||||||
|
'thumbnail': r're:https?://.*\.jpg',
|
||||||
|
'duration': 1864,
|
||||||
|
'uploader': '卡尔',
|
||||||
|
'uploader_id': '367138632',
|
||||||
|
'upload_date': '20210811',
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url: str):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
video_data = self._download_json(
|
||||||
|
'https://liveapi.huya.com/moment/getMomentContent', video_id,
|
||||||
|
query={'videoId': video_id})['data']['moment']['videoInfo']
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for definition in traverse_obj(video_data, ('definitions', lambda _, v: url_or_none(v['url']))):
|
||||||
|
formats.append({
|
||||||
|
'url': definition['url'],
|
||||||
|
**traverse_obj(definition, {
|
||||||
|
'format_id': ('defName', {str}),
|
||||||
|
'width': ('width', {int_or_none}),
|
||||||
|
'height': ('height', {int_or_none}),
|
||||||
|
'filesize': ('size', {int_or_none}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'formats': formats,
|
||||||
|
**traverse_obj(video_data, {
|
||||||
|
'title': ('videoTitle', {str}),
|
||||||
|
'thumbnail': ('videoCover', {url_or_none}),
|
||||||
|
'duration': ('videoDuration', {parse_duration}),
|
||||||
|
'uploader': ('nickName', {str}),
|
||||||
|
'uploader_id': ('uid', {str_or_none}),
|
||||||
|
'upload_date': ('videoUploadTime', {unified_strdate}),
|
||||||
|
'view_count': ('videoPlayNum', {int_or_none}),
|
||||||
|
'comment_count': ('videoCommentNum', {int_or_none}),
|
||||||
|
'like_count': ('favorCount', {int_or_none}),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
@ -48,7 +48,6 @@ class InstagramBaseIE(InfoExtractor):
|
|||||||
'X-IG-WWW-Claim': '0',
|
'X-IG-WWW-Claim': '0',
|
||||||
'Origin': 'https://www.instagram.com',
|
'Origin': 'https://www.instagram.com',
|
||||||
'Accept': '*/*',
|
'Accept': '*/*',
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def _perform_login(self, username, password):
|
def _perform_login(self, username, password):
|
||||||
@ -435,10 +434,10 @@ def _real_extract(self, url):
|
|||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
'Referer': url,
|
'Referer': url,
|
||||||
}, query={
|
}, query={
|
||||||
'query_hash': '9f8827793ef34641b2fb195d4d41151c',
|
'doc_id': '8845758582119845',
|
||||||
'variables': json.dumps(variables, separators=(',', ':')),
|
'variables': json.dumps(variables, separators=(',', ':')),
|
||||||
})
|
})
|
||||||
media.update(traverse_obj(general_info, ('data', 'shortcode_media')) or {})
|
media.update(traverse_obj(general_info, ('data', 'xdt_shortcode_media')) or {})
|
||||||
|
|
||||||
if not general_info:
|
if not general_info:
|
||||||
self.report_warning('General metadata extraction failed (some metadata might be missing).', video_id)
|
self.report_warning('General metadata extraction failed (some metadata might be missing).', video_id)
|
||||||
|
@ -25,9 +25,29 @@ class IPrimaIE(InfoExtractor):
|
|||||||
'id': 'p51388',
|
'id': 'p51388',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Partička (92)',
|
'title': 'Partička (92)',
|
||||||
'description': 'md5:859d53beae4609e6dd7796413f1b6cac',
|
'description': 'md5:57943f6a50d6188288c3a579d2fd5f01',
|
||||||
'upload_date': '20201103',
|
'episode': 'Partička (92)',
|
||||||
'timestamp': 1604437480,
|
'season': 'Partička',
|
||||||
|
'series': 'Prima Partička',
|
||||||
|
'episode_number': 92,
|
||||||
|
'thumbnail': 'https://d31b9s05ygj54s.cloudfront.net/prima-plus/image/video-ef6cf9de-c980-4443-92e4-17fe8bccd45c-16x9.jpeg',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True, # m3u8 download
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://zoom.iprima.cz/porady/krasy-kanarskych-ostrovu/tenerife-v-risi-ohne',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p1412199',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'episode_number': 3,
|
||||||
|
'episode': 'Tenerife: V říši ohně',
|
||||||
|
'description': 'md5:4b4a05c574b5eaef130e68d4811c3f2c',
|
||||||
|
'duration': 3111.0,
|
||||||
|
'thumbnail': 'https://d31b9s05ygj54s.cloudfront.net/prima-plus/image/video-f66dd7fb-c1a0-47d1-b3bc-7db328d566c5-16x9-1711636518.jpg/t_16x9_medium_1366_768',
|
||||||
|
'title': 'Tenerife: V říši ohně',
|
||||||
|
'timestamp': 1711825800,
|
||||||
|
'upload_date': '20240330',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True, # m3u8 download
|
'skip_download': True, # m3u8 download
|
||||||
@ -131,6 +151,7 @@ def _real_extract(self, url):
|
|||||||
video_id = self._search_regex((
|
video_id = self._search_regex((
|
||||||
r'productId\s*=\s*([\'"])(?P<id>p\d+)\1',
|
r'productId\s*=\s*([\'"])(?P<id>p\d+)\1',
|
||||||
r'pproduct_id\s*=\s*([\'"])(?P<id>p\d+)\1',
|
r'pproduct_id\s*=\s*([\'"])(?P<id>p\d+)\1',
|
||||||
|
r'let\s+videos\s*=\s*([\'"])(?P<id>p\d+)\1',
|
||||||
), webpage, 'real id', group='id', default=None)
|
), webpage, 'real id', group='id', default=None)
|
||||||
|
|
||||||
if not video_id:
|
if not video_id:
|
||||||
@ -176,7 +197,7 @@ def _real_extract(self, url):
|
|||||||
final_result = self._search_json_ld(webpage, video_id, default={})
|
final_result = self._search_json_ld(webpage, video_id, default={})
|
||||||
final_result.update({
|
final_result.update({
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': final_result.get('title') or title,
|
||||||
'thumbnail': self._html_search_meta(
|
'thumbnail': self._html_search_meta(
|
||||||
['thumbnail', 'og:image', 'twitter:image'],
|
['thumbnail', 'og:image', 'twitter:image'],
|
||||||
webpage, 'thumbnail', default=None),
|
webpage, 'thumbnail', default=None),
|
||||||
|
@ -194,11 +194,14 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
|
|
||||||
class SangiinInstructionIE(InfoExtractor):
|
class SangiinInstructionIE(InfoExtractor):
|
||||||
_VALID_URL = r'^https?://www\.webtv\.sangiin\.go\.jp/webtv/index\.php'
|
_VALID_URL = r'https?://www\.webtv\.sangiin\.go\.jp/webtv/index\.php'
|
||||||
IE_DESC = False # this shouldn't be listed as a supported site
|
IE_DESC = False # this shouldn't be listed as a supported site
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
raise ExtractorError('Copy the link from the botton below the video description or player, and use the link to download. If there are no button in the frame, get the URL of the frame showing the video.', expected=True)
|
raise ExtractorError(
|
||||||
|
'Copy the link from the button below the video description/player '
|
||||||
|
'and use that link to download. If there is no button in the frame, '
|
||||||
|
'get the URL of the frame showing the video.', expected=True)
|
||||||
|
|
||||||
|
|
||||||
class SangiinIE(InfoExtractor):
|
class SangiinIE(InfoExtractor):
|
||||||
|
@ -22,7 +22,7 @@ class KalturaIE(InfoExtractor):
|
|||||||
(?:
|
(?:
|
||||||
kaltura:(?P<partner_id>\w+):(?P<id>\w+)(?::(?P<player_type>\w+))?|
|
kaltura:(?P<partner_id>\w+):(?P<id>\w+)(?::(?P<player_type>\w+))?|
|
||||||
https?://
|
https?://
|
||||||
(:?(?:www|cdnapi(?:sec)?)\.)?kaltura\.com(?::\d+)?/
|
(?:(?:www|cdnapi(?:sec)?)\.)?kaltura\.com(?::\d+)?/
|
||||||
(?:
|
(?:
|
||||||
(?:
|
(?:
|
||||||
# flash player
|
# flash player
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
class KhanAcademyBaseIE(InfoExtractor):
|
class KhanAcademyBaseIE(InfoExtractor):
|
||||||
_VALID_URL_TEMPL = r'https?://(?:www\.)?khanacademy\.org/(?P<id>(?:[^/]+/){%s}%s[^?#/&]+)'
|
_VALID_URL_TEMPL = r'https?://(?:www\.)?khanacademy\.org/(?P<id>(?:[^/]+/){%s}%s[^?#/&]+)'
|
||||||
|
|
||||||
_PUBLISHED_CONTENT_VERSION = '171419ab20465d931b356f22d20527f13969bb70'
|
_PUBLISHED_CONTENT_VERSION = 'dc34750f0572c80f5effe7134082fe351143c1e4'
|
||||||
|
|
||||||
def _parse_video(self, video):
|
def _parse_video(self, video):
|
||||||
return {
|
return {
|
||||||
@ -39,7 +39,7 @@ def _real_extract(self, url):
|
|||||||
query={
|
query={
|
||||||
'fastly_cacheable': 'persist_until_publish',
|
'fastly_cacheable': 'persist_until_publish',
|
||||||
'pcv': self._PUBLISHED_CONTENT_VERSION,
|
'pcv': self._PUBLISHED_CONTENT_VERSION,
|
||||||
'hash': '1242644265',
|
'hash': '3712657851',
|
||||||
'variables': json.dumps({
|
'variables': json.dumps({
|
||||||
'path': display_id,
|
'path': display_id,
|
||||||
'countryCode': 'US',
|
'countryCode': 'US',
|
||||||
|
@ -67,7 +67,7 @@ class KickIE(KickBaseIE):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def suitable(cls, url):
|
def suitable(cls, url):
|
||||||
return False if KickClipIE.suitable(url) else super().suitable(url)
|
return False if (KickVODIE.suitable(url) or KickClipIE.suitable(url)) else super().suitable(url)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
channel = self._match_id(url)
|
channel = self._match_id(url)
|
||||||
@ -98,25 +98,25 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
class KickVODIE(KickBaseIE):
|
class KickVODIE(KickBaseIE):
|
||||||
IE_NAME = 'kick:vod'
|
IE_NAME = 'kick:vod'
|
||||||
_VALID_URL = r'https?://(?:www\.)?kick\.com/video/(?P<id>[\da-f]{8}-(?:[\da-f]{4}-){3}[\da-f]{12})'
|
_VALID_URL = r'https?://(?:www\.)?kick\.com/[\w-]+/videos/(?P<id>[\da-f]{8}-(?:[\da-f]{4}-){3}[\da-f]{12})'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://kick.com/video/e74614f4-5270-4319-90ad-32179f19a45c',
|
'url': 'https://kick.com/xqc/videos/8dd97a8d-e17f-48fb-8bc3-565f88dbc9ea',
|
||||||
'md5': '3870f94153e40e7121a6e46c068b70cb',
|
'md5': '3870f94153e40e7121a6e46c068b70cb',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'e74614f4-5270-4319-90ad-32179f19a45c',
|
'id': '8dd97a8d-e17f-48fb-8bc3-565f88dbc9ea',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': r're:❎ MEGA DRAMA ❎ LIVE ❎ CLICK ❎ ULTIMATE SKILLS .+',
|
'title': '18+ #ad 🛑LIVE🛑CLICK🛑DRAMA🛑NEWS🛑STUFF🛑REACT🛑GET IN HHERE🛑BOP BOP🛑WEEEE WOOOO🛑',
|
||||||
'description': 'THE BEST AT ABSOLUTELY EVERYTHING. THE JUICER. LEADER OF THE JUICERS.',
|
'description': 'THE BEST AT ABSOLUTELY EVERYTHING. THE JUICER. LEADER OF THE JUICERS.',
|
||||||
'channel': 'xqc',
|
'channel': 'xqc',
|
||||||
'channel_id': '668',
|
'channel_id': '668',
|
||||||
'uploader': 'xQc',
|
'uploader': 'xQc',
|
||||||
'uploader_id': '676',
|
'uploader_id': '676',
|
||||||
'upload_date': '20240724',
|
'upload_date': '20240909',
|
||||||
'timestamp': 1721796562,
|
'timestamp': 1725919141,
|
||||||
'duration': 18566.0,
|
'duration': 10155.0,
|
||||||
'thumbnail': r're:^https?://.*\.jpg',
|
'thumbnail': r're:^https?://.*\.jpg',
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'categories': ['VALORANT'],
|
'categories': ['Just Chatting'],
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
},
|
},
|
||||||
'params': {'skip_download': 'm3u8'},
|
'params': {'skip_download': 'm3u8'},
|
||||||
@ -148,7 +148,7 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
class KickClipIE(KickBaseIE):
|
class KickClipIE(KickBaseIE):
|
||||||
IE_NAME = 'kick:clips'
|
IE_NAME = 'kick:clips'
|
||||||
_VALID_URL = r'https?://(?:www\.)?kick\.com/[\w-]+/?\?(?:[^#]+&)?clip=(?P<id>clip_[\w-]+)'
|
_VALID_URL = r'https?://(?:www\.)?kick\.com/[\w-]+(?:/clips/|/?\?(?:[^#]+&)?clip=)(?P<id>clip_[\w-]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://kick.com/mxddy?clip=clip_01GYXVB5Y8PWAPWCWMSBCFB05X',
|
'url': 'https://kick.com/mxddy?clip=clip_01GYXVB5Y8PWAPWCWMSBCFB05X',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@ -189,6 +189,26 @@ class KickClipIE(KickBaseIE):
|
|||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
},
|
},
|
||||||
'params': {'skip_download': 'm3u8'},
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
}, {
|
||||||
|
'url': 'https://kick.com/spreen/clips/clip_01J8RGZRKHXHXXKJEHGRM932A5',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'clip_01J8RGZRKHXHXXKJEHGRM932A5',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'KLJASLDJKLJKASDLJKDAS',
|
||||||
|
'channel': 'spreen',
|
||||||
|
'channel_id': '5312671',
|
||||||
|
'uploader': 'AnormalBarraBaja',
|
||||||
|
'uploader_id': '26518262',
|
||||||
|
'duration': 43.0,
|
||||||
|
'upload_date': '20240927',
|
||||||
|
'timestamp': 1727399987,
|
||||||
|
'thumbnail': 'https://clips.kick.com/clips/f2/clip_01J8RGZRKHXHXXKJEHGRM932A5/thumbnail.webp',
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'categories': ['Minecraft'],
|
||||||
|
'age_limit': 0,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
126
yt_dlp/extractor/kika.py
Normal file
126
yt_dlp/extractor/kika.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
determine_ext,
|
||||||
|
int_or_none,
|
||||||
|
parse_duration,
|
||||||
|
parse_iso8601,
|
||||||
|
url_or_none,
|
||||||
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
|
class KikaIE(InfoExtractor):
|
||||||
|
IE_DESC = 'KiKA.de'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?kika\.de/[\w/-]+/videos/(?P<id>[a-z-]+\d+)'
|
||||||
|
_GEO_COUNTRIES = ['DE']
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.kika.de/logo/videos/logo-vom-samstag-einunddreissig-august-zweitausendvierundzwanzig-100',
|
||||||
|
'md5': 'fbfc8da483719ef06f396e5e5b938c69',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'logo-vom-samstag-einunddreissig-august-zweitausendvierundzwanzig-100',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'upload_date': '20240831',
|
||||||
|
'timestamp': 1725126600,
|
||||||
|
'season_number': 2024,
|
||||||
|
'modified_date': '20240831',
|
||||||
|
'episode': 'Episode 476',
|
||||||
|
'episode_number': 476,
|
||||||
|
'season': 'Season 2024',
|
||||||
|
'duration': 634,
|
||||||
|
'title': 'logo! vom Samstag, 31. August 2024',
|
||||||
|
'modified_timestamp': 1725129983,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.kika.de/kaltstart/videos/video92498',
|
||||||
|
'md5': '710ece827e5055094afeb474beacb7aa',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'video92498',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '7. Wo ist Leo?',
|
||||||
|
'description': 'md5:fb48396a5b75068bcac1df74f1524920',
|
||||||
|
'duration': 436,
|
||||||
|
'timestamp': 1702926876,
|
||||||
|
'upload_date': '20231218',
|
||||||
|
'episode_number': 7,
|
||||||
|
'modified_date': '20240319',
|
||||||
|
'modified_timestamp': 1710880610,
|
||||||
|
'episode': 'Episode 7',
|
||||||
|
'season_number': 1,
|
||||||
|
'season': 'Season 1',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.kika.de/bernd-das-brot/astrobrot/videos/video90088',
|
||||||
|
'md5': 'ffd1b700d7de0a6616a1d08544c77294',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'video90088',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'upload_date': '20221102',
|
||||||
|
'timestamp': 1667390580,
|
||||||
|
'duration': 197,
|
||||||
|
'modified_timestamp': 1711093771,
|
||||||
|
'episode_number': 8,
|
||||||
|
'title': 'Es ist nicht leicht, ein Astrobrot zu sein',
|
||||||
|
'modified_date': '20240322',
|
||||||
|
'description': 'md5:d3641deaf1b5515a160788b2be4159a9',
|
||||||
|
'season_number': 1,
|
||||||
|
'episode': 'Episode 8',
|
||||||
|
'season': 'Season 1',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
doc = self._download_json(f'https://www.kika.de/_next-api/proxy/v1/videos/{video_id}', video_id)
|
||||||
|
video_assets = self._download_json(doc['assets']['url'], video_id)
|
||||||
|
|
||||||
|
subtitles = {}
|
||||||
|
if ttml_resource := url_or_none(video_assets.get('videoSubtitle')):
|
||||||
|
subtitles['de'] = [{
|
||||||
|
'url': ttml_resource,
|
||||||
|
'ext': 'ttml',
|
||||||
|
}]
|
||||||
|
if webvtt_resource := url_or_none(video_assets.get('webvttUrl')):
|
||||||
|
subtitles.setdefault('de', []).append({
|
||||||
|
'url': webvtt_resource,
|
||||||
|
'ext': 'vtt',
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'formats': list(self._extract_formats(video_assets, video_id)),
|
||||||
|
'subtitles': subtitles,
|
||||||
|
**traverse_obj(doc, {
|
||||||
|
'title': ('title', {str}),
|
||||||
|
'description': ('description', {str}),
|
||||||
|
'timestamp': ('date', {parse_iso8601}),
|
||||||
|
'modified_timestamp': ('modificationDate', {parse_iso8601}),
|
||||||
|
'duration': ((
|
||||||
|
('durationInSeconds', {int_or_none}),
|
||||||
|
('duration', {parse_duration})), any),
|
||||||
|
'episode_number': ('episodeNumber', {int_or_none}),
|
||||||
|
'season_number': ('season', {int_or_none}),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _extract_formats(self, media_info, video_id):
|
||||||
|
for media in traverse_obj(media_info, ('assets', lambda _, v: url_or_none(v['url']))):
|
||||||
|
stream_url = media['url']
|
||||||
|
ext = determine_ext(stream_url)
|
||||||
|
if ext == 'm3u8':
|
||||||
|
yield from self._extract_m3u8_formats(
|
||||||
|
stream_url, video_id, 'mp4', m3u8_id='hls', fatal=False)
|
||||||
|
else:
|
||||||
|
yield {
|
||||||
|
'url': stream_url,
|
||||||
|
'format_id': ext,
|
||||||
|
**traverse_obj(media, {
|
||||||
|
'width': ('frameWidth', {int_or_none}),
|
||||||
|
'height': ('frameHeight', {int_or_none}),
|
||||||
|
# NB: filesize is 0 if unknown, bitrate is -1 if unknown
|
||||||
|
'filesize': ('fileSize', {int_or_none}, {lambda x: x or None}),
|
||||||
|
'abr': ('bitrateAudio', {int_or_none}, {lambda x: None if x == -1 else x}),
|
||||||
|
'vbr': ('bitrateVideo', {int_or_none}, {lambda x: None if x == -1 else x}),
|
||||||
|
}),
|
||||||
|
}
|
@ -1,86 +1,11 @@
|
|||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
clean_html,
|
|
||||||
format_field,
|
format_field,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LnkGoIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?lnk(?:go)?\.(?:alfa\.)?lt/(?:visi-video/[^/]+|video)/(?P<id>[A-Za-z0-9-]+)(?:/(?P<episode_id>\d+))?'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'http://www.lnkgo.lt/visi-video/aktualai-pratesimas/ziurek-putka-trys-klausimai',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '10809',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': "Put'ka: Trys Klausimai",
|
|
||||||
'upload_date': '20161216',
|
|
||||||
'description': 'Seniai matytas Put’ka užduoda tris klausimėlius. Pabandykime surasti atsakymus.',
|
|
||||||
'age_limit': 18,
|
|
||||||
'duration': 117,
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
|
||||||
'timestamp': 1481904000,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True, # HLS download
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'http://lnkgo.alfa.lt/visi-video/aktualai-pratesimas/ziurek-nerdas-taiso-kompiuteri-2',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '10467',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Nėrdas: Kompiuterio Valymas',
|
|
||||||
'upload_date': '20150113',
|
|
||||||
'description': 'md5:7352d113a242a808676ff17e69db6a69',
|
|
||||||
'age_limit': 18,
|
|
||||||
'duration': 346,
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
|
||||||
'timestamp': 1421164800,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True, # HLS download
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://lnk.lt/video/neigalieji-tv-bokste/37413',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
_AGE_LIMITS = {
|
|
||||||
'N-7': 7,
|
|
||||||
'N-14': 14,
|
|
||||||
'S': 18,
|
|
||||||
}
|
|
||||||
_M3U8_TEMPL = 'https://vod.lnk.lt/lnk_vod/lnk/lnk/%s:%s/playlist.m3u8%s'
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
display_id, video_id = self._match_valid_url(url).groups()
|
|
||||||
|
|
||||||
video_info = self._download_json(
|
|
||||||
'https://lnk.lt/api/main/video-page/{}/{}/false'.format(display_id, video_id or '0'),
|
|
||||||
display_id)['videoConfig']['videoInfo']
|
|
||||||
|
|
||||||
video_id = str(video_info['id'])
|
|
||||||
title = video_info['title']
|
|
||||||
prefix = 'smil' if video_info.get('isQualityChangeAvailable') else 'mp4'
|
|
||||||
formats = self._extract_m3u8_formats(
|
|
||||||
self._M3U8_TEMPL % (prefix, video_info['videoUrl'], video_info.get('secureTokenParams') or ''),
|
|
||||||
video_id, 'mp4', 'm3u8_native')
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'display_id': display_id,
|
|
||||||
'title': title,
|
|
||||||
'formats': formats,
|
|
||||||
'thumbnail': format_field(video_info, 'posterImage', 'https://lnk.lt/all-images/%s'),
|
|
||||||
'duration': int_or_none(video_info.get('duration')),
|
|
||||||
'description': clean_html(video_info.get('htmlDescription')),
|
|
||||||
'age_limit': self._AGE_LIMITS.get(video_info.get('pgRating'), 0),
|
|
||||||
'timestamp': parse_iso8601(video_info.get('airDate')),
|
|
||||||
'view_count': int_or_none(video_info.get('viewsCount')),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class LnkIE(InfoExtractor):
|
class LnkIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?lnk\.lt/[^/]+/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?lnk\.lt/[^/]+/(?P<id>\d+)'
|
||||||
|
|
@ -92,9 +92,9 @@ class LoomIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
'params': {'videopassword': 'seniorinfants2'},
|
'params': {'videopassword': 'seniorinfants2'},
|
||||||
}, {
|
}, {
|
||||||
# embed, transcoded-url endpoint sends empty JSON response
|
# embed, transcoded-url endpoint sends empty JSON response, split video and audio HLS formats
|
||||||
'url': 'https://www.loom.com/embed/ddcf1c1ad21f451ea7468b1e33917e4e',
|
'url': 'https://www.loom.com/embed/ddcf1c1ad21f451ea7468b1e33917e4e',
|
||||||
'md5': '8488817242a0db1cb2ad0ea522553cf6',
|
'md5': 'b321d261656848c184a94e3b93eae28d',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'ddcf1c1ad21f451ea7468b1e33917e4e',
|
'id': 'ddcf1c1ad21f451ea7468b1e33917e4e',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@ -104,6 +104,7 @@ class LoomIE(InfoExtractor):
|
|||||||
'timestamp': 1657216459,
|
'timestamp': 1657216459,
|
||||||
'duration': 181,
|
'duration': 181,
|
||||||
},
|
},
|
||||||
|
'params': {'format': 'bestvideo'}, # Test video-only fixup
|
||||||
'expected_warnings': ['Failed to parse JSON'],
|
'expected_warnings': ['Failed to parse JSON'],
|
||||||
}]
|
}]
|
||||||
_WEBPAGE_TESTS = [{
|
_WEBPAGE_TESTS = [{
|
||||||
@ -293,7 +294,11 @@ def get_formats(format_url, format_id, quality):
|
|||||||
format_url = format_url.replace('-split.m3u8', '.m3u8')
|
format_url = format_url.replace('-split.m3u8', '.m3u8')
|
||||||
m3u8_formats = self._extract_m3u8_formats(
|
m3u8_formats = self._extract_m3u8_formats(
|
||||||
format_url, video_id, 'mp4', m3u8_id=f'hls-{format_id}', fatal=False, quality=quality)
|
format_url, video_id, 'mp4', m3u8_id=f'hls-{format_id}', fatal=False, quality=quality)
|
||||||
|
# Sometimes only split video/audio formats are available, need to fixup video-only formats
|
||||||
|
is_not_premerged = 'none' in traverse_obj(m3u8_formats, (..., 'vcodec'))
|
||||||
for fmt in m3u8_formats:
|
for fmt in m3u8_formats:
|
||||||
|
if is_not_premerged and fmt.get('vcodec') != 'none':
|
||||||
|
fmt['acodec'] = 'none'
|
||||||
yield {
|
yield {
|
||||||
**fmt,
|
**fmt,
|
||||||
'url': update_url(fmt['url'], query=query),
|
'url': update_url(fmt['url'], query=query),
|
||||||
|
@ -126,7 +126,7 @@ def _real_extract(self, url):
|
|||||||
video_data = None
|
video_data = None
|
||||||
|
|
||||||
# fix meta_url if missing the host address
|
# fix meta_url if missing the host address
|
||||||
if re.match(r'^\/\+\/', meta_url):
|
if re.match(r'\/\+\/', meta_url):
|
||||||
meta_url = urljoin('https://my.mail.ru', meta_url)
|
meta_url = urljoin('https://my.mail.ru', meta_url)
|
||||||
|
|
||||||
if meta_url:
|
if meta_url:
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
|
|
||||||
|
|
||||||
class MDRIE(InfoExtractor):
|
class MDRIE(InfoExtractor):
|
||||||
IE_DESC = 'MDR.DE and KiKA'
|
IE_DESC = 'MDR.DE'
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?:mdr|kika)\.de/(?:.*)/[a-z-]+-?(?P<id>\d+)(?:_.+?)?\.html'
|
_VALID_URL = r'https?://(?:www\.)?mdr\.de/(?:.*)/[a-z-]+-?(?P<id>\d+)(?:_.+?)?\.html'
|
||||||
|
|
||||||
_GEO_COUNTRIES = ['DE']
|
_GEO_COUNTRIES = ['DE']
|
||||||
|
|
||||||
@ -34,30 +34,6 @@ class MDRIE(InfoExtractor):
|
|||||||
'uploader': 'MITTELDEUTSCHER RUNDFUNK',
|
'uploader': 'MITTELDEUTSCHER RUNDFUNK',
|
||||||
},
|
},
|
||||||
'skip': '404 not found',
|
'skip': '404 not found',
|
||||||
}, {
|
|
||||||
'url': 'http://www.kika.de/baumhaus/videos/video19636.html',
|
|
||||||
'md5': '4930515e36b06c111213e80d1e4aad0e',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '19636',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Baumhaus vom 30. Oktober 2015',
|
|
||||||
'duration': 134,
|
|
||||||
'uploader': 'KIKA',
|
|
||||||
},
|
|
||||||
'skip': '404 not found',
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.kika.de/sendungen/einzelsendungen/weihnachtsprogramm/videos/video8182.html',
|
|
||||||
'md5': '5fe9c4dd7d71e3b238f04b8fdd588357',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '8182',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Beutolomäus und der geheime Weihnachtswunsch',
|
|
||||||
'description': 'md5:b69d32d7b2c55cbe86945ab309d39bbd',
|
|
||||||
'timestamp': 1482541200,
|
|
||||||
'upload_date': '20161224',
|
|
||||||
'duration': 4628,
|
|
||||||
'uploader': 'KIKA',
|
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
# audio with alternative playerURL pattern
|
# audio with alternative playerURL pattern
|
||||||
'url': 'http://www.mdr.de/kultur/videos-und-audios/audio-radio/operation-mindfuck-robert-wilson100.html',
|
'url': 'http://www.mdr.de/kultur/videos-und-audios/audio-radio/operation-mindfuck-robert-wilson100.html',
|
||||||
@ -68,28 +44,7 @@ class MDRIE(InfoExtractor):
|
|||||||
'duration': 3239,
|
'duration': 3239,
|
||||||
'uploader': 'MITTELDEUTSCHER RUNDFUNK',
|
'uploader': 'MITTELDEUTSCHER RUNDFUNK',
|
||||||
},
|
},
|
||||||
}, {
|
'skip': '404 not found',
|
||||||
# empty bitrateVideo and bitrateAudio
|
|
||||||
'url': 'https://www.kika.de/filme/sendung128372_zc-572e3f45_zs-1d9fb70e.html',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '128372',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Der kleine Wichtel kehrt zurück',
|
|
||||||
'description': 'md5:f77fafdff90f7aa1e9dca14f662c052a',
|
|
||||||
'duration': 4876,
|
|
||||||
'timestamp': 1607823300,
|
|
||||||
'upload_date': '20201213',
|
|
||||||
'uploader': 'ZDF',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.kika.de/baumhaus/sendungen/video19636_zc-fea7f8a0_zs-4bf89c60.html',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.kika.de/sendungen/einzelsendungen/weihnachtsprogramm/einzelsendung2534.html',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.mdr.de/mediathek/mdr-videos/a/video-1334.html',
|
'url': 'http://www.mdr.de/mediathek/mdr-videos/a/video-1334.html',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
@ -16,6 +16,15 @@ class MediaKlikkIE(InfoExtractor):
|
|||||||
(?P<id>[^/#?_]+)'''
|
(?P<id>[^/#?_]+)'''
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://mediaklikk.hu/filmajanlo/cikk/az-ajto/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '668177',
|
||||||
|
'title': 'Az ajtó',
|
||||||
|
'display_id': 'az-ajto',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'thumbnail': 'https://cdn.cms.mtv.hu/wp-content/uploads/sites/4/2016/01/vlcsnap-2023-07-31-14h18m52s111.jpg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
# (old) mediaklikk. date in html.
|
# (old) mediaklikk. date in html.
|
||||||
'url': 'https://mediaklikk.hu/video/hazajaro-delnyugat-bacska-a-duna-menten-palankatol-doroszloig/',
|
'url': 'https://mediaklikk.hu/video/hazajaro-delnyugat-bacska-a-duna-menten-palankatol-doroszloig/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@ -37,6 +46,7 @@ class MediaKlikkIE(InfoExtractor):
|
|||||||
'upload_date': '20230903',
|
'upload_date': '20230903',
|
||||||
'thumbnail': 'https://mediaklikk.hu/wp-content/uploads/sites/4/2014/02/hazajarouj_JO.jpg',
|
'thumbnail': 'https://mediaklikk.hu/wp-content/uploads/sites/4/2014/02/hazajarouj_JO.jpg',
|
||||||
},
|
},
|
||||||
|
'skip': 'Webpage redirects to 404 page',
|
||||||
}, {
|
}, {
|
||||||
# (old) m4sport
|
# (old) m4sport
|
||||||
'url': 'https://m4sport.hu/video/2021/08/30/gyemant-liga-parizs/',
|
'url': 'https://m4sport.hu/video/2021/08/30/gyemant-liga-parizs/',
|
||||||
@ -59,6 +69,7 @@ class MediaKlikkIE(InfoExtractor):
|
|||||||
'upload_date': '20230908',
|
'upload_date': '20230908',
|
||||||
'thumbnail': 'https://m4sport.hu/wp-content/uploads/sites/4/2023/09/vlcsnap-2023-09-08-22h43m18s691.jpg',
|
'thumbnail': 'https://m4sport.hu/wp-content/uploads/sites/4/2023/09/vlcsnap-2023-09-08-22h43m18s691.jpg',
|
||||||
},
|
},
|
||||||
|
'skip': 'Webpage redirects to 404 page',
|
||||||
}, {
|
}, {
|
||||||
# m4sport with *video/ url and no date
|
# m4sport with *video/ url and no date
|
||||||
'url': 'https://m4sport.hu/bl-video/real-madrid-chelsea-1-1/',
|
'url': 'https://m4sport.hu/bl-video/real-madrid-chelsea-1-1/',
|
||||||
@ -69,6 +80,7 @@ class MediaKlikkIE(InfoExtractor):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'thumbnail': 'https://m4sport.hu/wp-content/uploads/sites/4/2021/04/Sequence-01.Still001-1024x576.png',
|
'thumbnail': 'https://m4sport.hu/wp-content/uploads/sites/4/2021/04/Sequence-01.Still001-1024x576.png',
|
||||||
},
|
},
|
||||||
|
'skip': 'Webpage redirects to 404 page',
|
||||||
}, {
|
}, {
|
||||||
# (old) hirado
|
# (old) hirado
|
||||||
'url': 'https://hirado.hu/videok/felteteleket-szabott-a-fovaros/',
|
'url': 'https://hirado.hu/videok/felteteleket-szabott-a-fovaros/',
|
||||||
@ -90,6 +102,7 @@ class MediaKlikkIE(InfoExtractor):
|
|||||||
'upload_date': '20230911',
|
'upload_date': '20230911',
|
||||||
'thumbnail': 'https://hirado.hu/wp-content/uploads/sites/4/2023/09/vlcsnap-2023-09-11-09h16m09s882.jpg',
|
'thumbnail': 'https://hirado.hu/wp-content/uploads/sites/4/2023/09/vlcsnap-2023-09-11-09h16m09s882.jpg',
|
||||||
},
|
},
|
||||||
|
'skip': 'Webpage redirects to video list page',
|
||||||
}, {
|
}, {
|
||||||
# (old) petofilive
|
# (old) petofilive
|
||||||
'url': 'https://petofilive.hu/video/2021/06/07/tha-shudras-az-akusztikban/',
|
'url': 'https://petofilive.hu/video/2021/06/07/tha-shudras-az-akusztikban/',
|
||||||
@ -112,6 +125,7 @@ class MediaKlikkIE(InfoExtractor):
|
|||||||
'upload_date': '20230909',
|
'upload_date': '20230909',
|
||||||
'thumbnail': 'https://petofilive.hu/wp-content/uploads/sites/4/2023/09/Clipboard11-2.jpg',
|
'thumbnail': 'https://petofilive.hu/wp-content/uploads/sites/4/2023/09/Clipboard11-2.jpg',
|
||||||
},
|
},
|
||||||
|
'skip': 'Webpage redirects to video list page',
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@ -143,14 +157,14 @@ def _real_extract(self, url):
|
|||||||
if not playlist_url:
|
if not playlist_url:
|
||||||
raise ExtractorError('Unable to extract playlist url')
|
raise ExtractorError('Unable to extract playlist url')
|
||||||
|
|
||||||
formats = self._extract_wowza_formats(
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(playlist_url, video_id)
|
||||||
playlist_url, video_id, skip_protocols=['f4m', 'smil', 'dash'])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
'upload_date': upload_date,
|
'upload_date': upload_date,
|
||||||
'thumbnail': player_data.get('bgImage') or self._og_search_thumbnail(webpage),
|
'thumbnail': player_data.get('bgImage') or self._og_search_thumbnail(webpage),
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
|
|
||||||
class MGTVIE(InfoExtractor):
|
class MGTVIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:w(?:ww)?\.)?mgtv\.com/(v|b)/(?:[^/]+/)*(?P<id>\d+)\.html'
|
_VALID_URL = r'https?://(?:w(?:ww)?\.)?mgtv\.com/[bv]/(?:[^/]+/)*(?P<id>\d+)\.html'
|
||||||
IE_DESC = '芒果TV'
|
IE_DESC = '芒果TV'
|
||||||
IE_NAME = 'MangoTV'
|
IE_NAME = 'MangoTV'
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
class OCWMITIE(InfoExtractor):
|
class OCWMITIE(InfoExtractor):
|
||||||
IE_NAME = 'ocw.mit.edu'
|
IE_NAME = 'ocw.mit.edu'
|
||||||
_VALID_URL = r'^https?://ocw\.mit\.edu/courses/(?P<topic>[a-z0-9\-]+)'
|
_VALID_URL = r'https?://ocw\.mit\.edu/courses/(?P<topic>[a-z0-9\-]+)'
|
||||||
_BASE_URL = 'http://ocw.mit.edu/'
|
_BASE_URL = 'http://ocw.mit.edu/'
|
||||||
|
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
|
121
yt_dlp/extractor/mojevideo.py
Normal file
121
yt_dlp/extractor/mojevideo.py
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import js_to_json, remove_end, update_url_query
|
||||||
|
|
||||||
|
|
||||||
|
class MojevideoIE(InfoExtractor):
|
||||||
|
IE_DESC = 'mojevideo.sk'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?mojevideo\.sk/video/(?P<id>\w+)/(?P<display_id>[\w()]+?)\.html'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.mojevideo.sk/video/3d17c/chlapci_dobetonovali_sme_mame_hotovo.html',
|
||||||
|
'md5': '384a4628bd2bbd261c5206cf77c38c17',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3d17c',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Chlapci dobetónovali sme, máme hotovo!',
|
||||||
|
'display_id': 'chlapci_dobetonovali_sme_mame_hotovo',
|
||||||
|
'description': 'md5:a0822126044050d304a9ef58c92ddb34',
|
||||||
|
'thumbnail': 'https://fs5.mojevideo.sk/imgfb/250236.jpg',
|
||||||
|
'duration': 21.0,
|
||||||
|
'upload_date': '20230919',
|
||||||
|
'timestamp': 1695129706,
|
||||||
|
'like_count': int,
|
||||||
|
'dislike_count': int,
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# 720p
|
||||||
|
'url': 'https://www.mojevideo.sk/video/14677/den_blbec.html',
|
||||||
|
'md5': '517c3e111c53a67d10b429c1f344ba2f',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '14677',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Deň blbec?',
|
||||||
|
'display_id': 'den_blbec',
|
||||||
|
'description': 'I maličkosť vám môže zmeniť celý deň. Nikdy nezahadzujte žuvačky na zem!',
|
||||||
|
'thumbnail': 'https://fs5.mojevideo.sk/imgfb/83575.jpg',
|
||||||
|
'duration': 100.0,
|
||||||
|
'upload_date': '20120515',
|
||||||
|
'timestamp': 1337076481,
|
||||||
|
'like_count': int,
|
||||||
|
'dislike_count': int,
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# 1080p
|
||||||
|
'url': 'https://www.mojevideo.sk/video/2feb2/band_maid_onset_(instrumental)_live_zepp_tokyo_(full_hd).html',
|
||||||
|
'md5': '64599a23d3ac31cf2fe069e4353d8162',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2feb2',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'BAND-MAID - onset (Instrumental) Live - Zepp Tokyo (Full HD)',
|
||||||
|
'display_id': 'band_maid_onset_(instrumental)_live_zepp_tokyo_(full_hd)',
|
||||||
|
'description': 'Výborná inštrumentálna skladba od skupiny BAND-MAID.',
|
||||||
|
'thumbnail': 'https://fs5.mojevideo.sk/imgfb/196274.jpg',
|
||||||
|
'duration': 240.0,
|
||||||
|
'upload_date': '20190708',
|
||||||
|
'timestamp': 1562576592,
|
||||||
|
'like_count': int,
|
||||||
|
'dislike_count': int,
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# 720p
|
||||||
|
'url': 'https://www.mojevideo.sk/video/358c8/dva_nissany_skyline_strielaju_v_londyne.html',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# 720p
|
||||||
|
'url': 'https://www.mojevideo.sk/video/2455d/gopro_hero4_session_nova_sportova_vodotesna_kamera.html',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# 1080p
|
||||||
|
'url': 'https://www.mojevideo.sk/video/352ee/amd_rx_6800_xt_vs_nvidia_rtx_3080_(test_v_9_hrach).html',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# 1080p
|
||||||
|
'url': 'https://www.mojevideo.sk/video/2cbeb/trailer_z_avengers_infinity_war.html',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id, display_id = self._match_valid_url(url).groups()
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
video_id_dec = self._search_regex(
|
||||||
|
r'\bvId\s*=\s*(\d+)', webpage, 'video id', fatal=False) or str(int(video_id, 16))
|
||||||
|
video_exp = self._search_regex(r'\bvEx\s*=\s*["\'](\d+)', webpage, 'video expiry')
|
||||||
|
video_hashes = self._search_json(
|
||||||
|
r'\bvHash\s*=', webpage, 'video hashes', video_id,
|
||||||
|
contains_pattern=r'\[(?s:.+)\]', transform_source=js_to_json)
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for video_hash, (suffix, quality, format_note) in zip(video_hashes, [
|
||||||
|
('', 1, 'normálna kvalita'),
|
||||||
|
('_lq', 0, 'nízka kvalita'),
|
||||||
|
('_hd', 2, 'HD-720p'),
|
||||||
|
('_fhd', 3, 'FULL HD-1080p'),
|
||||||
|
('_2k', 4, '2K-1440p'),
|
||||||
|
]):
|
||||||
|
formats.append({
|
||||||
|
'format_id': f'mp4-{quality}',
|
||||||
|
'quality': quality,
|
||||||
|
'format_note': format_note,
|
||||||
|
'url': update_url_query(
|
||||||
|
f'https://cache01.mojevideo.sk/securevideos69/{video_id_dec}{suffix}.mp4', {
|
||||||
|
'md5': video_hash,
|
||||||
|
'expires': video_exp,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
'formats': formats,
|
||||||
|
'title': (self._og_search_title(webpage, default=None)
|
||||||
|
or remove_end(self._html_extract_title(webpage, 'title'), ' - Mojevideo')),
|
||||||
|
'description': self._og_search_description(webpage),
|
||||||
|
**self._search_json_ld(webpage, video_id, default={}),
|
||||||
|
}
|
@ -420,7 +420,7 @@ def _yield_dms_formats(self, api_data, video_id):
|
|||||||
'x-request-with': 'https://www.nicovideo.jp',
|
'x-request-with': 'https://www.nicovideo.jp',
|
||||||
})['data']['contentUrl']
|
})['data']['contentUrl']
|
||||||
# Getting all audio formats results in duplicate video formats which we filter out later
|
# Getting all audio formats results in duplicate video formats which we filter out later
|
||||||
dms_fmts = self._extract_m3u8_formats(dms_m3u8_url, video_id)
|
dms_fmts = self._extract_m3u8_formats(dms_m3u8_url, video_id, 'mp4')
|
||||||
|
|
||||||
# m3u8 extraction does not provide audio bitrates, so extract from the API data and fix
|
# m3u8 extraction does not provide audio bitrates, so extract from the API data and fix
|
||||||
for audio_fmt in traverse_obj(dms_fmts, lambda _, v: v['vcodec'] == 'none'):
|
for audio_fmt in traverse_obj(dms_fmts, lambda _, v: v['vcodec'] == 'none'):
|
||||||
@ -432,7 +432,6 @@ def _yield_dms_formats(self, api_data, video_id):
|
|||||||
'asr': ('samplingRate', {int_or_none}),
|
'asr': ('samplingRate', {int_or_none}),
|
||||||
}), get_all=False),
|
}), get_all=False),
|
||||||
'acodec': 'aac',
|
'acodec': 'aac',
|
||||||
'ext': 'm4a',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Sort before removing dupes to keep the format dicts with the lowest tbr
|
# Sort before removing dupes to keep the format dicts with the lowest tbr
|
||||||
|
@ -43,14 +43,8 @@ def _real_extract(self, url):
|
|||||||
def build_url(url_or_path):
|
def build_url(url_or_path):
|
||||||
return urljoin('https://adult.noodlemagazine.com', url_or_path)
|
return urljoin('https://adult.noodlemagazine.com', url_or_path)
|
||||||
|
|
||||||
headers = {'Referer': url}
|
playlist_info = self._search_json(
|
||||||
player_path = self._html_search_regex(
|
r'window\.playlist\s*=', webpage, video_id, 'playlist info')
|
||||||
r'<iframe[^>]+\bid="iplayer"[^>]+\bsrc="([^"]+)"', webpage, 'player path')
|
|
||||||
player_iframe = self._download_webpage(
|
|
||||||
build_url(player_path), video_id, 'Downloading iframe page', headers=headers)
|
|
||||||
playlist_url = self._search_regex(
|
|
||||||
r'window\.playlistUrl\s*=\s*["\']([^"\']+)["\']', player_iframe, 'playlist url')
|
|
||||||
playlist_info = self._download_json(build_url(playlist_url), video_id, headers=headers)
|
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for source in traverse_obj(playlist_info, ('sources', lambda _, v: v['file'])):
|
for source in traverse_obj(playlist_info, ('sources', lambda _, v: v['file'])):
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
|
|
||||||
class NZOnScreenIE(InfoExtractor):
|
class NZOnScreenIE(InfoExtractor):
|
||||||
_VALID_URL = r'^https?://www\.nzonscreen\.com/title/(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://www\.nzonscreen\.com/title/(?P<id>[^/?#]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.nzonscreen.com/title/shoop-shoop-diddy-wop-cumma-cumma-wang-dang-1982',
|
'url': 'https://www.nzonscreen.com/title/shoop-shoop-diddy-wop-cumma-cumma-wang-dang-1982',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
|
||||||
extract_attributes,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class NZZIE(InfoExtractor):
|
class NZZIE(InfoExtractor):
|
||||||
@ -22,19 +19,14 @@ class NZZIE(InfoExtractor):
|
|||||||
'playlist_count': 1,
|
'playlist_count': 1,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
def _entries(self, webpage, page_id):
|
||||||
|
for script in re.findall(r'(?s)<script[^>]* data-hid="jw-video-jw[^>]+>(.+?)</script>', webpage):
|
||||||
|
settings = self._search_json(r'var\s+settings\s*=[^{]*', script, 'settings', page_id, fatal=False)
|
||||||
|
if entry := self._parse_jwplayer_data(settings, page_id):
|
||||||
|
yield entry
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
page_id = self._match_id(url)
|
page_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, page_id)
|
webpage = self._download_webpage(url, page_id)
|
||||||
|
|
||||||
entries = []
|
return self.playlist_result(self._entries(webpage, page_id), page_id)
|
||||||
for player_element in re.findall(
|
|
||||||
r'(<[^>]+class="kalturaPlayer[^"]*"[^>]*>)', webpage):
|
|
||||||
player_params = extract_attributes(player_element)
|
|
||||||
if player_params.get('data-type') not in ('kaltura_singleArticle',):
|
|
||||||
self.report_warning('Unsupported player type')
|
|
||||||
continue
|
|
||||||
entry_id = player_params['data-id']
|
|
||||||
entries.append(self.url_result(
|
|
||||||
'kaltura:1750922:' + entry_id, 'Kaltura', entry_id))
|
|
||||||
|
|
||||||
return self.playlist_result(entries, page_id)
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
@ -22,13 +23,19 @@
|
|||||||
|
|
||||||
|
|
||||||
class PatreonBaseIE(InfoExtractor):
|
class PatreonBaseIE(InfoExtractor):
|
||||||
USER_AGENT = 'Patreon/7.6.28 (Android; Android 11; Scale/2.10)'
|
@functools.cached_property
|
||||||
|
def patreon_user_agent(self):
|
||||||
|
# Patreon mobile UA is needed to avoid triggering Cloudflare anti-bot protection.
|
||||||
|
# Newer UA yields higher res m3u8 formats for locked posts, but gives 401 if not logged-in
|
||||||
|
if self._get_cookies('https://www.patreon.com/').get('session_id'):
|
||||||
|
return 'Patreon/72.2.28 (Android; Android 14; Scale/2.10)'
|
||||||
|
return 'Patreon/7.6.28 (Android; Android 11; Scale/2.10)'
|
||||||
|
|
||||||
def _call_api(self, ep, item_id, query=None, headers=None, fatal=True, note=None):
|
def _call_api(self, ep, item_id, query=None, headers=None, fatal=True, note=None):
|
||||||
if headers is None:
|
if headers is None:
|
||||||
headers = {}
|
headers = {}
|
||||||
if 'User-Agent' not in headers:
|
if 'User-Agent' not in headers:
|
||||||
headers['User-Agent'] = self.USER_AGENT
|
headers['User-Agent'] = self.patreon_user_agent
|
||||||
if query:
|
if query:
|
||||||
query.update({'json-api-version': 1.0})
|
query.update({'json-api-version': 1.0})
|
||||||
|
|
||||||
@ -48,6 +55,7 @@ def _call_api(self, ep, item_id, query=None, headers=None, fatal=True, note=None
|
|||||||
|
|
||||||
|
|
||||||
class PatreonIE(PatreonBaseIE):
|
class PatreonIE(PatreonBaseIE):
|
||||||
|
IE_NAME = 'patreon'
|
||||||
_VALID_URL = r'https?://(?:www\.)?patreon\.com/(?:creation\?hid=|posts/(?:[\w-]+-)?)(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?patreon\.com/(?:creation\?hid=|posts/(?:[\w-]+-)?)(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.patreon.com/creation?hid=743933',
|
'url': 'http://www.patreon.com/creation?hid=743933',
|
||||||
@ -111,6 +119,7 @@ class PatreonIE(PatreonBaseIE):
|
|||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'channel_is_verified': True,
|
'channel_is_verified': True,
|
||||||
'chapters': 'count:4',
|
'chapters': 'count:4',
|
||||||
|
'timestamp': 1423689666,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'noplaylist': True,
|
'noplaylist': True,
|
||||||
@ -221,6 +230,7 @@ class PatreonIE(PatreonBaseIE):
|
|||||||
'thumbnail': r're:^https?://.+',
|
'thumbnail': r're:^https?://.+',
|
||||||
},
|
},
|
||||||
'params': {'skip_download': 'm3u8'},
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
}, {
|
}, {
|
||||||
# multiple attachments/embeds
|
# multiple attachments/embeds
|
||||||
'url': 'https://www.patreon.com/posts/holy-wars-solos-100601977',
|
'url': 'https://www.patreon.com/posts/holy-wars-solos-100601977',
|
||||||
@ -326,8 +336,13 @@ def _real_extract(self, url):
|
|||||||
if embed_url and (urlh := self._request_webpage(
|
if embed_url and (urlh := self._request_webpage(
|
||||||
embed_url, video_id, 'Checking embed URL', headers=headers,
|
embed_url, video_id, 'Checking embed URL', headers=headers,
|
||||||
fatal=False, errnote=False, expected_status=403)):
|
fatal=False, errnote=False, expected_status=403)):
|
||||||
|
# Vimeo's Cloudflare anti-bot protection will return HTTP status 200 for 404, so we need
|
||||||
|
# to check for "Sorry, we couldn&rsquo;t find that page" in the meta description tag
|
||||||
|
meta_description = clean_html(self._html_search_meta(
|
||||||
|
'description', self._webpage_read_content(urlh, embed_url, video_id, fatal=False), default=None))
|
||||||
# Password-protected vids.io embeds return 403 errors w/o --video-password or session cookie
|
# Password-protected vids.io embeds return 403 errors w/o --video-password or session cookie
|
||||||
if urlh.status != 403 or VidsIoIE.suitable(embed_url):
|
if ((urlh.status != 403 and meta_description != 'Sorry, we couldn’t find that page')
|
||||||
|
or VidsIoIE.suitable(embed_url)):
|
||||||
entries.append(self.url_result(smuggle_url(embed_url, headers)))
|
entries.append(self.url_result(smuggle_url(embed_url, headers)))
|
||||||
|
|
||||||
post_file = traverse_obj(attributes, ('post_file', {dict}))
|
post_file = traverse_obj(attributes, ('post_file', {dict}))
|
||||||
@ -419,15 +434,19 @@ def _get_comments(self, post_id):
|
|||||||
|
|
||||||
|
|
||||||
class PatreonCampaignIE(PatreonBaseIE):
|
class PatreonCampaignIE(PatreonBaseIE):
|
||||||
|
IE_NAME = 'patreon:campaign'
|
||||||
_VALID_URL = r'https?://(?:www\.)?patreon\.com/(?!rss)(?:(?:m|api/campaigns)/(?P<campaign_id>\d+)|(?P<vanity>[-\w]+))'
|
_VALID_URL = r'''(?x)
|
||||||
|
https?://(?:www\.)?patreon\.com/(?:
|
||||||
|
(?:m|api/campaigns)/(?P<campaign_id>\d+)|
|
||||||
|
(?P<vanity>(?!creation[?/]|posts/|rss[?/])[\w-]+)
|
||||||
|
)(?:/posts)?/?(?:$|[?#])'''
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.patreon.com/dissonancepod/',
|
'url': 'https://www.patreon.com/dissonancepod/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'title': 'Cognitive Dissonance Podcast',
|
'title': 'Cognitive Dissonance Podcast',
|
||||||
'channel_url': 'https://www.patreon.com/dissonancepod',
|
'channel_url': 'https://www.patreon.com/dissonancepod',
|
||||||
'id': '80642',
|
'id': '80642',
|
||||||
'description': 'md5:eb2fa8b83da7ab887adeac34da6b7af7',
|
'description': r're:(?s).*We produce a weekly news podcast focusing on stories that deal with skepticism and religion.*',
|
||||||
'channel_id': '80642',
|
'channel_id': '80642',
|
||||||
'channel': 'Cognitive Dissonance Podcast',
|
'channel': 'Cognitive Dissonance Podcast',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
@ -445,7 +464,7 @@ class PatreonCampaignIE(PatreonBaseIE):
|
|||||||
'id': '4767637',
|
'id': '4767637',
|
||||||
'channel_id': '4767637',
|
'channel_id': '4767637',
|
||||||
'channel_url': 'https://www.patreon.com/notjustbikes',
|
'channel_url': 'https://www.patreon.com/notjustbikes',
|
||||||
'description': 'md5:9f4b70051216c4d5c58afe580ffc8d0f',
|
'description': r're:(?s).*Not Just Bikes started as a way to explain why we chose to live in the Netherlands.*',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
'channel': 'Not Just Bikes',
|
'channel': 'Not Just Bikes',
|
||||||
'uploader_url': 'https://www.patreon.com/notjustbikes',
|
'uploader_url': 'https://www.patreon.com/notjustbikes',
|
||||||
@ -462,7 +481,7 @@ class PatreonCampaignIE(PatreonBaseIE):
|
|||||||
'id': '4243769',
|
'id': '4243769',
|
||||||
'channel_id': '4243769',
|
'channel_id': '4243769',
|
||||||
'channel_url': 'https://www.patreon.com/secondthought',
|
'channel_url': 'https://www.patreon.com/secondthought',
|
||||||
'description': 'md5:69c89a3aba43efdb76e85eb023e8de8b',
|
'description': r're:(?s).*Second Thought is an educational YouTube channel.*',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
'channel': 'Second Thought',
|
'channel': 'Second Thought',
|
||||||
'uploader_url': 'https://www.patreon.com/secondthought',
|
'uploader_url': 'https://www.patreon.com/secondthought',
|
||||||
@ -482,10 +501,6 @@ class PatreonCampaignIE(PatreonBaseIE):
|
|||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def suitable(cls, url):
|
|
||||||
return False if PatreonIE.suitable(url) else super().suitable(url)
|
|
||||||
|
|
||||||
def _entries(self, campaign_id):
|
def _entries(self, campaign_id):
|
||||||
cursor = None
|
cursor = None
|
||||||
params = {
|
params = {
|
||||||
@ -512,7 +527,7 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
campaign_id, vanity = self._match_valid_url(url).group('campaign_id', 'vanity')
|
campaign_id, vanity = self._match_valid_url(url).group('campaign_id', 'vanity')
|
||||||
if campaign_id is None:
|
if campaign_id is None:
|
||||||
webpage = self._download_webpage(url, vanity, headers={'User-Agent': self.USER_AGENT})
|
webpage = self._download_webpage(url, vanity, headers={'User-Agent': self.patreon_user_agent})
|
||||||
campaign_id = self._search_nextjs_data(
|
campaign_id = self._search_nextjs_data(
|
||||||
webpage, vanity)['props']['pageProps']['bootstrapEnvelope']['pageBootstrap']['campaign']['data']['id']
|
webpage, vanity)['props']['pageProps']['bootstrapEnvelope']['pageBootstrap']['campaign']['data']['id']
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ def _extract_video(self, data, extract_formats=True):
|
|||||||
|
|
||||||
|
|
||||||
class PinterestIE(PinterestBaseIE):
|
class PinterestIE(PinterestBaseIE):
|
||||||
_VALID_URL = rf'{PinterestBaseIE._VALID_URL_BASE}/pin/(?P<id>\d+)'
|
_VALID_URL = rf'{PinterestBaseIE._VALID_URL_BASE}/pin/(?:[\w-]+--)?(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# formats found in data['videos']
|
# formats found in data['videos']
|
||||||
'url': 'https://www.pinterest.com/pin/664281013778109217/',
|
'url': 'https://www.pinterest.com/pin/664281013778109217/',
|
||||||
@ -174,6 +174,25 @@ class PinterestIE(PinterestBaseIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://co.pinterest.com/pin/824721750502199491/',
|
'url': 'https://co.pinterest.com/pin/824721750502199491/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://pinterest.com/pin/dive-into-serenity-blue-lagoon-pedi-nails-for-a-tranquil-and-refreshing-spa-experience-video-in-2024--2885187256207927',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2885187256207927',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Dive into Serenity: Blue Lagoon Pedi Nails for a Tranquil and Refreshing Spa Experience! 💙💅',
|
||||||
|
'description': 'md5:5da41c767d2317e42e49b663b0b2150f',
|
||||||
|
'uploader': 'Glamour Artistry |Everyday Outfits, Luxury Fashion & Nail Designs',
|
||||||
|
'uploader_id': '1142999717836434688',
|
||||||
|
'upload_date': '20240702',
|
||||||
|
'timestamp': 1719939156,
|
||||||
|
'duration': 7.967,
|
||||||
|
'comment_count': int,
|
||||||
|
'repost_count': int,
|
||||||
|
'categories': 'count:9',
|
||||||
|
'tags': ['#BlueLagoonPediNails', '#SpaExperience'],
|
||||||
|
'thumbnail': r're:^https?://.*\.(?:jpg|png)$',
|
||||||
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@ -628,8 +628,7 @@ def is_404(e):
|
|||||||
page_entries = self._extract_entries(webpage, host)
|
page_entries = self._extract_entries(webpage, host)
|
||||||
if not page_entries:
|
if not page_entries:
|
||||||
break
|
break
|
||||||
for e in page_entries:
|
yield from page_entries
|
||||||
yield e
|
|
||||||
if not self._has_more(webpage):
|
if not self._has_more(webpage):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
|
|
||||||
class RadioFranceIE(InfoExtractor):
|
class RadioFranceIE(InfoExtractor):
|
||||||
_VALID_URL = r'^https?://maison\.radiofrance\.fr/radiovisions/(?P<id>[^?#]+)'
|
_VALID_URL = r'https?://maison\.radiofrance\.fr/radiovisions/(?P<id>[^?#]+)'
|
||||||
IE_NAME = 'radiofrance'
|
IE_NAME = 'radiofrance'
|
||||||
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
@ -17,7 +18,7 @@
|
|||||||
|
|
||||||
class RedditIE(InfoExtractor):
|
class RedditIE(InfoExtractor):
|
||||||
_NETRC_MACHINE = 'reddit'
|
_NETRC_MACHINE = 'reddit'
|
||||||
_VALID_URL = r'https?://(?P<host>(?:\w+\.)?reddit(?:media)?\.com)/(?P<slug>(?:(?:r|user)/[^/]+/)?comments/(?P<id>[^/?#&]+))'
|
_VALID_URL = r'https?://(?:\w+\.)?reddit(?:media)?\.com/(?P<slug>(?:(?:r|user)/[^/]+/)?comments/(?P<id>[^/?#&]+))'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.reddit.com/r/videos/comments/6rrwyj/that_small_heart_attack/',
|
'url': 'https://www.reddit.com/r/videos/comments/6rrwyj/that_small_heart_attack/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@ -251,15 +252,15 @@ def _get_subtitles(self, video_id):
|
|||||||
return {'en': [{'url': caption_url}]}
|
return {'en': [{'url': caption_url}]}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
host, slug, video_id = self._match_valid_url(url).group('host', 'slug', 'id')
|
slug, video_id = self._match_valid_url(url).group('slug', 'id')
|
||||||
|
|
||||||
data = self._download_json(
|
try:
|
||||||
f'https://{host}/{slug}/.json', video_id, fatal=False, expected_status=403)
|
|
||||||
if not data:
|
|
||||||
fallback_host = 'old.reddit.com' if host != 'old.reddit.com' else 'www.reddit.com'
|
|
||||||
self.to_screen(f'{host} request failed, retrying with {fallback_host}')
|
|
||||||
data = self._download_json(
|
data = self._download_json(
|
||||||
f'https://{fallback_host}/{slug}/.json', video_id, expected_status=403)
|
f'https://www.reddit.com/{slug}/.json', video_id, expected_status=403)
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, json.JSONDecodeError):
|
||||||
|
self.raise_login_required('Account authentication is required')
|
||||||
|
raise
|
||||||
|
|
||||||
if traverse_obj(data, 'error') == 403:
|
if traverse_obj(data, 'error') == 403:
|
||||||
reason = data.get('reason')
|
reason = data.get('reason')
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
|
|
||||||
class ReverbNationIE(InfoExtractor):
|
class ReverbNationIE(InfoExtractor):
|
||||||
_VALID_URL = r'^https?://(?:www\.)?reverbnation\.com/.*?/song/(?P<id>\d+).*?$'
|
_VALID_URL = r'https?://(?:www\.)?reverbnation\.com/.*?/song/(?P<id>\d+).*?$'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.reverbnation.com/alkilados/song/16965047-mona-lisa',
|
'url': 'http://www.reverbnation.com/alkilados/song/16965047-mona-lisa',
|
||||||
'md5': 'c0aaf339bcee189495fdf5a8c8ba8645',
|
'md5': 'c0aaf339bcee189495fdf5a8c8ba8645',
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
|
|
||||||
class RTPIE(InfoExtractor):
|
class RTPIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?rtp\.pt/play/p(?P<program_id>[0-9]+)/(?P<id>[^/?#]+)/?'
|
_VALID_URL = r'https?://(?:www\.)?rtp\.pt/play/(?:(?:estudoemcasa|palco|zigzag)/)?p(?P<program_id>[0-9]+)/(?P<id>[^/?#]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.rtp.pt/play/p405/e174042/paixoes-cruzadas',
|
'url': 'http://www.rtp.pt/play/p405/e174042/paixoes-cruzadas',
|
||||||
'md5': 'e736ce0c665e459ddb818546220b4ef8',
|
'md5': 'e736ce0c665e459ddb818546220b4ef8',
|
||||||
@ -19,9 +19,25 @@ class RTPIE(InfoExtractor):
|
|||||||
'description': 'As paixões musicais de António Cartaxo e António Macedo',
|
'description': 'As paixões musicais de António Cartaxo e António Macedo',
|
||||||
'thumbnail': r're:^https?://.*\.jpg',
|
'thumbnail': r're:^https?://.*\.jpg',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.rtp.pt/play/zigzag/p13166/e757904/25-curiosidades-25-de-abril',
|
||||||
|
'md5': '9a81ed53f2b2197cfa7ed455b12f8ade',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'e757904',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '25 Curiosidades, 25 de Abril',
|
||||||
|
'description': 'Estudar ou não estudar - Em cada um dos episódios descobrimos uma curiosidade acerca de como era viver em Portugal antes da revolução do 25 de abr',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg',
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.rtp.pt/play/p831/a-quimica-das-coisas',
|
'url': 'http://www.rtp.pt/play/p831/a-quimica-das-coisas',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.rtp.pt/play/estudoemcasa/p7776/portugues-1-ano',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.rtp.pt/play/palco/p13785/l7nnon',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_RX_OBFUSCATION = re.compile(r'''(?xs)
|
_RX_OBFUSCATION = re.compile(r'''(?xs)
|
||||||
@ -49,17 +65,17 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
f, config = self._search_regex(
|
f, config = self._search_regex(
|
||||||
r'''(?sx)
|
r'''(?sx)
|
||||||
var\s+f\s*=\s*(?P<f>".*?"|{[^;]+?});\s*
|
(?:var\s+f\s*=\s*(?P<f>".*?"|{[^;]+?});\s*)?
|
||||||
var\s+player1\s+=\s+new\s+RTPPlayer\s*\((?P<config>{(?:(?!\*/).)+?})\);(?!\s*\*/)
|
var\s+player1\s+=\s+new\s+RTPPlayer\s*\((?P<config>{(?:(?!\*/).)+?})\);(?!\s*\*/)
|
||||||
''', webpage,
|
''', webpage,
|
||||||
'player config', group=('f', 'config'))
|
'player config', group=('f', 'config'))
|
||||||
|
|
||||||
f = self._parse_json(
|
|
||||||
f, video_id,
|
|
||||||
lambda data: self.__unobfuscate(data, video_id=video_id))
|
|
||||||
config = self._parse_json(
|
config = self._parse_json(
|
||||||
config, video_id,
|
config, video_id,
|
||||||
lambda data: self.__unobfuscate(data, video_id=video_id))
|
lambda data: self.__unobfuscate(data, video_id=video_id))
|
||||||
|
f = config['file'] if not f else self._parse_json(
|
||||||
|
f, video_id,
|
||||||
|
lambda data: self.__unobfuscate(data, video_id=video_id))
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
if isinstance(f, dict):
|
if isinstance(f, dict):
|
||||||
|
@ -8,14 +8,17 @@
|
|||||||
UnsupportedError,
|
UnsupportedError,
|
||||||
clean_html,
|
clean_html,
|
||||||
determine_ext,
|
determine_ext,
|
||||||
|
extract_attributes,
|
||||||
format_field,
|
format_field,
|
||||||
get_element_by_class,
|
get_element_by_class,
|
||||||
|
get_elements_html_by_class,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
join_nonempty,
|
join_nonempty,
|
||||||
parse_count,
|
parse_count,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
|
urljoin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -382,8 +385,10 @@ def entries(self, url, playlist_id):
|
|||||||
if isinstance(e.cause, HTTPError) and e.cause.status == 404:
|
if isinstance(e.cause, HTTPError) and e.cause.status == 404:
|
||||||
break
|
break
|
||||||
raise
|
raise
|
||||||
for video_url in re.findall(r'class="[^>"]*videostream__link[^>]+href="([^"]+\.html)"', webpage):
|
for video_url in traverse_obj(
|
||||||
yield self.url_result('https://rumble.com' + video_url)
|
get_elements_html_by_class('videostream__link', webpage), (..., {extract_attributes}, 'href'),
|
||||||
|
):
|
||||||
|
yield self.url_result(urljoin('https://rumble.com', video_url))
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
url, playlist_id = self._match_valid_url(url).groups()
|
url, playlist_id = self._match_valid_url(url).groups()
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
determine_ext,
|
determine_ext,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_qs,
|
parse_qs,
|
||||||
|
traverse_obj,
|
||||||
try_get,
|
try_get,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
@ -80,6 +81,8 @@ def _extract_formats(self, options, video_id):
|
|||||||
'url': format_url,
|
'url': format_url,
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
})
|
})
|
||||||
|
for hls_url in traverse_obj(options, ('live_streams', 'hls', ..., 'url', {url_or_none})):
|
||||||
|
formats.extend(self._extract_m3u8_formats(hls_url, video_id, ext='mp4', fatal=False))
|
||||||
return formats
|
return formats
|
||||||
|
|
||||||
def _download_and_extract_formats(self, video_id, query=None):
|
def _download_and_extract_formats(self, video_id, query=None):
|
||||||
@ -90,7 +93,7 @@ def _download_and_extract_formats(self, video_id, query=None):
|
|||||||
class RutubeIE(RutubeBaseIE):
|
class RutubeIE(RutubeBaseIE):
|
||||||
IE_NAME = 'rutube'
|
IE_NAME = 'rutube'
|
||||||
IE_DESC = 'Rutube videos'
|
IE_DESC = 'Rutube videos'
|
||||||
_VALID_URL = r'https?://rutube\.ru/(?:video(?:/private)?|(?:play/)?embed)/(?P<id>[\da-z]{32})'
|
_VALID_URL = r'https?://rutube\.ru/(?:(?:live/)?video(?:/private)?|(?:play/)?embed)/(?P<id>[\da-z]{32})'
|
||||||
_EMBED_REGEX = [r'<iframe[^>]+?src=(["\'])(?P<url>(?:https?:)?//rutube\.ru/(?:play/)?embed/[\da-z]{32}.*?)\1']
|
_EMBED_REGEX = [r'<iframe[^>]+?src=(["\'])(?P<url>(?:https?:)?//rutube\.ru/(?:play/)?embed/[\da-z]{32}.*?)\1']
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
@ -164,6 +167,29 @@ class RutubeIE(RutubeBaseIE):
|
|||||||
'uploader': 'Стас Быков',
|
'uploader': 'Стас Быков',
|
||||||
},
|
},
|
||||||
'expected_warnings': ['Unable to download f4m'],
|
'expected_warnings': ['Unable to download f4m'],
|
||||||
|
}, {
|
||||||
|
'url': 'https://rutube.ru/live/video/c58f502c7bb34a8fcdd976b221fca292/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'c58f502c7bb34a8fcdd976b221fca292',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'categories': ['Телепередачи'],
|
||||||
|
'description': '',
|
||||||
|
'thumbnail': 'http://pic.rutubelist.ru/video/14/19/14190807c0c48b40361aca93ad0867c7.jpg',
|
||||||
|
'live_status': 'is_live',
|
||||||
|
'age_limit': 0,
|
||||||
|
'uploader_id': '23460655',
|
||||||
|
'timestamp': 1652972968,
|
||||||
|
'view_count': int,
|
||||||
|
'upload_date': '20220519',
|
||||||
|
'title': r're:Первый канал. Прямой эфир \d{4}-\d{2}-\d{2} \d{2}:\d{2}$',
|
||||||
|
'uploader': 'Первый канал',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://rutube.ru/video/5ab908fccfac5bb43ef2b1e4182256b0/',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://rutube.ru/live/video/private/c58f502c7bb34a8fcdd976b221fca292/',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -36,7 +36,7 @@ class SampleFocusIE(InfoExtractor):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id, impersonate=True)
|
||||||
|
|
||||||
sample_id = self._search_regex(
|
sample_id = self._search_regex(
|
||||||
r'<input[^>]+id=(["\'])sample_id\1[^>]+value=(?:["\'])(?P<id>\d+)',
|
r'<input[^>]+id=(["\'])sample_id\1[^>]+value=(?:["\'])(?P<id>\d+)',
|
||||||
@ -82,7 +82,15 @@ def extract_count(klass):
|
|||||||
return {
|
return {
|
||||||
'id': sample_id,
|
'id': sample_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'url': mp3_url,
|
'formats': [{
|
||||||
|
'url': mp3_url,
|
||||||
|
'ext': 'mp3',
|
||||||
|
'vcodec': 'none',
|
||||||
|
'acodec': 'mp3',
|
||||||
|
'http_headers': {
|
||||||
|
'Referer': url,
|
||||||
|
},
|
||||||
|
}],
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
'uploader': uploader,
|
'uploader': uploader,
|
||||||
|
33
yt_dlp/extractor/screenrec.py
Normal file
33
yt_dlp/extractor/screenrec.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class ScreenRecIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?screenrec\.com/share/(?P<id>\w{10})'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://screenrec.com/share/DasLtbknYo',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'DasLtbknYo',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '02.05.2024_03.01.25_REC',
|
||||||
|
'description': 'Recorded with ScreenRec',
|
||||||
|
'thumbnail': r're:^https?://.*\.gif$',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
m3u8_url = self._search_regex(
|
||||||
|
r'customUrl\s*:\s*(["\'])(?P<url>(?:(?!\1).)+)\1', webpage, 'm3u8 URL', group='url')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': self._og_search_title(webpage, default=None) or self._html_extract_title(webpage),
|
||||||
|
'description': self._og_search_description(webpage),
|
||||||
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
|
'formats': self._extract_m3u8_formats(m3u8_url, video_id, ext='mp4'),
|
||||||
|
}
|
36
yt_dlp/extractor/sen.py
Normal file
36
yt_dlp/extractor/sen.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import url_or_none
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
|
class SenIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?sen\.com/video/(?P<id>[0-9a-f-]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://www.sen.com/video/eef46eb1-4d79-4e28-be9d-bd937767f8c4',
|
||||||
|
'md5': 'ff615aca9691053c94f8f10d96cd7884',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'eef46eb1-4d79-4e28-be9d-bd937767f8c4',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'description': 'Florida, 28 Sep 2022',
|
||||||
|
'title': 'Hurricane Ian',
|
||||||
|
'tags': ['North America', 'Storm', 'Weather'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
api_data = self._download_json(f'https://api.sen.com/content/public/video/{video_id}', video_id)
|
||||||
|
m3u8_url = (traverse_obj(api_data, (
|
||||||
|
'data', 'nodes', lambda _, v: v['id'] == 'player', 'video', 'url', {url_or_none}, any))
|
||||||
|
or f'https://vod.sen.com/videos/{video_id}/manifest.m3u8')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'formats': self._extract_m3u8_formats(m3u8_url, video_id, 'mp4'),
|
||||||
|
**traverse_obj(api_data, ('data', 'nodes', lambda _, v: v['id'] == 'details', any, 'content', {
|
||||||
|
'title': ('title', 'text', {str}),
|
||||||
|
'description': ('descriptions', 0, 'text', {str}),
|
||||||
|
'tags': ('badges', ..., 'text', {str}),
|
||||||
|
})),
|
||||||
|
}
|
@ -27,7 +27,7 @@ class ServusIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'AA-28BYCQNH92111',
|
'id': 'AA-28BYCQNH92111',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Klettersteige in den Alpen',
|
'title': 'Vie Ferrate - Klettersteige in den Alpen',
|
||||||
'description': 'md5:25e47ddd83a009a0f9789ba18f2850ce',
|
'description': 'md5:25e47ddd83a009a0f9789ba18f2850ce',
|
||||||
'thumbnail': r're:^https?://.*\.jpg',
|
'thumbnail': r're:^https?://.*\.jpg',
|
||||||
'duration': 2823,
|
'duration': 2823,
|
||||||
@ -38,6 +38,7 @@ class ServusIE(InfoExtractor):
|
|||||||
'season_number': 11,
|
'season_number': 11,
|
||||||
'episode': 'Episode 8 - Vie Ferrate – Klettersteige in den Alpen',
|
'episode': 'Episode 8 - Vie Ferrate – Klettersteige in den Alpen',
|
||||||
'episode_number': 8,
|
'episode_number': 8,
|
||||||
|
'categories': ['Bergwelten'],
|
||||||
},
|
},
|
||||||
'params': {'skip_download': 'm3u8'},
|
'params': {'skip_download': 'm3u8'},
|
||||||
}, {
|
}, {
|
||||||
@ -71,8 +72,11 @@ class ServusIE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url).upper()
|
video_id = self._match_id(url).upper()
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
next_data = self._search_nextjs_data(webpage, video_id, fatal=False)
|
||||||
|
|
||||||
video = self._download_json(
|
video = self._download_json(
|
||||||
'https://api-player.redbull.com/stv/servus-tv?timeZone=Europe/Berlin',
|
'https://api-player.redbull.com/stv/servus-tv-playnet',
|
||||||
video_id, 'Downloading video JSON', query={'videoId': video_id})
|
video_id, 'Downloading video JSON', query={'videoId': video_id})
|
||||||
if not video.get('videoUrl'):
|
if not video.get('videoUrl'):
|
||||||
self._report_errors(video)
|
self._report_errors(video)
|
||||||
@ -89,7 +93,7 @@ def _real_extract(self, url):
|
|||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': video.get('title'),
|
'title': video.get('title'),
|
||||||
'description': self._get_description(video_id) or video.get('description'),
|
'description': self._get_description(next_data) or video.get('description'),
|
||||||
'thumbnail': video.get('poster'),
|
'thumbnail': video.get('poster'),
|
||||||
'duration': float_or_none(video.get('duration')),
|
'duration': float_or_none(video.get('duration')),
|
||||||
'timestamp': unified_timestamp(video.get('currentSunrise')),
|
'timestamp': unified_timestamp(video.get('currentSunrise')),
|
||||||
@ -100,16 +104,19 @@ def _real_extract(self, url):
|
|||||||
'episode_number': episode_number,
|
'episode_number': episode_number,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
|
**traverse_obj(next_data, ('props', 'pageProps', 'data', {
|
||||||
|
'title': ('title', 'rendered', {str}),
|
||||||
|
'timestamp': ('stv_date', 'raw', {int}),
|
||||||
|
'duration': ('stv_duration', {float_or_none}),
|
||||||
|
'categories': ('category_names', ..., {str}),
|
||||||
|
})),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_description(self, video_id):
|
def _get_description(self, next_data):
|
||||||
info = self._download_json(
|
return join_nonempty(*traverse_obj(next_data, (
|
||||||
f'https://backend.servustv.com/wp-json/rbmh/v2/media_asset/aa_id/{video_id}?fieldset=page',
|
'props', 'pageProps', 'data',
|
||||||
video_id, fatal=False)
|
('stv_short_description', 'stv_long_description'), {str},
|
||||||
|
{lambda x: x.replace('\n\n', '\n')}, {unescapeHTML})), delim='\n\n')
|
||||||
return join_nonempty(*traverse_obj(info, (
|
|
||||||
('stv_short_description', 'stv_long_description'),
|
|
||||||
{lambda x: unescapeHTML(x.replace('\n\n', '\n'))})), delim='\n\n')
|
|
||||||
|
|
||||||
def _report_errors(self, video):
|
def _report_errors(self, video):
|
||||||
playability_errors = traverse_obj(video, ('playabilityErrors', ...))
|
playability_errors = traverse_obj(video, ('playabilityErrors', ...))
|
||||||
|
76
yt_dlp/extractor/snapchat.py
Normal file
76
yt_dlp/extractor/snapchat.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import float_or_none, int_or_none, url_or_none
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
|
class SnapchatSpotlightIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?snapchat\.com/spotlight/(?P<id>\w+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.snapchat.com/spotlight/W7_EDlXWTBiXAEEniNoMPwAAYYWtidGhudGZpAX1TKn0JAX1TKnXJAAAAAA',
|
||||||
|
'md5': '46c580f63592d0cbb76e974d2f9f0fcc',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'W7_EDlXWTBiXAEEniNoMPwAAYYWtidGhudGZpAX1TKn0JAX1TKnXJAAAAAA',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Views 💕',
|
||||||
|
'description': '',
|
||||||
|
'thumbnail': r're:https://cf-st\.sc-cdn\.net/d/kKJHIR1QAznRKK9jgYYDq\.256\.IRZXSOY',
|
||||||
|
'duration': 4.665,
|
||||||
|
'timestamp': 1637777831.369,
|
||||||
|
'upload_date': '20211124',
|
||||||
|
'repost_count': int,
|
||||||
|
'uploader': 'shreypatel57',
|
||||||
|
'uploader_url': 'https://www.snapchat.com/add/shreypatel57',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.snapchat.com/spotlight/W7_EDlXWTBiXAEEniNoMPwAAYcnVjYWdwcGV1AZEaIYn5AZEaIYnrAAAAAQ',
|
||||||
|
'md5': '4cd9626458c1a0e3e6dbe72c544a9ec2',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'W7_EDlXWTBiXAEEniNoMPwAAYcnVjYWdwcGV1AZEaIYn5AZEaIYnrAAAAAQ',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Spotlight Snap',
|
||||||
|
'description': 'How he flirt her teacher🤭🤭🤩😍 #kdrama#cdrama #dramaclips #dramaspotlight',
|
||||||
|
'thumbnail': r're:https://cf-st\.sc-cdn\.net/i/ztfr6xFs0FOcFhwVczWfj\.256\.IRZXSOY',
|
||||||
|
'duration': 10.91,
|
||||||
|
'timestamp': 1722720291.307,
|
||||||
|
'upload_date': '20240803',
|
||||||
|
'view_count': int,
|
||||||
|
'repost_count': int,
|
||||||
|
'uploader': 'ganda0535',
|
||||||
|
'uploader_url': 'https://www.snapchat.com/add/ganda0535',
|
||||||
|
'tags': ['#dramaspotlight', '#dramaclips', '#cdrama', '#kdrama'],
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
page_props = self._search_nextjs_data(webpage, video_id)['props']['pageProps']
|
||||||
|
video_data = traverse_obj(page_props, (
|
||||||
|
'spotlightFeed', 'spotlightStories',
|
||||||
|
lambda _, v: v['story']['storyId']['value'] == video_id, 'metadata', any), None)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'ext': 'mp4',
|
||||||
|
**traverse_obj(video_data, ('videoMetadata', {
|
||||||
|
'title': ('name', {str}),
|
||||||
|
'description': ('description', {str}),
|
||||||
|
'timestamp': ('uploadDateMs', {lambda x: float_or_none(x, 1000)}),
|
||||||
|
'view_count': ('viewCount', {int_or_none}, {lambda x: None if x == -1 else x}),
|
||||||
|
'repost_count': ('shareCount', {int_or_none}),
|
||||||
|
'url': ('contentUrl', {url_or_none}),
|
||||||
|
'width': ('width', {int_or_none}),
|
||||||
|
'height': ('height', {int_or_none}),
|
||||||
|
'duration': ('durationMs', {lambda x: float_or_none(x, 1000)}),
|
||||||
|
'thumbnail': ('thumbnailUrl', {url_or_none}),
|
||||||
|
'uploader': ('creator', 'personCreator', 'username', {str}),
|
||||||
|
'uploader_url': ('creator', 'personCreator', 'url', {url_or_none}),
|
||||||
|
})),
|
||||||
|
**traverse_obj(video_data, {
|
||||||
|
'description': ('description', {str}),
|
||||||
|
'tags': ('hashtags', ..., {str}),
|
||||||
|
'view_count': ('engagementStats', 'viewCount', {int_or_none}, {lambda x: None if x == -1 else x}),
|
||||||
|
'repost_count': ('engagementStats', 'shareCount', {int_or_none}),
|
||||||
|
}),
|
||||||
|
}
|
@ -472,7 +472,7 @@ def _real_extract(self, url):
|
|||||||
title = self._og_search_title(webpage)
|
title = self._og_search_title(webpage)
|
||||||
|
|
||||||
urql_state = self._search_json(
|
urql_state = self._search_json(
|
||||||
r'window\.svt\.nyh\.urqlState\s*=', webpage, 'json data', display_id)
|
r'window\.svt\.(?:nyh\.)?urqlState\s*=', webpage, 'json data', display_id)
|
||||||
|
|
||||||
data = traverse_obj(urql_state, (..., 'data', {str}, {json.loads}), get_all=False) or {}
|
data = traverse_obj(urql_state, (..., 'data', {str}, {json.loads}), get_all=False) or {}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
|
|
||||||
class Tele13IE(InfoExtractor):
|
class Tele13IE(InfoExtractor):
|
||||||
_VALID_URL = r'^https?://(?:www\.)?t13\.cl/videos(?:/[^/]+)+/(?P<id>[\w-]+)'
|
_VALID_URL = r'https?://(?:www\.)?t13\.cl/videos(?:/[^/]+)+/(?P<id>[\w-]+)'
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
'url': 'http://www.t13.cl/videos/actualidad/el-circulo-de-hierro-de-michelle-bachelet-en-su-regreso-a-la-moneda',
|
'url': 'http://www.t13.cl/videos/actualidad/el-circulo-de-hierro-de-michelle-bachelet-en-su-regreso-a-la-moneda',
|
||||||
|
@ -1,33 +1,31 @@
|
|||||||
import base64
|
|
||||||
import datetime as dt
|
|
||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..networking import HEADRequest
|
from ..networking import HEADRequest
|
||||||
from ..utils import int_or_none, traverse_obj, urlencode_postdata, urljoin
|
from ..utils import int_or_none, traverse_obj, url_or_none, urljoin
|
||||||
|
|
||||||
|
|
||||||
class TenPlayIE(InfoExtractor):
|
class TenPlayIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?10play\.com\.au/(?:[^/]+/)+(?P<id>tpv\d{6}[a-z]{5})'
|
_VALID_URL = r'https?://(?:www\.)?10play\.com\.au/(?:[^/]+/)+(?P<id>tpv\d{6}[a-z]{5})'
|
||||||
_NETRC_MACHINE = '10play'
|
_NETRC_MACHINE = '10play'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://10play.com.au/neighbours/web-extras/season-39/nathan-borg-is-the-first-aussie-actor-with-a-cochlear-implant-to-join-neighbours/tpv210128qupwd',
|
'url': 'https://10play.com.au/neighbours/web-extras/season-41/heres-a-first-look-at-mischa-bartons-neighbours-debut/tpv230911hyxnz',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '6226844312001',
|
'id': '6336940246112',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Nathan Borg Is The First Aussie Actor With A Cochlear Implant To Join Neighbours',
|
'title': 'Here\'s A First Look At Mischa Barton\'s Neighbours Debut',
|
||||||
'alt_title': 'Nathan Borg Is The First Aussie Actor With A Cochlear Implant To Join Neighbours',
|
'alt_title': 'Here\'s A First Look At Mischa Barton\'s Neighbours Debut',
|
||||||
'description': 'md5:a02d0199c901c2dd4c796f1e7dd0de43',
|
'description': 'Neighbours Premieres Monday, September 18 At 4:30pm On 10 And 10 Play And 6:30pm On 10 Peach',
|
||||||
'duration': 186,
|
'duration': 74,
|
||||||
'season': 'Season 39',
|
'season': 'Season 41',
|
||||||
'season_number': 39,
|
'season_number': 41,
|
||||||
'series': 'Neighbours',
|
'series': 'Neighbours',
|
||||||
'thumbnail': r're:https://.*\.jpg',
|
'thumbnail': r're:https://.*\.jpg',
|
||||||
'uploader': 'Channel 10',
|
'uploader': 'Channel 10',
|
||||||
'age_limit': 15,
|
'age_limit': 15,
|
||||||
'timestamp': 1611810000,
|
'timestamp': 1694386800,
|
||||||
'upload_date': '20210128',
|
'upload_date': '20230910',
|
||||||
'uploader_id': '2199827728001',
|
'uploader_id': '2199827728001',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
@ -35,21 +33,30 @@ class TenPlayIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
'skip': 'Only available in Australia',
|
'skip': 'Only available in Australia',
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://10play.com.au/todd-sampsons-body-hack/episodes/season-4/episode-7/tpv200921kvngh',
|
'url': 'https://10play.com.au/neighbours/episodes/season-42/episode-9107/tpv240902nzqyp',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '6192880312001',
|
'id': '9000000000091177',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': "Todd Sampson's Body Hack - S4 Ep. 2",
|
'title': 'Neighbours - S42 Ep. 9107',
|
||||||
'description': 'md5:fa278820ad90f08ea187f9458316ac74',
|
'alt_title': 'Thu 05 Sep',
|
||||||
|
'description': 'md5:37a1f4271be34b9ee2b533426a5fbaef',
|
||||||
|
'duration': 1388,
|
||||||
|
'episode': 'Episode 9107',
|
||||||
|
'episode_number': 9107,
|
||||||
|
'season': 'Season 42',
|
||||||
|
'season_number': 42,
|
||||||
|
'series': 'Neighbours',
|
||||||
|
'thumbnail': r're:https://.*\.jpg',
|
||||||
'age_limit': 15,
|
'age_limit': 15,
|
||||||
'timestamp': 1600770600,
|
'timestamp': 1725517860,
|
||||||
'upload_date': '20200922',
|
'upload_date': '20240905',
|
||||||
'uploader': 'Channel 10',
|
'uploader': 'Channel 10',
|
||||||
'uploader_id': '2199827728001',
|
'uploader_id': '2199827728001',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
'skip': 'Only available in Australia',
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://10play.com.au/how-to-stay-married/web-extras/season-1/terrys-talks-ep-1-embracing-change/tpv190915ylupc',
|
'url': 'https://10play.com.au/how-to-stay-married/web-extras/season-1/terrys-talks-ep-1-embracing-change/tpv190915ylupc',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@ -66,55 +73,42 @@ class TenPlayIE(InfoExtractor):
|
|||||||
'X': 18,
|
'X': 18,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_bearer_token(self, video_id):
|
|
||||||
username, password = self._get_login_info()
|
|
||||||
if username is None or password is None:
|
|
||||||
self.raise_login_required('Your 10play account\'s details must be provided with --username and --password.')
|
|
||||||
_timestamp = dt.datetime.now().strftime('%Y%m%d000000')
|
|
||||||
_auth_header = base64.b64encode(_timestamp.encode('ascii')).decode('ascii')
|
|
||||||
data = self._download_json('https://10play.com.au/api/user/auth', video_id, 'Getting bearer token', headers={
|
|
||||||
'X-Network-Ten-Auth': _auth_header,
|
|
||||||
}, data=urlencode_postdata({
|
|
||||||
'email': username,
|
|
||||||
'password': password,
|
|
||||||
}))
|
|
||||||
return 'Bearer ' + data['jwt']['accessToken']
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
content_id = self._match_id(url)
|
content_id = self._match_id(url)
|
||||||
data = self._download_json(
|
data = self._download_json(
|
||||||
'https://10play.com.au/api/v1/videos/' + content_id, content_id)
|
'https://10play.com.au/api/v1/videos/' + content_id, content_id)
|
||||||
headers = {}
|
|
||||||
|
|
||||||
if data.get('memberGated') is True:
|
video_data = self._download_json(
|
||||||
_token = self._get_bearer_token(content_id)
|
f'https://vod.ten.com.au/api/videos/bcquery?command=find_videos_by_id&video_id={data["altId"]}',
|
||||||
headers = {'Authorization': _token}
|
content_id, 'Downloading video JSON')
|
||||||
|
m3u8_url = self._request_webpage(
|
||||||
_video_url = self._download_json(
|
HEADRequest(video_data['items'][0]['HLSURL']),
|
||||||
data.get('playbackApiEndpoint'), content_id, 'Downloading video JSON',
|
content_id, 'Checking stream URL').url
|
||||||
headers=headers).get('source')
|
|
||||||
m3u8_url = self._request_webpage(HEADRequest(
|
|
||||||
_video_url), content_id).url
|
|
||||||
if '10play-not-in-oz' in m3u8_url:
|
if '10play-not-in-oz' in m3u8_url:
|
||||||
self.raise_geo_restricted(countries=['AU'])
|
self.raise_geo_restricted(countries=['AU'])
|
||||||
|
# Attempt to get a higher quality stream
|
||||||
|
m3u8_url = m3u8_url.replace(',150,75,55,0000', ',300,150,75,55,0000')
|
||||||
formats = self._extract_m3u8_formats(m3u8_url, content_id, 'mp4')
|
formats = self._extract_m3u8_formats(m3u8_url, content_id, 'mp4')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
'id': content_id,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': {'en': [{'url': data.get('captionUrl')}]} if data.get('captionUrl') else None,
|
'subtitles': {'en': [{'url': data['captionUrl']}]} if url_or_none(data.get('captionUrl')) else None,
|
||||||
'id': data.get('altId') or content_id,
|
|
||||||
'duration': data.get('duration'),
|
|
||||||
'title': data.get('subtitle'),
|
|
||||||
'alt_title': data.get('title'),
|
|
||||||
'description': data.get('description'),
|
|
||||||
'age_limit': self._AUS_AGES.get(data.get('classification')),
|
|
||||||
'series': data.get('tvShow'),
|
|
||||||
'season_number': int_or_none(data.get('season')),
|
|
||||||
'episode_number': int_or_none(data.get('episode')),
|
|
||||||
'timestamp': data.get('published'),
|
|
||||||
'thumbnail': data.get('imageUrl'),
|
|
||||||
'uploader': 'Channel 10',
|
'uploader': 'Channel 10',
|
||||||
'uploader_id': '2199827728001',
|
'uploader_id': '2199827728001',
|
||||||
|
**traverse_obj(data, {
|
||||||
|
'id': ('altId', {str}),
|
||||||
|
'duration': ('duration', {int_or_none}),
|
||||||
|
'title': ('subtitle', {str}),
|
||||||
|
'alt_title': ('title', {str}),
|
||||||
|
'description': ('description', {str}),
|
||||||
|
'age_limit': ('classification', {self._AUS_AGES.get}),
|
||||||
|
'series': ('tvShow', {str}),
|
||||||
|
'season_number': ('season', {int_or_none}),
|
||||||
|
'episode_number': ('episode', {int_or_none}),
|
||||||
|
'timestamp': ('published', {int_or_none}),
|
||||||
|
'thumbnail': ('imageUrl', {url_or_none}),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -542,16 +542,12 @@ def _extract_web_formats(self, aweme_detail):
|
|||||||
**COMMON_FORMAT_INFO,
|
**COMMON_FORMAT_INFO,
|
||||||
'format_id': 'download',
|
'format_id': 'download',
|
||||||
'url': self._proto_relative_url(download_url),
|
'url': self._proto_relative_url(download_url),
|
||||||
|
'format_note': 'watermarked',
|
||||||
|
'preference': -2,
|
||||||
})
|
})
|
||||||
|
|
||||||
self._remove_duplicate_formats(formats)
|
self._remove_duplicate_formats(formats)
|
||||||
|
|
||||||
for f in traverse_obj(formats, lambda _, v: 'unwatermarked' not in v['url']):
|
|
||||||
f.update({
|
|
||||||
'format_note': join_nonempty(f.get('format_note'), 'watermarked', delim=', '),
|
|
||||||
'preference': f.get('preference') or -2,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Is it a slideshow with only audio for download?
|
# Is it a slideshow with only audio for download?
|
||||||
if not formats and traverse_obj(aweme_detail, ('music', 'playUrl', {url_or_none})):
|
if not formats and traverse_obj(aweme_detail, ('music', 'playUrl', {url_or_none})):
|
||||||
audio_url = aweme_detail['music']['playUrl']
|
audio_url = aweme_detail['music']['playUrl']
|
||||||
@ -565,7 +561,8 @@ def _extract_web_formats(self, aweme_detail):
|
|||||||
'vcodec': 'none',
|
'vcodec': 'none',
|
||||||
})
|
})
|
||||||
|
|
||||||
return formats
|
# Filter out broken formats, see https://github.com/yt-dlp/yt-dlp/issues/11034
|
||||||
|
return [f for f in formats if urllib.parse.urlparse(f['url']).hostname != 'www.tiktok.com']
|
||||||
|
|
||||||
def _parse_aweme_video_web(self, aweme_detail, webpage_url, video_id, extract_flat=False):
|
def _parse_aweme_video_web(self, aweme_detail, webpage_url, video_id, extract_flat=False):
|
||||||
author_info = traverse_obj(aweme_detail, (('authorInfo', 'author', None), {
|
author_info = traverse_obj(aweme_detail, (('authorInfo', 'author', None), {
|
||||||
|
@ -6,11 +6,12 @@
|
|||||||
str_or_none,
|
str_or_none,
|
||||||
strip_or_none,
|
strip_or_none,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
|
update_url_query,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TVerIE(InfoExtractor):
|
class TVerIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?tver\.jp/(?:(?P<type>lp|corner|series|episodes?|feature|tokyo2020/video|olympic/paris2024/video)/)+(?P<id>[a-zA-Z0-9]+)'
|
_VALID_URL = r'https?://(?:www\.)?tver\.jp/(?:(?P<type>lp|corner|series|episodes?|feature)/)+(?P<id>[a-zA-Z0-9]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'skip': 'videos are only available for 7 days',
|
'skip': 'videos are only available for 7 days',
|
||||||
'url': 'https://tver.jp/episodes/ep83nf3w4p',
|
'url': 'https://tver.jp/episodes/ep83nf3w4p',
|
||||||
@ -21,80 +22,115 @@ class TVerIE(InfoExtractor):
|
|||||||
'episode': '売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着!',
|
'episode': '売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着!',
|
||||||
'alt_title': '売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着!',
|
'alt_title': '売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着!',
|
||||||
'channel': 'テレビ朝日',
|
'channel': 'テレビ朝日',
|
||||||
|
'id': 'ep83nf3w4p',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'onair_label': '5月3日(火)放送分',
|
||||||
|
'ext_title': '家事ヤロウ!!! 売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着! テレビ朝日 5月3日(火)放送分',
|
||||||
},
|
},
|
||||||
'add_ie': ['BrightcoveNew'],
|
'add_ie': ['BrightcoveNew'],
|
||||||
}, {
|
|
||||||
'url': 'https://tver.jp/olympic/paris2024/video/6359578055112/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '6359578055112',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': '堀米雄斗 金メダルで五輪連覇!「みんなの応援が最後に乗れたカギ」',
|
|
||||||
'timestamp': 1722279928,
|
|
||||||
'upload_date': '20240729',
|
|
||||||
'tags': ['20240729', 'japanese', 'japanmedal', 'paris'],
|
|
||||||
'uploader_id': '4774017240001',
|
|
||||||
'thumbnail': r're:https?://[^/?#]+boltdns\.net/[^?#]+/1920x1080/match/image\.jpg',
|
|
||||||
'duration': 670.571,
|
|
||||||
},
|
|
||||||
'params': {'skip_download': 'm3u8'},
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://tver.jp/corner/f0103888',
|
'url': 'https://tver.jp/corner/f0103888',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://tver.jp/lp/f0033031',
|
'url': 'https://tver.jp/lp/f0033031',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://tver.jp/series/srtxft431v',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'srtxft431v',
|
||||||
|
'title': '名探偵コナン',
|
||||||
|
},
|
||||||
|
'playlist': [
|
||||||
|
{
|
||||||
|
'md5': '779ffd97493ed59b0a6277ea726b389e',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'ref:conan-1137-241005',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '名探偵コナン #1137「行列店、味変の秘密」',
|
||||||
|
'uploader_id': '5330942432001',
|
||||||
|
'tags': [],
|
||||||
|
'channel': '読売テレビ',
|
||||||
|
'series': '名探偵コナン',
|
||||||
|
'description': 'md5:601fccc1d2430d942a2c8068c4b33eb5',
|
||||||
|
'episode': '#1137「行列店、味変の秘密」',
|
||||||
|
'duration': 1469.077,
|
||||||
|
'timestamp': 1728030405,
|
||||||
|
'upload_date': '20241004',
|
||||||
|
'alt_title': '名探偵コナン #1137「行列店、味変の秘密」 読売テレビ 10月5日(土)放送分',
|
||||||
|
'thumbnail': r're:https://.+\.jpg',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
}, {
|
||||||
|
'url': 'https://tver.jp/series/sru35hwdd2',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'sru35hwdd2',
|
||||||
|
'title': '神回だけ見せます!',
|
||||||
|
},
|
||||||
|
'playlist_count': 11,
|
||||||
|
}, {
|
||||||
|
'url': 'https://tver.jp/series/srkq2shp9d',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s'
|
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s'
|
||||||
_PLATFORM_UID = None
|
_HEADERS = {'x-tver-platform-type': 'web'}
|
||||||
_PLATFORM_TOKEN = None
|
_PLATFORM_QUERY = {}
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
create_response = self._download_json(
|
session_info = self._download_json(
|
||||||
'https://platform-api.tver.jp/v2/api/platform_users/browser/create', None,
|
'https://platform-api.tver.jp/v2/api/platform_users/browser/create',
|
||||||
note='Creating session', data=b'device_type=pc', headers={
|
None, 'Creating session', data=b'device_type=pc')
|
||||||
'Origin': 'https://s.tver.jp',
|
self._PLATFORM_QUERY = traverse_obj(session_info, ('result', {
|
||||||
'Referer': 'https://s.tver.jp/',
|
'platform_uid': 'platform_uid',
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'platform_token': 'platform_token',
|
||||||
|
}))
|
||||||
|
|
||||||
|
def _call_platform_api(self, path, video_id, note=None, fatal=True, query=None):
|
||||||
|
return self._download_json(
|
||||||
|
f'https://platform-api.tver.jp/service/api/{path}', video_id, note,
|
||||||
|
fatal=fatal, headers=self._HEADERS, query={
|
||||||
|
**self._PLATFORM_QUERY,
|
||||||
|
**(query or {}),
|
||||||
})
|
})
|
||||||
self._PLATFORM_UID = traverse_obj(create_response, ('result', 'platform_uid'))
|
|
||||||
self._PLATFORM_TOKEN = traverse_obj(create_response, ('result', 'platform_token'))
|
def _yield_episode_ids_for_series(self, series_id):
|
||||||
|
seasons_info = self._download_json(
|
||||||
|
f'https://service-api.tver.jp/api/v1/callSeriesSeasons/{series_id}',
|
||||||
|
series_id, 'Downloading seasons info', headers=self._HEADERS)
|
||||||
|
for season_id in traverse_obj(
|
||||||
|
seasons_info, ('result', 'contents', lambda _, v: v['type'] == 'season', 'content', 'id', {str})):
|
||||||
|
episodes_info = self._call_platform_api(
|
||||||
|
f'v1/callSeasonEpisodes/{season_id}', series_id, f'Downloading season {season_id} episodes info')
|
||||||
|
yield from traverse_obj(episodes_info, (
|
||||||
|
'result', 'contents', lambda _, v: v['type'] == 'episode', 'content', 'id', {str}))
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id, video_type = self._match_valid_url(url).group('id', 'type')
|
video_id, video_type = self._match_valid_url(url).group('id', 'type')
|
||||||
|
|
||||||
if video_type == 'olympic/paris2024/video':
|
if video_type == 'series':
|
||||||
# Player ID is taken from .content.brightcove.E200.pro.pc.account_id:
|
series_info = self._call_platform_api(
|
||||||
# https://tver.jp/olympic/paris2024/req/api/hook?q=https%3A%2F%2Folympic-assets.tver.jp%2Fweb-static%2Fjson%2Fconfig.json&d=
|
f'v2/callSeries/{video_id}', video_id, 'Downloading series info')
|
||||||
return self.url_result(smuggle_url(
|
return self.playlist_from_matches(
|
||||||
self.BRIGHTCOVE_URL_TEMPLATE % ('4774017240001', video_id),
|
self._yield_episode_ids_for_series(video_id), video_id,
|
||||||
{'geo_countries': ['JP']}), 'BrightcoveNew')
|
traverse_obj(series_info, ('result', 'content', 'content', 'title', {str})),
|
||||||
|
ie=TVerIE, getter=lambda x: f'https://tver.jp/episodes/{x}')
|
||||||
|
|
||||||
elif video_type not in {'series', 'episodes'}:
|
if video_type != 'episodes':
|
||||||
webpage = self._download_webpage(url, video_id, note='Resolving to new URL')
|
webpage = self._download_webpage(url, video_id, note='Resolving to new URL')
|
||||||
video_id = self._match_id(self._search_regex(
|
video_id = self._match_id(self._search_regex(
|
||||||
(r'canonical"\s*href="(https?://tver\.jp/[^"]+)"', r'&link=(https?://tver\.jp/[^?&]+)[?&]'),
|
(r'canonical"\s*href="(https?://tver\.jp/[^"]+)"', r'&link=(https?://tver\.jp/[^?&]+)[?&]'),
|
||||||
webpage, 'url regex'))
|
webpage, 'url regex'))
|
||||||
|
|
||||||
episode_info = self._download_json(
|
episode_info = self._call_platform_api(
|
||||||
f'https://platform-api.tver.jp/service/api/v1/callEpisode/{video_id}?require_data=mylist,later[epefy106ur],good[epefy106ur],resume[epefy106ur]',
|
f'v1/callEpisode/{video_id}', video_id, 'Downloading episode info', fatal=False, query={
|
||||||
video_id, fatal=False,
|
'require_data': 'mylist,later[epefy106ur],good[epefy106ur],resume[epefy106ur]',
|
||||||
query={
|
|
||||||
'platform_uid': self._PLATFORM_UID,
|
|
||||||
'platform_token': self._PLATFORM_TOKEN,
|
|
||||||
}, headers={
|
|
||||||
'x-tver-platform-type': 'web',
|
|
||||||
})
|
})
|
||||||
episode_content = traverse_obj(
|
episode_content = traverse_obj(
|
||||||
episode_info, ('result', 'episode', 'content')) or {}
|
episode_info, ('result', 'episode', 'content')) or {}
|
||||||
|
|
||||||
|
version = traverse_obj(episode_content, ('version', {str_or_none}), default='5')
|
||||||
video_info = self._download_json(
|
video_info = self._download_json(
|
||||||
f'https://statics.tver.jp/content/episode/{video_id}.json', video_id,
|
f'https://statics.tver.jp/content/episode/{video_id}.json', video_id, 'Downloading video info',
|
||||||
query={
|
query={'v': version}, headers={'Referer': 'https://tver.jp/'})
|
||||||
'v': str_or_none(episode_content.get('version')) or '5',
|
|
||||||
}, headers={
|
|
||||||
'Origin': 'https://tver.jp',
|
|
||||||
'Referer': 'https://tver.jp/',
|
|
||||||
})
|
|
||||||
p_id = video_info['video']['accountID']
|
p_id = video_info['video']['accountID']
|
||||||
r_id = traverse_obj(video_info, ('video', ('videoRefID', 'videoID')), get_all=False)
|
r_id = traverse_obj(video_info, ('video', ('videoRefID', 'videoID')), get_all=False)
|
||||||
if not r_id:
|
if not r_id:
|
||||||
@ -110,6 +146,23 @@ def _real_extract(self, url):
|
|||||||
provider = str_or_none(episode_content.get('productionProviderName'))
|
provider = str_or_none(episode_content.get('productionProviderName'))
|
||||||
onair_label = str_or_none(episode_content.get('broadcastDateLabel'))
|
onair_label = str_or_none(episode_content.get('broadcastDateLabel'))
|
||||||
|
|
||||||
|
thumbnails = [
|
||||||
|
{
|
||||||
|
'id': quality,
|
||||||
|
'url': update_url_query(
|
||||||
|
f'https://statics.tver.jp/images/content/thumbnail/episode/{quality}/{video_id}.jpg',
|
||||||
|
{'v': version}),
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
|
}
|
||||||
|
for quality, width, height in [
|
||||||
|
('small', 480, 270),
|
||||||
|
('medium', 640, 360),
|
||||||
|
('large', 960, 540),
|
||||||
|
('xlarge', 1280, 720),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'_type': 'url_transparent',
|
'_type': 'url_transparent',
|
||||||
'title': title,
|
'title': title,
|
||||||
@ -119,6 +172,7 @@ def _real_extract(self, url):
|
|||||||
'alt_title': join_nonempty(title, provider, onair_label, delim=' '),
|
'alt_title': join_nonempty(title, provider, onair_label, delim=' '),
|
||||||
'channel': provider,
|
'channel': provider,
|
||||||
'description': str_or_none(video_info.get('description')),
|
'description': str_or_none(video_info.get('description')),
|
||||||
|
'thumbnails': thumbnails,
|
||||||
'url': smuggle_url(
|
'url': smuggle_url(
|
||||||
self.BRIGHTCOVE_URL_TEMPLATE % (p_id, r_id), {'geo_countries': ['JP']}),
|
self.BRIGHTCOVE_URL_TEMPLATE % (p_id, r_id), {'geo_countries': ['JP']}),
|
||||||
'ie_key': 'BrightcoveNew',
|
'ie_key': 'BrightcoveNew',
|
||||||
|
@ -270,7 +270,7 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
|
|
||||||
class TwitCastingUserIE(InfoExtractor):
|
class TwitCastingUserIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:[^/?#]+\.)?twitcasting\.tv/(?P<id>[^/?#]+)/(:?show|archive)/?(?:[#?]|$)'
|
_VALID_URL = r'https?://(?:[^/?#]+\.)?twitcasting\.tv/(?P<id>[^/?#]+)/(?:show|archive)/?(?:[#?]|$)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://twitcasting.tv/natsuiromatsuri/archive/',
|
'url': 'https://twitcasting.tv/natsuiromatsuri/archive/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
148
yt_dlp/extractor/vidflex.py
Normal file
148
yt_dlp/extractor/vidflex.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import base64
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
join_nonempty,
|
||||||
|
mimetype2ext,
|
||||||
|
url_or_none,
|
||||||
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
|
class VidflexIE(InfoExtractor):
|
||||||
|
_DOMAINS_RE = [
|
||||||
|
r'[^.]+\.vidflex\.tv',
|
||||||
|
r'(?:www\.)?acactv\.ca',
|
||||||
|
r'(?:www\.)?albertalacrossetv\.com',
|
||||||
|
r'(?:www\.)?cjfltv\.com',
|
||||||
|
r'(?:www\.)?figureitoutbaseball\.com',
|
||||||
|
r'(?:www\.)?ocaalive\.com',
|
||||||
|
r'(?:www\.)?pegasussports\.tv',
|
||||||
|
r'(?:www\.)?praxisseries\.ca',
|
||||||
|
r'(?:www\.)?silenticetv\.com',
|
||||||
|
r'(?:www\.)?tuffhedemantv\.com',
|
||||||
|
r'(?:www\.)?watchfuntv\.com',
|
||||||
|
r'live\.ofsaa\.on\.ca',
|
||||||
|
r'tv\.procoro\.ca',
|
||||||
|
r'tv\.realcastmedia\.net',
|
||||||
|
r'tv\.fringetheatre\.ca',
|
||||||
|
r'video\.haisla\.ca',
|
||||||
|
r'video\.hockeycanada\.ca',
|
||||||
|
r'video\.huuayaht\.org',
|
||||||
|
r'video\.turningpointensemble\.ca',
|
||||||
|
r'videos\.livingworks\.net',
|
||||||
|
r'videos\.telusworldofscienceedmonton\.ca',
|
||||||
|
r'watch\.binghamtonbulldogs\.com',
|
||||||
|
r'watch\.rekindle\.tv',
|
||||||
|
r'watch\.wpca\.com',
|
||||||
|
]
|
||||||
|
_VALID_URL = rf'https?://(?:{"|".join(_DOMAINS_RE)})/[a-z]{{2}}(?:-[a-z]{{2}})?/c/[\w-]+\.(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://video.hockeycanada.ca/en/c/nwt-micd-up-with-jamie-lee-rattray.107486',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# m3u8 + https
|
||||||
|
'url': 'https://video.hockeycanada.ca/en-us/c/nwt-micd-up-with-jamie-lee-rattray.107486',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '107486',
|
||||||
|
'title': 'NWT: Mic’d up with Jamie Lee Rattray',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'duration': 115,
|
||||||
|
'timestamp': 1634310409,
|
||||||
|
'upload_date': '20211015',
|
||||||
|
'tags': ['English', '2021', "National Women's Team"],
|
||||||
|
'description': 'md5:efb1cf6165b48cc3f5555c4262dd5b23',
|
||||||
|
'thumbnail': r're:^https?://wpmedia01-a\.akamaihd\.net/en/asset/public/image/.+',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True},
|
||||||
|
}, {
|
||||||
|
'url': 'https://video.hockeycanada.ca/en/c/mwc-remembering-the-wild-ride-in-riga.112307',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '112307',
|
||||||
|
'title': 'MWC: Remembering the wild ride in Riga',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'duration': 322,
|
||||||
|
'timestamp': 1716235607,
|
||||||
|
'upload_date': '20240520',
|
||||||
|
'tags': ['English', '2024', "National Men's Team", 'IIHF World Championship', 'Fan'],
|
||||||
|
'description': r're:.+Canada’s National Men’s Team.+',
|
||||||
|
'thumbnail': r're:^https?://wpmedia01-a\.akamaihd\.net/en/asset/public/image/.+',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True},
|
||||||
|
}, {
|
||||||
|
# the same video in French
|
||||||
|
'url': 'https://video.hockeycanada.ca/fr/c/cmm-retour-sur-un-parcours-endiable-a-riga.112304',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '112304',
|
||||||
|
'title': 'CMM : Retour sur un parcours endiablé à Riga',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'duration': 322,
|
||||||
|
'timestamp': 1716235545,
|
||||||
|
'upload_date': '20240520',
|
||||||
|
'tags': ['French', '2024', "National Men's Team", 'IIHF World Championship', 'Fan'],
|
||||||
|
'description': 'md5:cf825222882a3dab1cd62cffcf3b4d1f',
|
||||||
|
'thumbnail': r're:^https?://wpmedia01-a\.akamaihd\.net/en/asset/public/image/.+',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True},
|
||||||
|
}, {
|
||||||
|
'url': 'https://myfbcgreenville.vidflex.tv/en/c/may-12th-2024.658',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.figureitoutbaseball.com/en/c/fiob-podcast-14-dan-bertolini-ncaa-d1-head-coach-recorded-11-29-2018.1367',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://videos.telusworldofscienceedmonton.ca/en/c/the-aurora-project-timelapse-4.577',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.tuffhedemantv.com/en/c/2022-tuff-hedeman-tour-hobbs-nm-january-22.227',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.albertalacrossetv.com/en/c/up-floor-ground-balls-one-more.3449',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.silenticetv.com/en/c/jp-unlocked-day-in-the-life-of-langley-ha-15u.5197',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://jphl.vidflex.tv/en/c/jp-unlocked-day-in-the-life-of-langley-ha-15u.5197',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
data_url = self._html_search_regex(
|
||||||
|
r'content_api:\s*(["\'])(?P<url>https?://(?:(?!\1).)+)\1', webpage, 'content api url', group='url')
|
||||||
|
media_config = traverse_obj(
|
||||||
|
self._download_json(data_url, video_id),
|
||||||
|
('config', {base64.b64decode}, {bytes.decode}, {json.loads}, {dict}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'formats': list(self._yield_formats(media_config, video_id)),
|
||||||
|
**self._search_json_ld(
|
||||||
|
webpage.replace('/*<![CDATA[*/', '').replace('/*]]>*/', ''), video_id),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _yield_formats(self, media_config, video_id):
|
||||||
|
for media_source in traverse_obj(media_config, ('media', 'source', lambda _, v: url_or_none(v['src']))):
|
||||||
|
media_url = media_source['src']
|
||||||
|
media_type = mimetype2ext(media_source.get('type'))
|
||||||
|
|
||||||
|
if media_type == 'm3u8':
|
||||||
|
yield from self._extract_m3u8_formats(media_url, video_id, fatal=False, m3u8_id='hls')
|
||||||
|
elif media_type == 'mp4':
|
||||||
|
bitrate = self._search_regex(r'_(\d+)k\.mp4', media_url, 'bitrate', default=None)
|
||||||
|
yield {
|
||||||
|
'format_id': join_nonempty('http', bitrate),
|
||||||
|
'url': media_url,
|
||||||
|
'ext': 'mp4',
|
||||||
|
'tbr': int_or_none(bitrate),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
yield {
|
||||||
|
'url': media_url,
|
||||||
|
'ext': media_type,
|
||||||
|
}
|
@ -21,6 +21,7 @@
|
|||||||
parse_filesize,
|
parse_filesize,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
parse_qs,
|
parse_qs,
|
||||||
|
qualities,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
@ -146,6 +147,8 @@ def _parse_config(self, config, video_id):
|
|||||||
})
|
})
|
||||||
|
|
||||||
# TODO: fix handling of 308 status code returned for live archive manifest requests
|
# TODO: fix handling of 308 status code returned for live archive manifest requests
|
||||||
|
QUALITIES = ('low', 'medium', 'high')
|
||||||
|
quality = qualities(QUALITIES)
|
||||||
sep_pattern = r'/sep/video/'
|
sep_pattern = r'/sep/video/'
|
||||||
for files_type in ('hls', 'dash'):
|
for files_type in ('hls', 'dash'):
|
||||||
for cdn_name, cdn_data in (try_get(config_files, lambda x: x[files_type]['cdns']) or {}).items():
|
for cdn_name, cdn_data in (try_get(config_files, lambda x: x[files_type]['cdns']) or {}).items():
|
||||||
@ -166,6 +169,11 @@ def _parse_config(self, config, video_id):
|
|||||||
m_url, video_id, 'mp4', live=is_live, m3u8_id=f_id,
|
m_url, video_id, 'mp4', live=is_live, m3u8_id=f_id,
|
||||||
note=f'Downloading {cdn_name} m3u8 information',
|
note=f'Downloading {cdn_name} m3u8 information',
|
||||||
fatal=False)
|
fatal=False)
|
||||||
|
# m3u8 doesn't give audio bitrates; need to prioritize based on GROUP-ID
|
||||||
|
# See: https://github.com/yt-dlp/yt-dlp/issues/10854
|
||||||
|
for f in fmts:
|
||||||
|
if mobj := re.search(rf'audio-({"|".join(QUALITIES)})', f['format_id']):
|
||||||
|
f['quality'] = quality(mobj.group(1))
|
||||||
formats.extend(fmts)
|
formats.extend(fmts)
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
self._merge_subtitles(subs, target=subtitles)
|
||||||
elif files_type == 'dash':
|
elif files_type == 'dash':
|
||||||
@ -234,13 +242,30 @@ def _parse_config(self, config, video_id):
|
|||||||
'_format_sort_fields': ('quality', 'res', 'fps', 'hdr:12', 'source'),
|
'_format_sort_fields': ('quality', 'res', 'fps', 'hdr:12', 'source'),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _extract_original_format(self, url, video_id, unlisted_hash=None):
|
def _call_videos_api(self, video_id, jwt_token, unlisted_hash=None, **kwargs):
|
||||||
|
return self._download_json(
|
||||||
|
join_nonempty(f'https://api.vimeo.com/videos/{video_id}', unlisted_hash, delim=':'),
|
||||||
|
video_id, 'Downloading API JSON', headers={
|
||||||
|
'Authorization': f'jwt {jwt_token}',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}, query={
|
||||||
|
'fields': ','.join((
|
||||||
|
'config_url', 'created_time', 'description', 'download', 'license',
|
||||||
|
'metadata.connections.comments.total', 'metadata.connections.likes.total',
|
||||||
|
'release_time', 'stats.plays')),
|
||||||
|
}, **kwargs)
|
||||||
|
|
||||||
|
def _extract_original_format(self, url, video_id, unlisted_hash=None, jwt=None, api_data=None):
|
||||||
|
# Original/source formats are only available when logged in
|
||||||
|
if not self._get_cookies('https://vimeo.com/').get('vimeo'):
|
||||||
|
return
|
||||||
|
|
||||||
query = {'action': 'load_download_config'}
|
query = {'action': 'load_download_config'}
|
||||||
if unlisted_hash:
|
if unlisted_hash:
|
||||||
query['unlisted_hash'] = unlisted_hash
|
query['unlisted_hash'] = unlisted_hash
|
||||||
download_data = self._download_json(
|
download_data = self._download_json(
|
||||||
url, video_id, fatal=False, query=query,
|
url, video_id, 'Loading download config JSON', fatal=False,
|
||||||
headers={'X-Requested-With': 'XMLHttpRequest'},
|
query=query, headers={'X-Requested-With': 'XMLHttpRequest'},
|
||||||
expected_status=(403, 404)) or {}
|
expected_status=(403, 404)) or {}
|
||||||
source_file = download_data.get('source_file')
|
source_file = download_data.get('source_file')
|
||||||
download_url = try_get(source_file, lambda x: x['download_url'])
|
download_url = try_get(source_file, lambda x: x['download_url'])
|
||||||
@ -261,15 +286,13 @@ def _extract_original_format(self, url, video_id, unlisted_hash=None):
|
|||||||
'quality': 1,
|
'quality': 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt_response = self._download_json(
|
jwt = jwt or traverse_obj(self._download_json(
|
||||||
'https://vimeo.com/_rv/viewer', video_id, note='Downloading jwt token', fatal=False) or {}
|
'https://vimeo.com/_rv/viewer', video_id, 'Downloading jwt token', fatal=False), ('jwt', {str}))
|
||||||
if not jwt_response.get('jwt'):
|
if not jwt:
|
||||||
return
|
return
|
||||||
headers = {'Authorization': 'jwt {}'.format(jwt_response['jwt']), 'Accept': 'application/json'}
|
original_response = api_data or self._call_videos_api(
|
||||||
original_response = self._download_json(
|
video_id, jwt, unlisted_hash, fatal=False, expected_status=(403, 404))
|
||||||
f'https://api.vimeo.com/videos/{video_id}', video_id,
|
for download_data in traverse_obj(original_response, ('download', ..., {dict})):
|
||||||
headers=headers, fatal=False, expected_status=(403, 404)) or {}
|
|
||||||
for download_data in original_response.get('download') or []:
|
|
||||||
download_url = download_data.get('link')
|
download_url = download_data.get('link')
|
||||||
if not download_url or download_data.get('quality') != 'source':
|
if not download_url or download_data.get('quality') != 'source':
|
||||||
continue
|
continue
|
||||||
@ -354,7 +377,7 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'skip': 'No longer available',
|
'skip': 'No longer available',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://player.vimeo.com/video/54469442',
|
'url': 'https://player.vimeo.com/video/54469442',
|
||||||
'md5': '619b811a4417aa4abe78dc653becf511',
|
'md5': '619b811a4417aa4abe78dc653becf511',
|
||||||
'note': 'Videos that embed the url in the player page',
|
'note': 'Videos that embed the url in the player page',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@ -370,6 +393,7 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'params': {
|
'params': {
|
||||||
'format': 'best[protocol=https]',
|
'format': 'best[protocol=https]',
|
||||||
},
|
},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://vimeo.com/68375962',
|
'url': 'http://vimeo.com/68375962',
|
||||||
@ -379,22 +403,23 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'id': '68375962',
|
'id': '68375962',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'youtube-dl password protected test video',
|
'title': 'youtube-dl password protected test video',
|
||||||
'timestamp': 1371200155,
|
'timestamp': 1371214555,
|
||||||
'upload_date': '20130614',
|
'upload_date': '20130614',
|
||||||
|
'release_timestamp': 1371214555,
|
||||||
|
'release_date': '20130614',
|
||||||
'uploader_url': r're:https?://(?:www\.)?vimeo\.com/user18948128',
|
'uploader_url': r're:https?://(?:www\.)?vimeo\.com/user18948128',
|
||||||
'uploader_id': 'user18948128',
|
'uploader_id': 'user18948128',
|
||||||
'uploader': 'Jaime Marquínez Ferrándiz',
|
'uploader': 'Jaime Marquínez Ferrándiz',
|
||||||
'duration': 10,
|
'duration': 10,
|
||||||
'description': 'md5:6173f270cd0c0119f22817204b3eb86c',
|
|
||||||
'thumbnail': 'https://i.vimeocdn.com/video/440665496-b2c5aee2b61089442c794f64113a8e8f7d5763c3e6b3ebfaf696ae6413f8b1f4-d_1280',
|
|
||||||
'view_count': int,
|
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
|
'thumbnail': 'https://i.vimeocdn.com/video/440665496-b2c5aee2b61089442c794f64113a8e8f7d5763c3e6b3ebfaf696ae6413f8b1f4-d_1280',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'format': 'best[protocol=https]',
|
'format': 'best[protocol=https]',
|
||||||
'videopassword': 'youtube-dl',
|
'videopassword': 'youtube-dl',
|
||||||
},
|
},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://vimeo.com/channels/keypeele/75629013',
|
'url': 'http://vimeo.com/channels/keypeele/75629013',
|
||||||
@ -418,29 +443,38 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'like_count': int,
|
'like_count': int,
|
||||||
},
|
},
|
||||||
'params': {'format': 'http-1080p'},
|
'params': {'format': 'http-1080p'},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://vimeo.com/76979871',
|
'url': 'http://vimeo.com/76979871',
|
||||||
'note': 'Video with subtitles',
|
'note': 'Video with subtitles',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '76979871',
|
'id': '76979871',
|
||||||
'ext': 'mov',
|
'ext': 'mp4',
|
||||||
'title': 'The New Vimeo Player (You Know, For Videos)',
|
'title': 'The New Vimeo Player (You Know, For Videos)',
|
||||||
'description': 'md5:2ec900bf97c3f389378a96aee11260ea',
|
'description': str, # FIXME: Dynamic SEO spam description
|
||||||
'timestamp': 1381846109,
|
'timestamp': 1381860509,
|
||||||
'upload_date': '20131015',
|
'upload_date': '20131015',
|
||||||
|
'release_timestamp': 1381860509,
|
||||||
|
'release_date': '20131015',
|
||||||
'uploader_url': r're:https?://(?:www\.)?vimeo\.com/staff',
|
'uploader_url': r're:https?://(?:www\.)?vimeo\.com/staff',
|
||||||
'uploader_id': 'staff',
|
'uploader_id': 'staff',
|
||||||
'uploader': 'Vimeo Staff',
|
'uploader': 'Vimeo',
|
||||||
'duration': 62,
|
'duration': 62,
|
||||||
|
'comment_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'thumbnail': 'https://i.vimeocdn.com/video/452001751-8216e0571c251a09d7a8387550942d89f7f86f6398f8ed886e639b0dd50d3c90-d_1280',
|
||||||
'subtitles': {
|
'subtitles': {
|
||||||
'de': [{'ext': 'vtt'}],
|
'de': 'count:3',
|
||||||
'en': [{'ext': 'vtt'}],
|
'en': 'count:3',
|
||||||
'es': [{'ext': 'vtt'}],
|
'es': 'count:3',
|
||||||
'fr': [{'ext': 'vtt'}],
|
'fr': 'count:3',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'expected_warnings': ['Ignoring subtitle tracks found in the HLS manifest'],
|
'expected_warnings': [
|
||||||
|
'Ignoring subtitle tracks found in the HLS manifest',
|
||||||
|
'Failed to parse XML: not well-formed',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
# from https://www.ouya.tv/game/Pier-Solar-and-the-Great-Architects/
|
# from https://www.ouya.tv/game/Pier-Solar-and-the-Great-Architects/
|
||||||
@ -456,11 +490,12 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'duration': 118,
|
'duration': 118,
|
||||||
'thumbnail': 'https://i.vimeocdn.com/video/478636036-c18440305ef3df9decfb6bf207a61fe39d2d17fa462a96f6f2d93d30492b037d-d_1280',
|
'thumbnail': 'https://i.vimeocdn.com/video/478636036-c18440305ef3df9decfb6bf207a61fe39d2d17fa462a96f6f2d93d30492b037d-d_1280',
|
||||||
},
|
},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
# contains original format
|
# contains Original format
|
||||||
'url': 'https://vimeo.com/33951933',
|
'url': 'https://vimeo.com/33951933',
|
||||||
'md5': '53c688fa95a55bf4b7293d37a89c5c53',
|
# 'md5': '53c688fa95a55bf4b7293d37a89c5c53',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '33951933',
|
'id': '33951933',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@ -476,15 +511,19 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'view_count': int,
|
'view_count': int,
|
||||||
'thumbnail': 'https://i.vimeocdn.com/video/231174622-dd07f015e9221ff529d451e1cc31c982b5d87bfafa48c4189b1da72824ee289a-d_1280',
|
'thumbnail': 'https://i.vimeocdn.com/video/231174622-dd07f015e9221ff529d451e1cc31c982b5d87bfafa48c4189b1da72824ee289a-d_1280',
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
|
'tags': 'count:11',
|
||||||
},
|
},
|
||||||
|
# 'params': {'format': 'Original'},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'note': 'Contains original format not accessible in webpage',
|
'note': 'Contains source format not accessible in webpage',
|
||||||
'url': 'https://vimeo.com/393756517',
|
'url': 'https://vimeo.com/393756517',
|
||||||
'md5': 'c464af248b592190a5ffbb5d33f382b0',
|
# 'md5': 'c464af248b592190a5ffbb5d33f382b0',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '393756517',
|
'id': '393756517',
|
||||||
'ext': 'mov',
|
# 'ext': 'mov',
|
||||||
|
'ext': 'mp4',
|
||||||
'timestamp': 1582642091,
|
'timestamp': 1582642091,
|
||||||
'uploader_id': 'frameworkla',
|
'uploader_id': 'frameworkla',
|
||||||
'title': 'Straight To Hell - Sabrina: Netflix',
|
'title': 'Straight To Hell - Sabrina: Netflix',
|
||||||
@ -495,6 +534,8 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'thumbnail': 'https://i.vimeocdn.com/video/859377297-836494a4ef775e9d4edbace83937d9ad34dc846c688c0c419c0e87f7ab06c4b3-d_1280',
|
'thumbnail': 'https://i.vimeocdn.com/video/859377297-836494a4ef775e9d4edbace83937d9ad34dc846c688c0c419c0e87f7ab06c4b3-d_1280',
|
||||||
'uploader_url': 'https://vimeo.com/frameworkla',
|
'uploader_url': 'https://vimeo.com/frameworkla',
|
||||||
},
|
},
|
||||||
|
# 'params': {'format': 'source'},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
# only available via https://vimeo.com/channels/tributes/6213729 and
|
# only available via https://vimeo.com/channels/tributes/6213729 and
|
||||||
@ -511,16 +552,18 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'channel_id': 'tributes',
|
'channel_id': 'tributes',
|
||||||
'timestamp': 1250886430,
|
'timestamp': 1250886430,
|
||||||
'upload_date': '20090821',
|
'upload_date': '20090821',
|
||||||
'description': 'md5:bdbf314014e58713e6e5b66eb252f4a6',
|
'description': str, # FIXME: Dynamic SEO spam description
|
||||||
'duration': 321,
|
'duration': 321,
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'thumbnail': 'https://i.vimeocdn.com/video/22728298-bfc22146f930de7cf497821c7b0b9f168099201ecca39b00b6bd31fcedfca7a6-d_1280',
|
'thumbnail': 'https://i.vimeocdn.com/video/22728298-bfc22146f930de7cf497821c7b0b9f168099201ecca39b00b6bd31fcedfca7a6-d_1280',
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
|
'tags': ['[the shining', 'vimeohq', 'cv', 'vimeo tribute]'],
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
# redirects to ondemand extractor and should be passed through it
|
# redirects to ondemand extractor and should be passed through it
|
||||||
@ -543,28 +586,23 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'skip': 'this page is no longer available.',
|
'skip': 'this page is no longer available.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://player.vimeo.com/video/68375962',
|
'url': 'https://player.vimeo.com/video/68375962',
|
||||||
'md5': 'aaf896bdb7ddd6476df50007a0ac0ae7',
|
'md5': 'aaf896bdb7ddd6476df50007a0ac0ae7',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '68375962',
|
'id': '68375962',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'youtube-dl password protected test video',
|
'title': 'youtube-dl password protected test video',
|
||||||
'timestamp': 1371200155,
|
|
||||||
'upload_date': '20130614',
|
|
||||||
'uploader_url': r're:https?://(?:www\.)?vimeo\.com/user18948128',
|
'uploader_url': r're:https?://(?:www\.)?vimeo\.com/user18948128',
|
||||||
'uploader_id': 'user18948128',
|
'uploader_id': 'user18948128',
|
||||||
'uploader': 'Jaime Marquínez Ferrándiz',
|
'uploader': 'Jaime Marquínez Ferrándiz',
|
||||||
'duration': 10,
|
'duration': 10,
|
||||||
'description': 'md5:6173f270cd0c0119f22817204b3eb86c',
|
|
||||||
'thumbnail': 'https://i.vimeocdn.com/video/440665496-b2c5aee2b61089442c794f64113a8e8f7d5763c3e6b3ebfaf696ae6413f8b1f4-d_1280',
|
'thumbnail': 'https://i.vimeocdn.com/video/440665496-b2c5aee2b61089442c794f64113a8e8f7d5763c3e6b3ebfaf696ae6413f8b1f4-d_1280',
|
||||||
'view_count': int,
|
|
||||||
'comment_count': int,
|
|
||||||
'like_count': int,
|
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'format': 'best[protocol=https]',
|
'format': 'best[protocol=https]',
|
||||||
'videopassword': 'youtube-dl',
|
'videopassword': 'youtube-dl',
|
||||||
},
|
},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://vimeo.com/moogaloop.swf?clip_id=2539741',
|
'url': 'http://vimeo.com/moogaloop.swf?clip_id=2539741',
|
||||||
@ -592,7 +630,7 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'title': "youtube-dl test video '' ä↭𝕐-BaW jenozKc",
|
'title': "youtube-dl test video '' ä↭𝕐-BaW jenozKc",
|
||||||
'uploader': 'Philipp Hagemeister',
|
'uploader': 'Philipp Hagemeister',
|
||||||
'uploader_id': 'user20132939',
|
'uploader_id': 'user20132939',
|
||||||
'description': 'md5:fa7b6c6d8db0bdc353893df2f111855b',
|
'description': str, # FIXME: Dynamic SEO spam description
|
||||||
'upload_date': '20150209',
|
'upload_date': '20150209',
|
||||||
'timestamp': 1423518307,
|
'timestamp': 1423518307,
|
||||||
'thumbnail': 'https://i.vimeocdn.com/video/default_1280',
|
'thumbnail': 'https://i.vimeocdn.com/video/default_1280',
|
||||||
@ -606,6 +644,7 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'format': 'best[protocol=https]',
|
'format': 'best[protocol=https]',
|
||||||
'videopassword': 'youtube-dl',
|
'videopassword': 'youtube-dl',
|
||||||
},
|
},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
# source file returns 403: Forbidden
|
# source file returns 403: Forbidden
|
||||||
@ -633,11 +672,13 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'release_date': '20160329',
|
'release_date': '20160329',
|
||||||
},
|
},
|
||||||
'params': {'skip_download': True},
|
'params': {'skip_download': True},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'https://vimeo.com/138909882',
|
'url': 'https://vimeo.com/138909882',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '138909882',
|
'id': '138909882',
|
||||||
|
# 'ext': 'm4v',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Eastnor Castle 2015 Firework Champions - The Promo!',
|
'title': 'Eastnor Castle 2015 Firework Champions - The Promo!',
|
||||||
'description': 'md5:5967e090768a831488f6e74b7821b3c1',
|
'description': 'md5:5967e090768a831488f6e74b7821b3c1',
|
||||||
@ -645,11 +686,19 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'uploader': 'Firework Champions',
|
'uploader': 'Firework Champions',
|
||||||
'upload_date': '20150910',
|
'upload_date': '20150910',
|
||||||
'timestamp': 1441901895,
|
'timestamp': 1441901895,
|
||||||
|
'thumbnail': 'https://i.vimeocdn.com/video/534715882-6ff8e4660cbf2fea68282876d8d44f318825dfe572cc4016e73b3266eac8ae3a-d_1280',
|
||||||
|
'uploader_url': 'https://vimeo.com/fireworkchampions',
|
||||||
|
'tags': 'count:6',
|
||||||
|
'duration': 229,
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'comment_count': int,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
'format': 'Original',
|
# 'format': 'source',
|
||||||
},
|
},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'https://vimeo.com/channels/staffpicks/143603739',
|
'url': 'https://vimeo.com/channels/staffpicks/143603739',
|
||||||
@ -670,8 +719,10 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'like_count': int,
|
'like_count': int,
|
||||||
'uploader_url': 'https://vimeo.com/karimhd',
|
'uploader_url': 'https://vimeo.com/karimhd',
|
||||||
'channel_url': 'https://vimeo.com/channels/staffpicks',
|
'channel_url': 'https://vimeo.com/channels/staffpicks',
|
||||||
|
'tags': 'count:6',
|
||||||
},
|
},
|
||||||
'params': {'skip_download': 'm3u8'},
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
# requires passing unlisted_hash(a52724358e) to load_download_config request
|
# requires passing unlisted_hash(a52724358e) to load_download_config request
|
||||||
@ -701,6 +752,7 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
# chapters must be sorted, see: https://github.com/yt-dlp/yt-dlp/issues/5308
|
# chapters must be sorted, see: https://github.com/yt-dlp/yt-dlp/issues/5308
|
||||||
@ -735,6 +787,48 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
|||||||
},
|
},
|
||||||
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
# vimeo.com URL with unlisted hash and Original format
|
||||||
|
'url': 'https://vimeo.com/144579403/ec02229140',
|
||||||
|
# 'md5': '6b662c2884e0373183fbde2a0d15cb78',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '144579403',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'SALESMANSHIP',
|
||||||
|
'description': 'md5:4338302f347a1ff8841b4a3aecaa09f0',
|
||||||
|
'uploader': 'Off the Picture Pictures',
|
||||||
|
'uploader_id': 'offthepicturepictures',
|
||||||
|
'uploader_url': 'https://vimeo.com/offthepicturepictures',
|
||||||
|
'duration': 669,
|
||||||
|
'upload_date': '20151104',
|
||||||
|
'timestamp': 1446607180,
|
||||||
|
'release_date': '20151104',
|
||||||
|
'release_timestamp': 1446607180,
|
||||||
|
'like_count': int,
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'thumbnail': r're:https://i\.vimeocdn\.com/video/1018638656-[\da-f]+-d_1280',
|
||||||
|
},
|
||||||
|
# 'params': {'format': 'Original'},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# player.vimeo.com URL with source format
|
||||||
|
'url': 'https://player.vimeo.com/video/859028877',
|
||||||
|
# 'md5': '19ca3d2463441dee2d2f0671ac2916a2',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '859028877',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Ariana Grande - Honeymoon Avenue (Live from London)',
|
||||||
|
'uploader': 'Raja Virdi',
|
||||||
|
'uploader_id': 'rajavirdi',
|
||||||
|
'uploader_url': 'https://vimeo.com/rajavirdi',
|
||||||
|
'duration': 309,
|
||||||
|
'thumbnail': r're:https://i\.vimeocdn\.com/video/1716727772-[\da-f]+-d_1280',
|
||||||
|
},
|
||||||
|
# 'params': {'format': 'source'},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
# user playlist alias -> https://vimeo.com/258705797
|
# user playlist alias -> https://vimeo.com/258705797
|
||||||
'url': 'https://vimeo.com/user26785108/newspiritualguide',
|
'url': 'https://vimeo.com/user26785108/newspiritualguide',
|
||||||
@ -768,16 +862,6 @@ def _verify_player_video_password(self, url, video_id, headers):
|
|||||||
raise ExtractorError('Wrong video password', expected=True)
|
raise ExtractorError('Wrong video password', expected=True)
|
||||||
return checked
|
return checked
|
||||||
|
|
||||||
def _call_videos_api(self, video_id, jwt_token, unlisted_hash=None):
|
|
||||||
return self._download_json(
|
|
||||||
join_nonempty(f'https://api.vimeo.com/videos/{video_id}', unlisted_hash, delim=':'),
|
|
||||||
video_id, 'Downloading API JSON', headers={
|
|
||||||
'Authorization': f'jwt {jwt_token}',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
}, query={
|
|
||||||
'fields': 'config_url,created_time,description,license,metadata.connections.comments.total,metadata.connections.likes.total,release_time,stats.plays',
|
|
||||||
})
|
|
||||||
|
|
||||||
def _extract_from_api(self, video_id, unlisted_hash=None):
|
def _extract_from_api(self, video_id, unlisted_hash=None):
|
||||||
viewer = self._download_json(
|
viewer = self._download_json(
|
||||||
'https://vimeo.com/_next/viewer', video_id, 'Downloading viewer info')
|
'https://vimeo.com/_next/viewer', video_id, 'Downloading viewer info')
|
||||||
@ -798,6 +882,11 @@ def _extract_from_api(self, video_id, unlisted_hash=None):
|
|||||||
|
|
||||||
info = self._parse_config(self._download_json(
|
info = self._parse_config(self._download_json(
|
||||||
video['config_url'], video_id), video_id)
|
video['config_url'], video_id), video_id)
|
||||||
|
source_format = self._extract_original_format(
|
||||||
|
f'https://vimeo.com/{video_id}', video_id, unlisted_hash, jwt=viewer['jwt'], api_data=video)
|
||||||
|
if source_format:
|
||||||
|
info['formats'].append(source_format)
|
||||||
|
|
||||||
get_timestamp = lambda x: parse_iso8601(video.get(x + '_time'))
|
get_timestamp = lambda x: parse_iso8601(video.get(x + '_time'))
|
||||||
info.update({
|
info.update({
|
||||||
'description': video.get('description'),
|
'description': video.get('description'),
|
||||||
@ -899,7 +988,12 @@ def _real_extract(self, url):
|
|||||||
if config.get('view') == 4:
|
if config.get('view') == 4:
|
||||||
config = self._verify_player_video_password(
|
config = self._verify_player_video_password(
|
||||||
redirect_url, video_id, headers)
|
redirect_url, video_id, headers)
|
||||||
return self._parse_config(config, video_id)
|
info = self._parse_config(config, video_id)
|
||||||
|
source_format = self._extract_original_format(
|
||||||
|
f'https://vimeo.com/{video_id}', video_id, unlisted_hash)
|
||||||
|
if source_format:
|
||||||
|
info['formats'].append(source_format)
|
||||||
|
return info
|
||||||
|
|
||||||
vimeo_config = self._extract_vimeo_config(webpage, video_id, default=None)
|
vimeo_config = self._extract_vimeo_config(webpage, video_id, default=None)
|
||||||
if vimeo_config:
|
if vimeo_config:
|
||||||
@ -1269,6 +1363,20 @@ class VimeoReviewIE(VimeoBaseInfoExtractor):
|
|||||||
IE_DESC = 'Review pages on vimeo'
|
IE_DESC = 'Review pages on vimeo'
|
||||||
_VALID_URL = r'https?://vimeo\.com/(?P<user>[^/?#]+)/review/(?P<id>\d+)/(?P<hash>[\da-f]{10})'
|
_VALID_URL = r'https?://vimeo\.com/(?P<user>[^/?#]+)/review/(?P<id>\d+)/(?P<hash>[\da-f]{10})'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://vimeo.com/user170863801/review/996447483/a316d6ed8d',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '996447483',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Rodeo day 1-_2',
|
||||||
|
'uploader': 'BROADKAST',
|
||||||
|
'uploader_id': 'user170863801',
|
||||||
|
'uploader_url': 'https://vimeo.com/user170863801',
|
||||||
|
'duration': 30,
|
||||||
|
'thumbnail': 'https://i.vimeocdn.com/video/1912612821-09a43bd2e75c203d503aed89de7534f28fc4474a48f59c51999716931a246af5-d_1280',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
'expected_warnings': ['Failed to parse XML'],
|
||||||
|
}, {
|
||||||
'url': 'https://vimeo.com/user21297594/review/75524534/3c257a1b5d',
|
'url': 'https://vimeo.com/user21297594/review/75524534/3c257a1b5d',
|
||||||
'md5': 'c507a72f780cacc12b2248bb4006d253',
|
'md5': 'c507a72f780cacc12b2248bb4006d253',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@ -1282,6 +1390,7 @@ class VimeoReviewIE(VimeoBaseInfoExtractor):
|
|||||||
'thumbnail': 'https://i.vimeocdn.com/video/450115033-43303819d9ebe24c2630352e18b7056d25197d09b3ae901abdac4c4f1d68de71-d_1280',
|
'thumbnail': 'https://i.vimeocdn.com/video/450115033-43303819d9ebe24c2630352e18b7056d25197d09b3ae901abdac4c4f1d68de71-d_1280',
|
||||||
'uploader_url': 'https://vimeo.com/user21297594',
|
'uploader_url': 'https://vimeo.com/user21297594',
|
||||||
},
|
},
|
||||||
|
'skip': '404 Not Found',
|
||||||
}, {
|
}, {
|
||||||
'note': 'video player needs Referer',
|
'note': 'video player needs Referer',
|
||||||
'url': 'https://vimeo.com/user22258446/review/91613211/13f927e053',
|
'url': 'https://vimeo.com/user22258446/review/91613211/13f927e053',
|
||||||
@ -1316,6 +1425,7 @@ def _real_extract(self, url):
|
|||||||
user, video_id, review_hash = self._match_valid_url(url).group('user', 'id', 'hash')
|
user, video_id, review_hash = self._match_valid_url(url).group('user', 'id', 'hash')
|
||||||
data_url = f'https://vimeo.com/{user}/review/data/{video_id}/{review_hash}'
|
data_url = f'https://vimeo.com/{user}/review/data/{video_id}/{review_hash}'
|
||||||
data = self._download_json(data_url, video_id)
|
data = self._download_json(data_url, video_id)
|
||||||
|
viewer = {}
|
||||||
if data.get('isLocked') is True:
|
if data.get('isLocked') is True:
|
||||||
video_password = self._get_video_password()
|
video_password = self._get_video_password()
|
||||||
viewer = self._download_json(
|
viewer = self._download_json(
|
||||||
@ -1327,8 +1437,8 @@ def _real_extract(self, url):
|
|||||||
config = self._download_json(config_url, video_id)
|
config = self._download_json(config_url, video_id)
|
||||||
info_dict = self._parse_config(config, video_id)
|
info_dict = self._parse_config(config, video_id)
|
||||||
source_format = self._extract_original_format(
|
source_format = self._extract_original_format(
|
||||||
f'https://vimeo.com/{user}/review/{video_id}/{review_hash}/action', video_id,
|
f'https://vimeo.com/{user}/review/{video_id}/{review_hash}/action',
|
||||||
unlisted_hash=traverse_obj(config_url, ({parse_qs}, 'h', -1)))
|
video_id, unlisted_hash=clip_data.get('unlistedHash'), jwt=viewer.get('jwt'))
|
||||||
if source_format:
|
if source_format:
|
||||||
info_dict['formats'].append(source_format)
|
info_dict['formats'].append(source_format)
|
||||||
info_dict['description'] = clean_html(clip_data.get('description'))
|
info_dict['description'] = clean_html(clip_data.get('description'))
|
||||||
|
@ -90,7 +90,7 @@ def _real_extract(self, url):
|
|||||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, 'mp4')
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, 'mp4')
|
||||||
|
|
||||||
for key, value in video_data.items():
|
for key, value in video_data.items():
|
||||||
mobj = re.match(r'^subtitle_(?P<lang>[^_]+)_(?P<ext>(vtt|srt))', key)
|
mobj = re.match(r'subtitle_(?P<lang>[^_]+)_(?P<ext>(vtt|srt))', key)
|
||||||
if not mobj:
|
if not mobj:
|
||||||
continue
|
continue
|
||||||
subtitles.setdefault(mobj.group('lang'), []).append({
|
subtitles.setdefault(mobj.group('lang'), []).append({
|
||||||
|
@ -27,8 +27,9 @@
|
|||||||
|
|
||||||
class WeverseBaseIE(InfoExtractor):
|
class WeverseBaseIE(InfoExtractor):
|
||||||
_NETRC_MACHINE = 'weverse'
|
_NETRC_MACHINE = 'weverse'
|
||||||
_ACCOUNT_API_BASE = 'https://accountapi.weverse.io/web/api/v2'
|
_ACCOUNT_API_BASE = 'https://accountapi.weverse.io/web/api'
|
||||||
_API_HEADERS = {
|
_API_HEADERS = {
|
||||||
|
'Accept': 'application/json',
|
||||||
'Referer': 'https://weverse.io/',
|
'Referer': 'https://weverse.io/',
|
||||||
'WEV-device-Id': str(uuid.uuid4()),
|
'WEV-device-Id': str(uuid.uuid4()),
|
||||||
}
|
}
|
||||||
@ -39,14 +40,14 @@ def _perform_login(self, username, password):
|
|||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'x-acc-app-secret': '5419526f1c624b38b10787e5c10b2a7a',
|
'x-acc-app-secret': '5419526f1c624b38b10787e5c10b2a7a',
|
||||||
'x-acc-app-version': '2.2.6',
|
'x-acc-app-version': '3.3.6',
|
||||||
'x-acc-language': 'en',
|
'x-acc-language': 'en',
|
||||||
'x-acc-service-id': 'weverse',
|
'x-acc-service-id': 'weverse',
|
||||||
'x-acc-trace-id': str(uuid.uuid4()),
|
'x-acc-trace-id': str(uuid.uuid4()),
|
||||||
'x-clog-user-device-id': str(uuid.uuid4()),
|
'x-clog-user-device-id': str(uuid.uuid4()),
|
||||||
}
|
}
|
||||||
valid_username = traverse_obj(self._download_json(
|
valid_username = traverse_obj(self._download_json(
|
||||||
f'{self._ACCOUNT_API_BASE}/signup/email/status', None, note='Checking username',
|
f'{self._ACCOUNT_API_BASE}/v2/signup/email/status', None, note='Checking username',
|
||||||
query={'email': username}, headers=headers, expected_status=(400, 404)), 'hasPassword')
|
query={'email': username}, headers=headers, expected_status=(400, 404)), 'hasPassword')
|
||||||
if not valid_username:
|
if not valid_username:
|
||||||
raise ExtractorError('Invalid username provided', expected=True)
|
raise ExtractorError('Invalid username provided', expected=True)
|
||||||
@ -54,8 +55,9 @@ def _perform_login(self, username, password):
|
|||||||
headers['content-type'] = 'application/json'
|
headers['content-type'] = 'application/json'
|
||||||
try:
|
try:
|
||||||
auth = self._download_json(
|
auth = self._download_json(
|
||||||
f'{self._ACCOUNT_API_BASE}/auth/token/by-credentials', None, data=json.dumps({
|
f'{self._ACCOUNT_API_BASE}/v3/auth/token/by-credentials', None, data=json.dumps({
|
||||||
'email': username,
|
'email': username,
|
||||||
|
'otpSessionId': 'BY_PASS',
|
||||||
'password': password,
|
'password': password,
|
||||||
}, separators=(',', ':')).encode(), headers=headers, note='Logging in')
|
}, separators=(',', ':')).encode(), headers=headers, note='Logging in')
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
@ -78,8 +80,10 @@ def _call_api(self, ep, video_id, data=None, note='Downloading API JSON'):
|
|||||||
# From https://ssl.pstatic.net/static/wevweb/2_3_2_11101725/public/static/js/main.e206f7c1.js:
|
# From https://ssl.pstatic.net/static/wevweb/2_3_2_11101725/public/static/js/main.e206f7c1.js:
|
||||||
key = b'1b9cb6378d959b45714bec49971ade22e6e24e42'
|
key = b'1b9cb6378d959b45714bec49971ade22e6e24e42'
|
||||||
api_path = update_url_query(ep, {
|
api_path = update_url_query(ep, {
|
||||||
|
# 'gcc': 'US',
|
||||||
'appId': 'be4d79eb8fc7bd008ee82c8ec4ff6fd4',
|
'appId': 'be4d79eb8fc7bd008ee82c8ec4ff6fd4',
|
||||||
'language': 'en',
|
'language': 'en',
|
||||||
|
'os': 'WEB',
|
||||||
'platform': 'WEB',
|
'platform': 'WEB',
|
||||||
'wpf': 'pc',
|
'wpf': 'pc',
|
||||||
})
|
})
|
||||||
@ -152,7 +156,7 @@ def _parse_post_meta(self, metadata):
|
|||||||
'description': ((('extension', 'mediaInfo', 'body'), 'body'), {str}),
|
'description': ((('extension', 'mediaInfo', 'body'), 'body'), {str}),
|
||||||
'uploader': ('author', 'profileName', {str}),
|
'uploader': ('author', 'profileName', {str}),
|
||||||
'uploader_id': ('author', 'memberId', {str}),
|
'uploader_id': ('author', 'memberId', {str}),
|
||||||
'creator': ('community', 'communityName', {str}),
|
'creators': ('community', 'communityName', {str}, all),
|
||||||
'channel_id': (('community', 'author'), 'communityId', {str_or_none}),
|
'channel_id': (('community', 'author'), 'communityId', {str_or_none}),
|
||||||
'duration': ('extension', 'video', 'playTime', {float_or_none}),
|
'duration': ('extension', 'video', 'playTime', {float_or_none}),
|
||||||
'timestamp': ('publishedAt', {lambda x: int_or_none(x, 1000)}),
|
'timestamp': ('publishedAt', {lambda x: int_or_none(x, 1000)}),
|
||||||
@ -196,7 +200,7 @@ class WeverseIE(WeverseBaseIE):
|
|||||||
'channel': 'billlie',
|
'channel': 'billlie',
|
||||||
'channel_id': '72',
|
'channel_id': '72',
|
||||||
'channel_url': 'https://weverse.io/billlie',
|
'channel_url': 'https://weverse.io/billlie',
|
||||||
'creator': 'Billlie',
|
'creators': ['Billlie'],
|
||||||
'timestamp': 1666262062,
|
'timestamp': 1666262062,
|
||||||
'upload_date': '20221020',
|
'upload_date': '20221020',
|
||||||
'release_timestamp': 1666262058,
|
'release_timestamp': 1666262058,
|
||||||
@ -222,7 +226,7 @@ class WeverseIE(WeverseBaseIE):
|
|||||||
'channel': 'lesserafim',
|
'channel': 'lesserafim',
|
||||||
'channel_id': '47',
|
'channel_id': '47',
|
||||||
'channel_url': 'https://weverse.io/lesserafim',
|
'channel_url': 'https://weverse.io/lesserafim',
|
||||||
'creator': 'LE SSERAFIM',
|
'creators': ['LE SSERAFIM'],
|
||||||
'timestamp': 1659353400,
|
'timestamp': 1659353400,
|
||||||
'upload_date': '20220801',
|
'upload_date': '20220801',
|
||||||
'release_timestamp': 1659353400,
|
'release_timestamp': 1659353400,
|
||||||
@ -286,7 +290,7 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
elif live_status == 'is_live':
|
elif live_status == 'is_live':
|
||||||
video_info = self._call_api(
|
video_info = self._call_api(
|
||||||
f'/video/v1.0/lives/{api_video_id}/playInfo?preview.format=json&preview.version=v2',
|
f'/video/v1.2/lives/{api_video_id}/playInfo?preview.format=json&preview.version=v2',
|
||||||
video_id, note='Downloading live JSON')
|
video_id, note='Downloading live JSON')
|
||||||
playback = self._parse_json(video_info['lipPlayback'], video_id)
|
playback = self._parse_json(video_info['lipPlayback'], video_id)
|
||||||
m3u8_url = traverse_obj(playback, (
|
m3u8_url = traverse_obj(playback, (
|
||||||
@ -302,7 +306,7 @@ def _real_extract(self, url):
|
|||||||
else:
|
else:
|
||||||
infra_video_id = post['extension']['video']['infraVideoId']
|
infra_video_id = post['extension']['video']['infraVideoId']
|
||||||
in_key = self._call_api(
|
in_key = self._call_api(
|
||||||
f'/video/v1.0/vod/{api_video_id}/inKey?preview=false', video_id,
|
f'/video/v1.1/vod/{api_video_id}/inKey?preview=false', video_id,
|
||||||
data=b'{}', note='Downloading VOD API key')['inKey']
|
data=b'{}', note='Downloading VOD API key')['inKey']
|
||||||
|
|
||||||
video_info = self._download_json(
|
video_info = self._download_json(
|
||||||
@ -347,7 +351,6 @@ class WeverseMediaIE(WeverseBaseIE):
|
|||||||
_VALID_URL = r'https?://(?:www\.|m\.)?weverse\.io/(?P<artist>[^/?#]+)/media/(?P<id>[\d-]+)'
|
_VALID_URL = r'https?://(?:www\.|m\.)?weverse\.io/(?P<artist>[^/?#]+)/media/(?P<id>[\d-]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://weverse.io/billlie/media/4-116372884',
|
'url': 'https://weverse.io/billlie/media/4-116372884',
|
||||||
'md5': '8efc9cfd61b2f25209eb1a5326314d28',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'e-C9wLSQs6o',
|
'id': 'e-C9wLSQs6o',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@ -358,8 +361,9 @@ class WeverseMediaIE(WeverseBaseIE):
|
|||||||
'channel_url': 'https://www.youtube.com/channel/UCyc9sUCxELTDK9vELO5Fzeg',
|
'channel_url': 'https://www.youtube.com/channel/UCyc9sUCxELTDK9vELO5Fzeg',
|
||||||
'uploader': 'Billlie',
|
'uploader': 'Billlie',
|
||||||
'uploader_id': '@Billlie',
|
'uploader_id': '@Billlie',
|
||||||
'uploader_url': 'http://www.youtube.com/@Billlie',
|
'uploader_url': 'https://www.youtube.com/@Billlie',
|
||||||
'upload_date': '20230403',
|
'upload_date': '20230403',
|
||||||
|
'timestamp': 1680533992,
|
||||||
'duration': 211,
|
'duration': 211,
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
'playable_in_embed': True,
|
'playable_in_embed': True,
|
||||||
@ -372,6 +376,8 @@ class WeverseMediaIE(WeverseBaseIE):
|
|||||||
'thumbnail': 'https://i.ytimg.com/vi/e-C9wLSQs6o/maxresdefault.jpg',
|
'thumbnail': 'https://i.ytimg.com/vi/e-C9wLSQs6o/maxresdefault.jpg',
|
||||||
'categories': ['Entertainment'],
|
'categories': ['Entertainment'],
|
||||||
'tags': 'count:7',
|
'tags': 'count:7',
|
||||||
|
'channel_is_verified': True,
|
||||||
|
'heatmap': 'count:100',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://weverse.io/billlie/media/3-102914520',
|
'url': 'https://weverse.io/billlie/media/3-102914520',
|
||||||
@ -386,7 +392,7 @@ class WeverseMediaIE(WeverseBaseIE):
|
|||||||
'channel': 'billlie',
|
'channel': 'billlie',
|
||||||
'channel_id': '72',
|
'channel_id': '72',
|
||||||
'channel_url': 'https://weverse.io/billlie',
|
'channel_url': 'https://weverse.io/billlie',
|
||||||
'creator': 'Billlie',
|
'creators': ['Billlie'],
|
||||||
'timestamp': 1662174000,
|
'timestamp': 1662174000,
|
||||||
'upload_date': '20220903',
|
'upload_date': '20220903',
|
||||||
'release_timestamp': 1662174000,
|
'release_timestamp': 1662174000,
|
||||||
@ -432,7 +438,7 @@ class WeverseMomentIE(WeverseBaseIE):
|
|||||||
'uploader_id': '66a07e164b56a696ee71c99315ffe27b',
|
'uploader_id': '66a07e164b56a696ee71c99315ffe27b',
|
||||||
'channel': 'secretnumber',
|
'channel': 'secretnumber',
|
||||||
'channel_id': '56',
|
'channel_id': '56',
|
||||||
'creator': 'SECRET NUMBER',
|
'creators': ['SECRET NUMBER'],
|
||||||
'duration': 10,
|
'duration': 10,
|
||||||
'upload_date': '20230405',
|
'upload_date': '20230405',
|
||||||
'timestamp': 1680653968,
|
'timestamp': 1680653968,
|
||||||
@ -441,7 +447,6 @@ class WeverseMomentIE(WeverseBaseIE):
|
|||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'availability': 'needs_auth',
|
'availability': 'needs_auth',
|
||||||
},
|
},
|
||||||
'skip': 'Moment has expired',
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@ -571,7 +576,7 @@ class WeverseLiveIE(WeverseBaseIE):
|
|||||||
'channel': 'purplekiss',
|
'channel': 'purplekiss',
|
||||||
'channel_id': '35',
|
'channel_id': '35',
|
||||||
'channel_url': 'https://weverse.io/purplekiss',
|
'channel_url': 'https://weverse.io/purplekiss',
|
||||||
'creator': 'PURPLE KISS',
|
'creators': ['PURPLE KISS'],
|
||||||
'timestamp': 1680780892,
|
'timestamp': 1680780892,
|
||||||
'upload_date': '20230406',
|
'upload_date': '20230406',
|
||||||
'release_timestamp': 1680780883,
|
'release_timestamp': 1680780883,
|
||||||
@ -584,6 +589,31 @@ class WeverseLiveIE(WeverseBaseIE):
|
|||||||
'live_status': 'is_live',
|
'live_status': 'is_live',
|
||||||
},
|
},
|
||||||
'skip': 'Livestream has ended',
|
'skip': 'Livestream has ended',
|
||||||
|
}, {
|
||||||
|
'url': 'https://weverse.io/lesserafim',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4-181521628',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': r're:심심해서요',
|
||||||
|
'description': '',
|
||||||
|
'uploader': '채채🤎',
|
||||||
|
'uploader_id': 'd49b8b06f3cc1d92d655b25ab27ac2e7',
|
||||||
|
'channel': 'lesserafim',
|
||||||
|
'channel_id': '47',
|
||||||
|
'creators': ['LE SSERAFIM'],
|
||||||
|
'channel_url': 'https://weverse.io/lesserafim',
|
||||||
|
'timestamp': 1728570273,
|
||||||
|
'upload_date': '20241010',
|
||||||
|
'release_timestamp': 1728570264,
|
||||||
|
'release_date': '20241010',
|
||||||
|
'thumbnail': r're:https://phinf\.wevpstatic\.net/.+\.png',
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'availability': 'needs_auth',
|
||||||
|
'live_status': 'is_live',
|
||||||
|
},
|
||||||
|
'skip': 'Livestream has ended',
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://weverse.io/billlie/',
|
'url': 'https://weverse.io/billlie/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
determine_ext,
|
determine_ext,
|
||||||
|
filter_dict,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_qs,
|
parse_qs,
|
||||||
@ -25,16 +26,25 @@ class WistiaBaseIE(InfoExtractor):
|
|||||||
|
|
||||||
def _download_embed_config(self, config_type, config_id, referer):
|
def _download_embed_config(self, config_type, config_id, referer):
|
||||||
base_url = self._EMBED_BASE_URL + f'{config_type}/{config_id}'
|
base_url = self._EMBED_BASE_URL + f'{config_type}/{config_id}'
|
||||||
|
video_password = self.get_param('videopassword')
|
||||||
embed_config = self._download_json(
|
embed_config = self._download_json(
|
||||||
base_url + '.json', config_id, headers={
|
base_url + '.json', config_id, headers={
|
||||||
'Referer': referer if referer.startswith('http') else base_url, # Some videos require this.
|
'Referer': referer if referer.startswith('http') else base_url, # Some videos require this.
|
||||||
})
|
}, query=filter_dict({'password': video_password}))
|
||||||
|
|
||||||
error = traverse_obj(embed_config, 'error')
|
error = traverse_obj(embed_config, 'error')
|
||||||
if error:
|
if error:
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
f'Error while getting the playlist: {error}', expected=True)
|
f'Error while getting the playlist: {error}', expected=True)
|
||||||
|
|
||||||
|
if traverse_obj(embed_config, (
|
||||||
|
'media', ('embed_options', 'embedOptions'), 'plugin',
|
||||||
|
'passwordProtectedVideo', 'on', any)) == 'true':
|
||||||
|
if video_password:
|
||||||
|
raise ExtractorError('Invalid video password', expected=True)
|
||||||
|
raise ExtractorError(
|
||||||
|
'This content is password-protected. Use the --video-password option', expected=True)
|
||||||
|
|
||||||
return embed_config
|
return embed_config
|
||||||
|
|
||||||
def _get_real_ext(self, url):
|
def _get_real_ext(self, url):
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
|
import base64
|
||||||
import math
|
import math
|
||||||
|
import time
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import InAdvancePagedList, str_or_none, traverse_obj, try_call
|
from .videa import VideaIE
|
||||||
|
from ..utils import (
|
||||||
|
InAdvancePagedList,
|
||||||
|
int_or_none,
|
||||||
|
str_or_none,
|
||||||
|
traverse_obj,
|
||||||
|
try_call,
|
||||||
|
update_url_query,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class XimalayaBaseIE(InfoExtractor):
|
class XimalayaBaseIE(InfoExtractor):
|
||||||
@ -11,7 +21,7 @@ class XimalayaBaseIE(InfoExtractor):
|
|||||||
class XimalayaIE(XimalayaBaseIE):
|
class XimalayaIE(XimalayaBaseIE):
|
||||||
IE_NAME = 'ximalaya'
|
IE_NAME = 'ximalaya'
|
||||||
IE_DESC = '喜马拉雅FM'
|
IE_DESC = '喜马拉雅FM'
|
||||||
_VALID_URL = r'https?://(?:www\.|m\.)?ximalaya\.com/(:?(?P<uid>\d+)/)?sound/(?P<id>[0-9]+)'
|
_VALID_URL = r'https?://(?:www\.|m\.)?ximalaya\.com/(?:(?P<uid>\d+)/)?sound/(?P<id>[0-9]+)'
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
'url': 'http://www.ximalaya.com/sound/47740352/',
|
'url': 'http://www.ximalaya.com/sound/47740352/',
|
||||||
@ -71,23 +81,92 @@ class XimalayaIE(XimalayaBaseIE):
|
|||||||
'like_count': int,
|
'like_count': int,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
# VIP-restricted audio
|
||||||
|
'url': 'https://www.ximalaya.com/sound/562111701',
|
||||||
|
'only_matching': True,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _decrypt_filename(file_id, seed):
|
||||||
|
cgstr = ''
|
||||||
|
key = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/\\:._-1234567890'
|
||||||
|
for _ in key:
|
||||||
|
seed = float(int(211 * seed + 30031) % 65536)
|
||||||
|
r = int(seed / 65536 * len(key))
|
||||||
|
cgstr += key[r]
|
||||||
|
key = key.replace(key[r], '')
|
||||||
|
parts = file_id.split('*')
|
||||||
|
filename = ''.join(cgstr[int(part)] for part in parts if part.isdecimal())
|
||||||
|
if not filename.startswith('/'):
|
||||||
|
filename = '/' + filename
|
||||||
|
return filename
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _decrypt_url_params(encrypted_params):
|
||||||
|
params = VideaIE.rc4(
|
||||||
|
base64.b64decode(encrypted_params), 'xkt3a41psizxrh9l').split('-')
|
||||||
|
# sign, token, timestamp
|
||||||
|
return params[1], params[2], params[3]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
scheme = 'https' if url.startswith('https') else 'http'
|
scheme = 'https' if url.startswith('https') else 'http'
|
||||||
|
|
||||||
audio_id = self._match_id(url)
|
audio_id = self._match_id(url)
|
||||||
audio_info_file = f'{scheme}://m.ximalaya.com/tracks/{audio_id}.json'
|
|
||||||
audio_info = self._download_json(
|
audio_info = self._download_json(
|
||||||
audio_info_file, audio_id,
|
f'{scheme}://m.ximalaya.com/tracks/{audio_id}.json', audio_id,
|
||||||
f'Downloading info json {audio_info_file}', 'Unable to download info file')
|
'Downloading info json', 'Unable to download info file')
|
||||||
|
|
||||||
formats = [{
|
formats = []
|
||||||
|
# NOTE: VIP-restricted audio
|
||||||
|
if audio_info.get('is_paid'):
|
||||||
|
ts = int(time.time())
|
||||||
|
vip_info = self._download_json(
|
||||||
|
f'{scheme}://mpay.ximalaya.com/mobile/track/pay/{audio_id}/{ts}',
|
||||||
|
audio_id, 'Downloading VIP info json', 'Unable to download VIP info file',
|
||||||
|
query={'device': 'pc', 'isBackend': 'true', '_': ts})
|
||||||
|
filename = self._decrypt_filename(vip_info['fileId'], vip_info['seed'])
|
||||||
|
sign, token, timestamp = self._decrypt_url_params(vip_info['ep'])
|
||||||
|
vip_url = update_url_query(
|
||||||
|
f'{vip_info["domain"]}/download/{vip_info["apiVersion"]}{filename}', {
|
||||||
|
'sign': sign,
|
||||||
|
'token': token,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'buy_key': vip_info['buyKey'],
|
||||||
|
'duration': vip_info['duration'],
|
||||||
|
})
|
||||||
|
fmt = {
|
||||||
|
'format_id': 'vip',
|
||||||
|
'url': vip_url,
|
||||||
|
'vcodec': 'none',
|
||||||
|
}
|
||||||
|
if '_preview_' in vip_url:
|
||||||
|
self.report_warning(
|
||||||
|
f'This tracks requires a VIP account. Using a sample instead. {self._login_hint()}')
|
||||||
|
fmt.update({
|
||||||
|
'format_note': 'Sample',
|
||||||
|
'preference': -10,
|
||||||
|
**traverse_obj(vip_info, {
|
||||||
|
'filesize': ('sampleLength', {int_or_none}),
|
||||||
|
'duration': ('sampleDuration', {int_or_none}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
fmt.update(traverse_obj(vip_info, {
|
||||||
|
'filesize': ('totalLength', {int_or_none}),
|
||||||
|
'duration': ('duration', {int_or_none}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
fmt['abr'] = try_call(lambda: fmt['filesize'] * 8 / fmt['duration'] / 1024)
|
||||||
|
formats.append(fmt)
|
||||||
|
|
||||||
|
formats.extend([{
|
||||||
'format_id': f'{bps}k',
|
'format_id': f'{bps}k',
|
||||||
'url': audio_info[k],
|
'url': audio_info[k],
|
||||||
'abr': bps,
|
'abr': bps,
|
||||||
'vcodec': 'none',
|
'vcodec': 'none',
|
||||||
} for bps, k in ((24, 'play_path_32'), (64, 'play_path_64')) if audio_info.get(k)]
|
} for bps, k in ((24, 'play_path_32'), (64, 'play_path_64')) if audio_info.get(k)])
|
||||||
|
|
||||||
thumbnails = []
|
thumbnails = []
|
||||||
for k in audio_info:
|
for k in audio_info:
|
||||||
|
@ -3,16 +3,13 @@
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
try_get,
|
try_get,
|
||||||
update_url_query,
|
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class XinpianchangIE(InfoExtractor):
|
class XinpianchangIE(InfoExtractor):
|
||||||
_WORKING = False
|
_VALID_URL = r'https?://(www\.)?xinpianchang\.com/(?P<id>a\d+)'
|
||||||
_VALID_URL = r'https?://www\.xinpianchang\.com/(?P<id>[^/]+?)(?:\D|$)'
|
IE_DESC = '新片场'
|
||||||
IE_NAME = 'xinpianchang'
|
|
||||||
IE_DESC = 'xinpianchang.com'
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.xinpianchang.com/a11766551',
|
'url': 'https://www.xinpianchang.com/a11766551',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@ -49,11 +46,11 @@ class XinpianchangIE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, video_id=video_id)
|
webpage = self._download_webpage(url, video_id=video_id)
|
||||||
domain = self.find_value_with_regex(var='requireNewDomain', webpage=webpage)
|
video_data = self._search_nextjs_data(webpage, video_id)['props']['pageProps']['detail']['video']
|
||||||
vid = self.find_value_with_regex(var='vid', webpage=webpage)
|
|
||||||
app_key = self.find_value_with_regex(var='modeServerAppKey', webpage=webpage)
|
data = self._download_json(
|
||||||
api = update_url_query(f'{domain}/mod/api/v2/media/{vid}', {'appKey': app_key})
|
f'https://mod-api.xinpianchang.com/mod/api/v2/media/{video_data["vid"]}', video_id,
|
||||||
data = self._download_json(api, video_id=video_id)['data']
|
query={'appKey': video_data['appKey']})['data']
|
||||||
formats, subtitles = [], {}
|
formats, subtitles = [], {}
|
||||||
for k, v in data.get('resource').items():
|
for k, v in data.get('resource').items():
|
||||||
if k in ('dash', 'hls'):
|
if k in ('dash', 'hls'):
|
||||||
@ -72,6 +69,10 @@ def _real_extract(self, url):
|
|||||||
'width': int_or_none(prog.get('width')),
|
'width': int_or_none(prog.get('width')),
|
||||||
'height': int_or_none(prog.get('height')),
|
'height': int_or_none(prog.get('height')),
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
|
'http_headers': {
|
||||||
|
# NB: Server returns 403 without the Range header
|
||||||
|
'Range': 'bytes=0-',
|
||||||
|
},
|
||||||
} for prog in v if prog.get('url') or []])
|
} for prog in v if prog.get('url') or []])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -87,6 +88,3 @@ def _real_extract(self, url):
|
|||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
}
|
}
|
||||||
|
|
||||||
def find_value_with_regex(self, var, webpage):
|
|
||||||
return self._search_regex(rf'var\s{var}\s=\s\"(?P<vid>[^\"]+)\"', webpage, name=var)
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user