mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-08-09 13:48:35 +03:00
Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
264044286d | ||
|
|
a98e7f9f58 | ||
|
|
0ea5d5882d | ||
|
|
cdc533b114 | ||
|
|
c2e124881f | ||
|
|
ad55bfcfb7 | ||
|
|
739125d40f | ||
|
|
5f94f05490 | ||
|
|
5d7678195a | ||
|
|
eafedc2181 | ||
|
|
8eb8695139 | ||
|
|
df160ab18d | ||
|
|
6d41aaf21c | ||
|
|
a6673a8e82 | ||
|
|
87be1bb96a | ||
|
|
ccc25d6710 | ||
|
|
5513036104 | ||
|
|
bd5ed90419 | ||
|
|
88e2a2de8e | ||
|
|
12b57d2858 | ||
|
|
b7b7910d96 | ||
|
|
50e452fd7d | ||
|
|
94c5622be9 | ||
|
|
7df5acc546 | ||
|
|
4429fd0450 | ||
|
|
2e81e298cd | ||
|
|
7f5d9f8543 | ||
|
|
f8750504c2 | ||
|
|
8821682f15 | ||
|
|
08d7899683 | ||
|
|
98b6b0d339 | ||
|
|
bf5d18016b | ||
|
|
4bc19adc87 | ||
|
|
b2c01d0498 | ||
|
|
e123a48f11 | ||
|
|
820c6e2445 | ||
|
|
677997d84e | ||
|
|
b81e9272dc | ||
|
|
df4b4e8ccf | ||
|
|
f3829463c7 | ||
|
|
ae3923b6b2 | ||
|
|
8ab262c66b | ||
|
|
e2d37bcc8e | ||
|
|
eb4b3a5fc7 | ||
|
|
65e90aea29 | ||
|
|
17bfaa53ed | ||
|
|
8cb037c0b0 | ||
|
|
7d9e48b22a | ||
|
|
f5cb721185 | ||
|
|
83b8409366 | ||
|
|
ba80446855 | ||
|
|
22ea0688ed | ||
|
|
5c1abcdc49 | ||
|
|
3d9a88bd8e | ||
|
|
9def9a4b0e | ||
|
|
679587dac7 | ||
|
|
a1c98226a4 | ||
|
|
c8ede5f34d | ||
|
|
a183837ec8 | ||
|
|
067062bb87 | ||
|
|
8597a4331e | ||
|
|
48a214bef4 | ||
|
|
6a763a55d8 | ||
|
|
e6e6b51214 | ||
|
|
7c9b10ebc8 | ||
|
|
cd94e70040 |
@@ -24,6 +24,8 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar issues **including closed ones**. DO NOT post duplicates
|
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar issues **including closed ones**. DO NOT post duplicates
|
||||||
required: true
|
required: true
|
||||||
|
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
|
||||||
|
required: true
|
||||||
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required
|
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required
|
||||||
- type: input
|
- type: input
|
||||||
id: region
|
id: region
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar requests **including closed ones**. DO NOT post duplicates
|
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar requests **including closed ones**. DO NOT post duplicates
|
||||||
required: true
|
required: true
|
||||||
|
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
|
||||||
|
required: true
|
||||||
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and am willing to share it if required
|
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and am willing to share it if required
|
||||||
- type: input
|
- type: input
|
||||||
id: region
|
id: region
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar requests **including closed ones**. DO NOT post duplicates
|
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar requests **including closed ones**. DO NOT post duplicates
|
||||||
required: true
|
required: true
|
||||||
|
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
|
||||||
|
required: true
|
||||||
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required
|
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required
|
||||||
- type: input
|
- type: input
|
||||||
id: region
|
id: region
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar issues **including closed ones**. DO NOT post duplicates
|
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar issues **including closed ones**. DO NOT post duplicates
|
||||||
required: true
|
required: true
|
||||||
|
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar requests **including closed ones**. DO NOT post duplicates
|
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar requests **including closed ones**. DO NOT post duplicates
|
||||||
required: true
|
required: true
|
||||||
|
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar questions **including closed ones**. DO NOT post duplicates
|
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%3Aissue%20-label%3Aspam%20%20) for similar questions **including closed ones**. DO NOT post duplicates
|
||||||
required: true
|
required: true
|
||||||
|
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: question
|
id: question
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar issues **including closed ones**. DO NOT post duplicates
|
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar issues **including closed ones**. DO NOT post duplicates
|
||||||
required: true
|
required: true
|
||||||
|
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
|
||||||
|
required: true
|
||||||
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required
|
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required
|
||||||
- type: input
|
- type: input
|
||||||
id: region
|
id: region
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar requests **including closed ones**. DO NOT post duplicates
|
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar requests **including closed ones**. DO NOT post duplicates
|
||||||
required: true
|
required: true
|
||||||
|
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
|
||||||
|
required: true
|
||||||
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and am willing to share it if required
|
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and am willing to share it if required
|
||||||
- type: input
|
- type: input
|
||||||
id: region
|
id: region
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar requests **including closed ones**. DO NOT post duplicates
|
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar requests **including closed ones**. DO NOT post duplicates
|
||||||
required: true
|
required: true
|
||||||
|
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
|
||||||
|
required: true
|
||||||
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required
|
- label: I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required
|
||||||
- type: input
|
- type: input
|
||||||
id: region
|
id: region
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar issues **including closed ones**. DO NOT post duplicates
|
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar issues **including closed ones**. DO NOT post duplicates
|
||||||
required: true
|
required: true
|
||||||
|
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar requests **including closed ones**. DO NOT post duplicates
|
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar requests **including closed ones**. DO NOT post duplicates
|
||||||
required: true
|
required: true
|
||||||
|
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar questions **including closed ones**. DO NOT post duplicates
|
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766), [the FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ), and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=is%%3Aissue%%20-label%%3Aspam%%20%%20) for similar questions **including closed ones**. DO NOT post duplicates
|
||||||
required: true
|
required: true
|
||||||
|
- label: I've read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: question
|
id: question
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ Fixes #
|
|||||||
### In order to be accepted and merged into yt-dlp each piece of code must be in public domain or released under [Unlicense](http://unlicense.org/). Check those that apply and remove the others:
|
### In order to be accepted and merged into yt-dlp each piece of code must be in public domain or released under [Unlicense](http://unlicense.org/). Check those that apply and remove the others:
|
||||||
- [ ] I am the original author of the code in this PR, and I am willing to release it under [Unlicense](http://unlicense.org/)
|
- [ ] I am the original author of the code in this PR, and I am willing to release it under [Unlicense](http://unlicense.org/)
|
||||||
- [ ] I am not the original author of the code in this PR, but it is in the public domain or released under [Unlicense](http://unlicense.org/) (provide reliable evidence)
|
- [ ] I am not the original author of the code in this PR, but it is in the public domain or released under [Unlicense](http://unlicense.org/) (provide reliable evidence)
|
||||||
|
- [ ] I have read the [policy against AI/LLM contributions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#automated-contributions-ai--llm-policy) and understand I may be blocked from the repository if it is violated
|
||||||
|
|
||||||
### What is the purpose of your *pull request*? Check those that apply and remove the others:
|
### What is the purpose of your *pull request*? Check those that apply and remove the others:
|
||||||
- [ ] Fix or improvement to an extractor (Make sure to add/update tests)
|
- [ ] Fix or improvement to an extractor (Make sure to add/update tests)
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
self-hosted-runner:
|
||||||
|
labels:
|
||||||
|
# Workaround for the outdated runner list in actionlint v1.7.7
|
||||||
|
# Ref: https://github.com/rhysd/actionlint/issues/533
|
||||||
|
- windows-11-arm
|
||||||
|
|
||||||
|
config-variables:
|
||||||
|
- KEEP_CACHE_WARM
|
||||||
|
- PUSH_VERSION_COMMIT
|
||||||
|
- UPDATE_TO_VERIFICATION
|
||||||
|
- PYPI_PROJECT
|
||||||
|
- PYPI_SUFFIX
|
||||||
|
- NIGHTLY_PYPI_PROJECT
|
||||||
|
- NIGHTLY_PYPI_SUFFIX
|
||||||
|
- NIGHTLY_ARCHIVE_REPO
|
||||||
|
- BUILD_NIGHTLY
|
||||||
|
- MASTER_PYPI_PROJECT
|
||||||
|
- MASTER_PYPI_SUFFIX
|
||||||
|
- MASTER_ARCHIVE_REPO
|
||||||
|
- BUILD_MASTER
|
||||||
|
- ISSUE_LOCKDOWN
|
||||||
|
- SANITIZE_COMMENT
|
||||||
|
|
||||||
|
paths:
|
||||||
|
.github/workflows/build.yml:
|
||||||
|
ignore:
|
||||||
|
# SC1090 "Can't follow non-constant source": ignore when using `source` to activate venv
|
||||||
|
- '.+SC1090.+'
|
||||||
+142
-144
@@ -9,6 +9,9 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: stable
|
default: stable
|
||||||
type: string
|
type: string
|
||||||
|
origin:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
unix:
|
unix:
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -27,10 +30,6 @@ on:
|
|||||||
windows:
|
windows:
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
origin:
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
type: string
|
|
||||||
secrets:
|
secrets:
|
||||||
GPG_SIGNING_KEY:
|
GPG_SIGNING_KEY:
|
||||||
required: false
|
required: false
|
||||||
@@ -74,13 +73,6 @@ on:
|
|||||||
description: yt-dlp.exe, yt-dlp_win.zip, yt-dlp_x86.exe, yt-dlp_win_x86.zip, yt-dlp_arm64.exe, yt-dlp_win_arm64.zip
|
description: yt-dlp.exe, yt-dlp_win.zip, yt-dlp_x86.exe, yt-dlp_win_x86.zip, yt-dlp_arm64.exe, yt-dlp_win_arm64.zip
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
origin:
|
|
||||||
description: Origin
|
|
||||||
required: false
|
|
||||||
default: 'current repo'
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- 'current repo'
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -89,25 +81,26 @@ jobs:
|
|||||||
process:
|
process:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
origin: ${{ steps.process_origin.outputs.origin }}
|
origin: ${{ steps.process_inputs.outputs.origin }}
|
||||||
timestamp: ${{ steps.process_origin.outputs.timestamp }}
|
timestamp: ${{ steps.process_inputs.outputs.timestamp }}
|
||||||
version: ${{ steps.process_origin.outputs.version }}
|
version: ${{ steps.process_inputs.outputs.version }}
|
||||||
|
linux_matrix: ${{ steps.linux_matrix.outputs.matrix }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Process origin
|
- name: Process inputs
|
||||||
id: process_origin
|
id: process_inputs
|
||||||
env:
|
env:
|
||||||
ORIGIN: ${{ inputs.origin }}
|
INPUTS: ${{ toJSON(inputs) }}
|
||||||
REPOSITORY: ${{ github.repository }}
|
REPOSITORY: ${{ github.repository }}
|
||||||
VERSION: ${{ inputs.version }}
|
|
||||||
shell: python
|
shell: python
|
||||||
run: |
|
run: |
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
origin = os.environ['ORIGIN']
|
INPUTS = json.loads(os.environ['INPUTS'])
|
||||||
timestamp = dt.datetime.now(tz=dt.timezone.utc).strftime('%Y.%m.%d.%H%M%S.%f')
|
timestamp = dt.datetime.now(tz=dt.timezone.utc).strftime('%Y.%m.%d.%H%M%S.%f')
|
||||||
version = os.getenv('VERSION')
|
version = INPUTS.get('version')
|
||||||
if version and '.' not in version:
|
if version and '.' not in version:
|
||||||
# build.yml was dispatched with only a revision as the version input value
|
# build.yml was dispatched with only a revision as the version input value
|
||||||
version_parts = [*timestamp.split('.')[:3], version]
|
version_parts = [*timestamp.split('.')[:3], version]
|
||||||
@@ -119,7 +112,7 @@ jobs:
|
|||||||
version_parts = version.split('.')
|
version_parts = version.split('.')
|
||||||
assert all(re.fullmatch(r'[0-9]+', part) for part in version_parts), 'Version must be numeric'
|
assert all(re.fullmatch(r'[0-9]+', part) for part in version_parts), 'Version must be numeric'
|
||||||
outputs = {
|
outputs = {
|
||||||
'origin': os.environ['REPOSITORY'] if origin == 'current repo' else origin,
|
'origin': INPUTS.get('origin') or os.environ['REPOSITORY'],
|
||||||
'timestamp': timestamp,
|
'timestamp': timestamp,
|
||||||
'version': '.'.join(version_parts),
|
'version': '.'.join(version_parts),
|
||||||
}
|
}
|
||||||
@@ -127,6 +120,69 @@ jobs:
|
|||||||
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
||||||
f.write('\n'.join(f'{key}={value}' for key, value in outputs.items()))
|
f.write('\n'.join(f'{key}={value}' for key, value in outputs.items()))
|
||||||
|
|
||||||
|
- name: Build Linux matrix
|
||||||
|
id: linux_matrix
|
||||||
|
env:
|
||||||
|
INPUTS: ${{ toJSON(inputs) }}
|
||||||
|
PYTHON_VERSION: '3.13'
|
||||||
|
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
|
||||||
|
shell: python
|
||||||
|
run: |
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
EXE_MAP = {
|
||||||
|
'linux': [{
|
||||||
|
'os': 'linux',
|
||||||
|
'arch': 'x86_64',
|
||||||
|
'runner': 'ubuntu-24.04',
|
||||||
|
}, {
|
||||||
|
'os': 'linux',
|
||||||
|
'arch': 'aarch64',
|
||||||
|
'runner': 'ubuntu-24.04-arm',
|
||||||
|
}],
|
||||||
|
'linux_armv7l': [{
|
||||||
|
'os': 'linux',
|
||||||
|
'arch': 'armv7l',
|
||||||
|
'runner': 'ubuntu-24.04-arm',
|
||||||
|
'qemu_platform': 'linux/arm/v7',
|
||||||
|
'onefile': False,
|
||||||
|
'cache_requirements': True,
|
||||||
|
'update_to': 'yt-dlp/yt-dlp@2023.03.04',
|
||||||
|
}],
|
||||||
|
'musllinux': [{
|
||||||
|
'os': 'musllinux',
|
||||||
|
'arch': 'x86_64',
|
||||||
|
'runner': 'ubuntu-24.04',
|
||||||
|
}, {
|
||||||
|
'os': 'musllinux',
|
||||||
|
'arch': 'aarch64',
|
||||||
|
'runner': 'ubuntu-24.04-arm',
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
INPUTS = json.loads(os.environ['INPUTS'])
|
||||||
|
matrix = [exe for key, group in EXE_MAP.items() for exe in group if INPUTS.get(key)]
|
||||||
|
if not matrix:
|
||||||
|
# If we send an empty matrix when no linux inputs are given, the entire workflow fails
|
||||||
|
matrix = [EXE_MAP['linux'][0]]
|
||||||
|
for exe in matrix:
|
||||||
|
exe['exe'] = '_'.join(filter(None, (
|
||||||
|
'yt-dlp',
|
||||||
|
exe['os'],
|
||||||
|
exe['arch'] != 'x86_64' and exe['arch'],
|
||||||
|
)))
|
||||||
|
exe.setdefault('qemu_platform', None)
|
||||||
|
exe.setdefault('onefile', True)
|
||||||
|
exe.setdefault('onedir', True)
|
||||||
|
exe.setdefault('cache_requirements', False)
|
||||||
|
exe.setdefault('python_version', os.environ['PYTHON_VERSION'])
|
||||||
|
exe.setdefault('update_to', os.environ['UPDATE_TO'])
|
||||||
|
if not any(INPUTS.get(key) for key in EXE_MAP):
|
||||||
|
print('skipping linux job')
|
||||||
|
else:
|
||||||
|
print(json.dumps(matrix, indent=2))
|
||||||
|
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
||||||
|
f.write(f'matrix={json.dumps(matrix)}')
|
||||||
|
|
||||||
unix:
|
unix:
|
||||||
needs: process
|
needs: process
|
||||||
if: inputs.unix
|
if: inputs.unix
|
||||||
@@ -135,33 +191,41 @@ jobs:
|
|||||||
CHANNEL: ${{ inputs.channel }}
|
CHANNEL: ${{ inputs.channel }}
|
||||||
ORIGIN: ${{ needs.process.outputs.origin }}
|
ORIGIN: ${{ needs.process.outputs.origin }}
|
||||||
VERSION: ${{ needs.process.outputs.version }}
|
VERSION: ${{ needs.process.outputs.version }}
|
||||||
|
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Needed for changelog
|
fetch-depth: 0 # Needed for changelog
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
|
||||||
- name: Install Requirements
|
- name: Install Requirements
|
||||||
run: |
|
run: |
|
||||||
sudo apt -y install zip pandoc man sed
|
sudo apt -y install zip pandoc man sed
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
python devscripts/update-version.py -c "${CHANNEL}" -r "${ORIGIN}" "${VERSION}"
|
python devscripts/update-version.py -c "${CHANNEL}" -r "${ORIGIN}" "${VERSION}"
|
||||||
python devscripts/update_changelog.py -vv
|
python devscripts/update_changelog.py -vv
|
||||||
python devscripts/make_lazy_extractors.py
|
python devscripts/make_lazy_extractors.py
|
||||||
|
|
||||||
- name: Build Unix platform-independent binary
|
- name: Build Unix platform-independent binary
|
||||||
run: |
|
run: |
|
||||||
make all tar
|
make all tar
|
||||||
|
|
||||||
- name: Verify --update-to
|
- name: Verify --update-to
|
||||||
if: vars.UPDATE_TO_VERIFICATION
|
if: vars.UPDATE_TO_VERIFICATION
|
||||||
run: |
|
run: |
|
||||||
chmod +x ./yt-dlp
|
chmod +x ./yt-dlp
|
||||||
cp ./yt-dlp ./yt-dlp_downgraded
|
cp ./yt-dlp ./yt-dlp_downgraded
|
||||||
version="$(./yt-dlp --version)"
|
version="$(./yt-dlp --version)"
|
||||||
./yt-dlp_downgraded -v --update-to yt-dlp/yt-dlp@2023.03.04
|
./yt-dlp_downgraded -v --update-to "${UPDATE_TO}"
|
||||||
downgraded_version="$(./yt-dlp_downgraded --version)"
|
downgraded_version="$(./yt-dlp_downgraded --version)"
|
||||||
[[ "${version}" != "${downgraded_version}" ]]
|
[[ "${version}" != "${downgraded_version}" ]]
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -172,153 +236,71 @@ jobs:
|
|||||||
compression-level: 0
|
compression-level: 0
|
||||||
|
|
||||||
linux:
|
linux:
|
||||||
|
name: ${{ matrix.os }} (${{ matrix.arch }})
|
||||||
|
if: inputs.linux || inputs.linux_armv7l || inputs.musllinux
|
||||||
needs: process
|
needs: process
|
||||||
if: inputs.linux
|
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include: ${{ fromJSON(needs.process.outputs.linux_matrix) }}
|
||||||
- exe: yt-dlp_linux
|
|
||||||
platform: x86_64
|
|
||||||
runner: ubuntu-24.04
|
|
||||||
- exe: yt-dlp_linux_aarch64
|
|
||||||
platform: aarch64
|
|
||||||
runner: ubuntu-24.04-arm
|
|
||||||
env:
|
env:
|
||||||
CHANNEL: ${{ inputs.channel }}
|
CHANNEL: ${{ inputs.channel }}
|
||||||
ORIGIN: ${{ needs.process.outputs.origin }}
|
ORIGIN: ${{ needs.process.outputs.origin }}
|
||||||
VERSION: ${{ needs.process.outputs.version }}
|
VERSION: ${{ needs.process.outputs.version }}
|
||||||
EXE_NAME: ${{ matrix.exe }}
|
EXE_NAME: ${{ matrix.exe }}
|
||||||
steps:
|
PYTHON_VERSION: ${{ matrix.python_version }}
|
||||||
- uses: actions/checkout@v4
|
UPDATE_TO: ${{ (vars.UPDATE_TO_VERIFICATION && matrix.update_to) || '' }}
|
||||||
- name: Build executable
|
SKIP_ONEDIR_BUILD: ${{ (!matrix.onedir && '1') || '' }}
|
||||||
env:
|
SKIP_ONEFILE_BUILD: ${{ (!matrix.onefile && '1') || '' }}
|
||||||
SERVICE: linux_${{ matrix.platform }}
|
|
||||||
run: |
|
|
||||||
mkdir -p ./dist
|
|
||||||
pushd bundle/docker
|
|
||||||
docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
|
|
||||||
popd
|
|
||||||
sudo chown "${USER}:docker" "./dist/${EXE_NAME}"
|
|
||||||
- name: Verify executable in container
|
|
||||||
if: vars.UPDATE_TO_VERIFICATION
|
|
||||||
env:
|
|
||||||
SERVICE: linux_${{ matrix.platform }}_verify
|
|
||||||
run: |
|
|
||||||
cd bundle/docker
|
|
||||||
docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
|
|
||||||
- name: Verify --update-to
|
|
||||||
if: vars.UPDATE_TO_VERIFICATION
|
|
||||||
run: |
|
|
||||||
chmod +x "./dist/${EXE_NAME}"
|
|
||||||
mkdir -p ~/testing
|
|
||||||
cp "./dist/${EXE_NAME}" ~/testing/"${EXE_NAME}_downgraded"
|
|
||||||
version="$("./dist/${EXE_NAME}" --version)"
|
|
||||||
~/testing/"${EXE_NAME}_downgraded" -v --update-to yt-dlp/yt-dlp@2023.03.04
|
|
||||||
downgraded_version="$(~/testing/"${EXE_NAME}_downgraded" --version)"
|
|
||||||
[[ "${version}" != "${downgraded_version}" ]]
|
|
||||||
- name: Upload artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build-bin-${{ github.job }}_${{ matrix.platform }}
|
|
||||||
path: |
|
|
||||||
dist/${{ matrix.exe }}*
|
|
||||||
compression-level: 0
|
|
||||||
|
|
||||||
linux_armv7l:
|
|
||||||
needs: process
|
|
||||||
if: inputs.linux_armv7l
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
runs-on: ubuntu-24.04-arm
|
|
||||||
env:
|
|
||||||
CHANNEL: ${{ inputs.channel }}
|
|
||||||
ORIGIN: ${{ needs.process.outputs.origin }}
|
|
||||||
VERSION: ${{ needs.process.outputs.version }}
|
|
||||||
EXE_NAME: yt-dlp_linux_armv7l
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Cache requirements
|
- name: Cache requirements
|
||||||
|
if: matrix.cache_requirements
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
|
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/yt-dlp-build-venv
|
venv
|
||||||
key: cache-reqs-${{ github.job }}-${{ github.ref }}-${{ needs.process.outputs.timestamp }}
|
key: cache-reqs-${{ matrix.os }}_${{ matrix.arch }}-${{ github.ref }}-${{ needs.process.outputs.timestamp }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
cache-reqs-${{ github.job }}-${{ github.ref }}-
|
cache-reqs-${{ matrix.os }}_${{ matrix.arch }}-${{ github.ref }}-
|
||||||
cache-reqs-${{ github.job }}-
|
cache-reqs-${{ matrix.os }}_${{ matrix.arch }}-
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
|
if: matrix.qemu_platform
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
with:
|
||||||
platforms: linux/arm/v7
|
platforms: ${{ matrix.qemu_platform }}
|
||||||
- name: Build executable
|
|
||||||
env:
|
|
||||||
SERVICE: linux_armv7l
|
|
||||||
run: |
|
|
||||||
mkdir -p ./dist
|
|
||||||
mkdir -p ~/yt-dlp-build-venv
|
|
||||||
cd bundle/docker
|
|
||||||
docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
|
|
||||||
- name: Verify executable in container
|
|
||||||
if: vars.UPDATE_TO_VERIFICATION
|
|
||||||
env:
|
|
||||||
SERVICE: linux_armv7l_verify
|
|
||||||
run: |
|
|
||||||
cd bundle/docker
|
|
||||||
docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
|
|
||||||
- name: Upload artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build-bin-${{ github.job }}
|
|
||||||
path: |
|
|
||||||
dist/yt-dlp_linux_armv7l.zip
|
|
||||||
compression-level: 0
|
|
||||||
|
|
||||||
musllinux:
|
|
||||||
needs: process
|
|
||||||
if: inputs.musllinux
|
|
||||||
runs-on: ${{ matrix.runner }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- exe: yt-dlp_musllinux
|
|
||||||
platform: x86_64
|
|
||||||
runner: ubuntu-24.04
|
|
||||||
- exe: yt-dlp_musllinux_aarch64
|
|
||||||
platform: aarch64
|
|
||||||
runner: ubuntu-24.04-arm
|
|
||||||
env:
|
|
||||||
CHANNEL: ${{ inputs.channel }}
|
|
||||||
ORIGIN: ${{ needs.process.outputs.origin }}
|
|
||||||
VERSION: ${{ needs.process.outputs.version }}
|
|
||||||
EXE_NAME: ${{ matrix.exe }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Build executable
|
- name: Build executable
|
||||||
env:
|
env:
|
||||||
SERVICE: musllinux_${{ matrix.platform }}
|
SERVICE: ${{ matrix.os }}_${{ matrix.arch }}
|
||||||
run: |
|
run: |
|
||||||
|
mkdir -p ./venv
|
||||||
mkdir -p ./dist
|
mkdir -p ./dist
|
||||||
pushd bundle/docker
|
pushd bundle/docker
|
||||||
docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
|
docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
|
||||||
popd
|
popd
|
||||||
sudo chown "${USER}:docker" "./dist/${EXE_NAME}"
|
if [[ -z "${SKIP_ONEFILE_BUILD}" ]]; then
|
||||||
|
sudo chown "${USER}:docker" "./dist/${EXE_NAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Verify executable in container
|
- name: Verify executable in container
|
||||||
if: vars.UPDATE_TO_VERIFICATION
|
|
||||||
env:
|
env:
|
||||||
SERVICE: musllinux_${{ matrix.platform }}_verify
|
SERVICE: ${{ matrix.os }}_${{ matrix.arch }}_verify
|
||||||
run: |
|
run: |
|
||||||
cd bundle/docker
|
cd bundle/docker
|
||||||
docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
|
docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build-bin-${{ github.job }}_${{ matrix.platform }}
|
name: build-bin-${{ matrix.os }}_${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
dist/${{ matrix.exe }}*
|
dist/${{ matrix.exe }}*
|
||||||
compression-level: 0
|
compression-level: 0
|
||||||
@@ -333,6 +315,7 @@ jobs:
|
|||||||
CHANNEL: ${{ inputs.channel }}
|
CHANNEL: ${{ inputs.channel }}
|
||||||
ORIGIN: ${{ needs.process.outputs.origin }}
|
ORIGIN: ${{ needs.process.outputs.origin }}
|
||||||
VERSION: ${{ needs.process.outputs.version }}
|
VERSION: ${{ needs.process.outputs.version }}
|
||||||
|
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -409,7 +392,7 @@ jobs:
|
|||||||
chmod +x ./dist/yt-dlp_macos
|
chmod +x ./dist/yt-dlp_macos
|
||||||
cp ./dist/yt-dlp_macos ./dist/yt-dlp_macos_downgraded
|
cp ./dist/yt-dlp_macos ./dist/yt-dlp_macos_downgraded
|
||||||
version="$(./dist/yt-dlp_macos --version)"
|
version="$(./dist/yt-dlp_macos --version)"
|
||||||
./dist/yt-dlp_macos_downgraded -v --update-to yt-dlp/yt-dlp@2023.03.04
|
./dist/yt-dlp_macos_downgraded -v --update-to "${UPDATE_TO}"
|
||||||
downgraded_version="$(./dist/yt-dlp_macos_downgraded --version)"
|
downgraded_version="$(./dist/yt-dlp_macos_downgraded --version)"
|
||||||
[[ "$version" != "$downgraded_version" ]]
|
[[ "$version" != "$downgraded_version" ]]
|
||||||
|
|
||||||
@@ -423,6 +406,7 @@ jobs:
|
|||||||
compression-level: 0
|
compression-level: 0
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
|
name: windows (${{ matrix.arch }})
|
||||||
needs: process
|
needs: process
|
||||||
if: inputs.windows
|
if: inputs.windows
|
||||||
permissions:
|
permissions:
|
||||||
@@ -435,27 +419,37 @@ jobs:
|
|||||||
- arch: 'x64'
|
- arch: 'x64'
|
||||||
runner: windows-2025
|
runner: windows-2025
|
||||||
python_version: '3.10'
|
python_version: '3.10'
|
||||||
suffix: ''
|
platform_tag: win_amd64
|
||||||
|
pyi_version: '6.16.0'
|
||||||
|
pyi_tag: '2025.09.13.221251'
|
||||||
|
pyi_hash: b6496c7630c3afe66900cfa824e8234a8c2e2c81704bd7facd79586abc76c0e5
|
||||||
- arch: 'x86'
|
- arch: 'x86'
|
||||||
runner: windows-2025
|
runner: windows-2025
|
||||||
python_version: '3.10'
|
python_version: '3.10'
|
||||||
suffix: '_x86'
|
platform_tag: win32
|
||||||
|
pyi_version: '6.16.0'
|
||||||
|
pyi_tag: '2025.09.13.221251'
|
||||||
|
pyi_hash: 2d881843580efdc54f3523507fc6d9c5b6051ee49c743a6d9b7003ac5758c226
|
||||||
- arch: 'arm64'
|
- arch: 'arm64'
|
||||||
runner: windows-11-arm
|
runner: windows-11-arm
|
||||||
python_version: '3.13' # arm64 only has Python >= 3.11 available
|
python_version: '3.13' # arm64 only has Python >= 3.11 available
|
||||||
suffix: '_arm64'
|
platform_tag: win_arm64
|
||||||
|
pyi_version: '6.16.0'
|
||||||
|
pyi_tag: '2025.09.13.221251'
|
||||||
|
pyi_hash: 4250c9085e34a95c898f3ee2f764914fc36ec59f0d97c28e6a75fcf21f7b144f
|
||||||
env:
|
env:
|
||||||
CHANNEL: ${{ inputs.channel }}
|
CHANNEL: ${{ inputs.channel }}
|
||||||
ORIGIN: ${{ needs.process.outputs.origin }}
|
ORIGIN: ${{ needs.process.outputs.origin }}
|
||||||
VERSION: ${{ needs.process.outputs.version }}
|
VERSION: ${{ needs.process.outputs.version }}
|
||||||
SUFFIX: ${{ matrix.suffix }}
|
SUFFIX: ${{ (matrix.arch != 'x64' && format('_{0}', matrix.arch)) || '' }}
|
||||||
|
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
|
||||||
BASE_CACHE_KEY: cache-reqs-${{ github.job }}_${{ matrix.arch }}-${{ matrix.python_version }}
|
BASE_CACHE_KEY: cache-reqs-${{ github.job }}_${{ matrix.arch }}-${{ matrix.python_version }}
|
||||||
# Use custom PyInstaller built with https://github.com/yt-dlp/Pyinstaller-builds
|
PYI_REPO: https://github.com/yt-dlp/Pyinstaller-Builds
|
||||||
PYINSTALLER_URL: https://yt-dlp.github.io/Pyinstaller-Builds/${{ matrix.arch }}/pyinstaller-6.15.0-py3-none-any.whl
|
PYI_WHEEL: pyinstaller-${{ matrix.pyi_version }}-py3-none-${{ matrix.platform_tag }}.whl
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python_version }}
|
python-version: ${{ matrix.python_version }}
|
||||||
architecture: ${{ matrix.arch }}
|
architecture: ${{ matrix.arch }}
|
||||||
@@ -477,17 +471,23 @@ jobs:
|
|||||||
- name: Install Requirements
|
- name: Install Requirements
|
||||||
env:
|
env:
|
||||||
ARCH: ${{ matrix.arch }}
|
ARCH: ${{ matrix.arch }}
|
||||||
|
PYI_URL: ${{ env.PYI_REPO }}/releases/download/${{ matrix.pyi_tag }}/${{ env.PYI_WHEEL }}
|
||||||
|
PYI_HASH: ${{ matrix.pyi_hash }}
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
python -m venv /yt-dlp-build-venv
|
python -m venv /yt-dlp-build-venv
|
||||||
/yt-dlp-build-venv/Scripts/Activate.ps1
|
/yt-dlp-build-venv/Scripts/Activate.ps1
|
||||||
|
python -m pip install -U pip
|
||||||
|
# Install custom PyInstaller build and verify hash
|
||||||
|
mkdir /pyi-wheels
|
||||||
|
python -m pip download -d /pyi-wheels --no-deps --require-hashes "pyinstaller@${Env:PYI_URL}#sha256=${Env:PYI_HASH}"
|
||||||
|
python -m pip install --force-reinstall -U "/pyi-wheels/${Env:PYI_WHEEL}"
|
||||||
python devscripts/install_deps.py -o --include build
|
python devscripts/install_deps.py -o --include build
|
||||||
if ("${Env:ARCH}" -eq "x86") {
|
if ("${Env:ARCH}" -eq "x86") {
|
||||||
python devscripts/install_deps.py
|
python devscripts/install_deps.py
|
||||||
} else {
|
} else {
|
||||||
python devscripts/install_deps.py --include curl-cffi
|
python devscripts/install_deps.py --include curl-cffi
|
||||||
}
|
}
|
||||||
python -m pip install -U "${Env:PYINSTALLER_URL}"
|
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
@@ -510,7 +510,7 @@ jobs:
|
|||||||
$name = "yt-dlp${Env:SUFFIX}"
|
$name = "yt-dlp${Env:SUFFIX}"
|
||||||
Copy-Item "./dist/${name}.exe" "./dist/${name}_downgraded.exe"
|
Copy-Item "./dist/${name}.exe" "./dist/${name}_downgraded.exe"
|
||||||
$version = & "./dist/${name}.exe" --version
|
$version = & "./dist/${name}.exe" --version
|
||||||
& "./dist/${name}_downgraded.exe" -v --update-to yt-dlp/yt-dlp@2025.08.20
|
& "./dist/${name}_downgraded.exe" -v --update-to "${Env:UPDATE_TO}"
|
||||||
$downgraded_version = & "./dist/${name}_downgraded.exe" --version
|
$downgraded_version = & "./dist/${name}_downgraded.exe" --version
|
||||||
if ($version -eq $downgraded_version) {
|
if ($version -eq $downgraded_version) {
|
||||||
exit 1
|
exit 1
|
||||||
@@ -521,8 +521,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: build-bin-${{ github.job }}-${{ matrix.arch }}
|
name: build-bin-${{ github.job }}-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
dist/yt-dlp${{ matrix.suffix }}.exe
|
dist/yt-dlp${{ env.SUFFIX }}.exe
|
||||||
dist/yt-dlp_win${{ matrix.suffix }}.zip
|
dist/yt-dlp_win${{ env.SUFFIX }}.zip
|
||||||
compression-level: 0
|
compression-level: 0
|
||||||
|
|
||||||
meta_files:
|
meta_files:
|
||||||
@@ -531,8 +531,6 @@ jobs:
|
|||||||
- process
|
- process
|
||||||
- unix
|
- unix
|
||||||
- linux
|
- linux
|
||||||
- linux_armv7l
|
|
||||||
- musllinux
|
|
||||||
- macos
|
- macos
|
||||||
- windows
|
- windows
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: '999999'
|
version: '999999'
|
||||||
channel: stable
|
channel: stable
|
||||||
|
origin: ${{ github.repository }}
|
||||||
unix: false
|
unix: false
|
||||||
linux: false
|
linux: false
|
||||||
linux_armv7l: true
|
linux_armv7l: true
|
||||||
|
|||||||
@@ -37,23 +37,27 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
# CPython 3.9 is in quick-test
|
# CPython 3.9 is in quick-test
|
||||||
python-version: ['3.10', '3.11', '3.12', '3.13', pypy-3.11]
|
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14-dev', pypy-3.11]
|
||||||
include:
|
include:
|
||||||
# atleast one of each CPython/PyPy tests must be in windows
|
# atleast one of each CPython/PyPy tests must be in windows
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: '3.10'
|
python-version: '3.10'
|
||||||
|
- os: windows-latest
|
||||||
|
python-version: '3.11'
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: '3.13'
|
python-version: '3.13'
|
||||||
|
- os: windows-latest
|
||||||
|
python-version: '3.14-dev'
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: pypy-3.11
|
python-version: pypy-3.11
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install test requirements
|
- name: Install test requirements
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
- name: Install test requirements
|
- name: Install test requirements
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
python-version: ['3.10', '3.11', '3.12', '3.13', pypy-3.11]
|
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14-dev', pypy-3.11]
|
||||||
include:
|
include:
|
||||||
# atleast one of each CPython/PyPy tests must be in windows
|
# atleast one of each CPython/PyPy tests must be in windows
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install test requirements
|
- name: Install test requirements
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
- name: Install test requirements
|
- name: Install test requirements
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.9'
|
||||||
- name: Install dev dependencies
|
- name: Install dev dependencies
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10" # Keep this in sync with test-workflows.yml
|
||||||
|
|
||||||
- name: Process inputs
|
- name: Process inputs
|
||||||
id: process_inputs
|
id: process_inputs
|
||||||
@@ -173,7 +173,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
|
||||||
@@ -241,7 +241,7 @@ jobs:
|
|||||||
path: artifact
|
path: artifact
|
||||||
pattern: build-*
|
pattern: build-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
|
||||||
@@ -269,12 +269,11 @@ jobs:
|
|||||||
"[]" \
|
"[]" \
|
||||||
"(https://github.com/${MASTER_REPO}/releases/latest \"Master builds\")" >> ./RELEASE_NOTES
|
"(https://github.com/${MASTER_REPO}/releases/latest \"Master builds\")" >> ./RELEASE_NOTES
|
||||||
fi
|
fi
|
||||||
printf '\n\n' >> ./RELEASE_NOTES
|
printf '\n\n%s\n\n%s%s\n\n---\n' \
|
||||||
cat >> ./RELEASE_NOTES << EOF
|
"#### A description of the various files is in the [README](https://github.com/${REPOSITORY}#release-files)" \
|
||||||
#### A description of the various files is in the [README](https://github.com/${REPOSITORY}#release-files)
|
"The PyInstaller-bundled executables are subject to the licenses described in " \
|
||||||
---
|
"[THIRD_PARTY_LICENSES.txt](https://github.com/${BASE_REPO}/blob/${HEAD_SHA}/THIRD_PARTY_LICENSES.txt)" >> ./RELEASE_NOTES
|
||||||
$(python ./devscripts/make_changelog.py -vv --collapsible)
|
python ./devscripts/make_changelog.py -vv --collapsible >> ./RELEASE_NOTES
|
||||||
EOF
|
|
||||||
printf '%s\n\n' '**This is a pre-release build**' >> ./PRERELEASE_NOTES
|
printf '%s\n\n' '**This is a pre-release build**' >> ./PRERELEASE_NOTES
|
||||||
cat ./RELEASE_NOTES >> ./PRERELEASE_NOTES
|
cat ./RELEASE_NOTES >> ./PRERELEASE_NOTES
|
||||||
printf '%s\n\n' "Generated from: https://github.com/${REPOSITORY}/commit/${HEAD_SHA}" >> ./ARCHIVE_NOTES
|
printf '%s\n\n' "Generated from: https://github.com/${REPOSITORY}/commit/${HEAD_SHA}" >> ./ARCHIVE_NOTES
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest]
|
os: [ubuntu-latest, windows-latest]
|
||||||
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', pypy-3.11]
|
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14-dev', pypy-3.11]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install test requirements
|
- name: Install test requirements
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
name: Test and lint workflows
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/*
|
||||||
|
- bundle/docker/linux/*.sh
|
||||||
|
- devscripts/setup_variables.py
|
||||||
|
- devscripts/setup_variables_tests.py
|
||||||
|
- devscripts/utils.py
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/*
|
||||||
|
- bundle/docker/linux/*.sh
|
||||||
|
- devscripts/setup_variables.py
|
||||||
|
- devscripts/setup_variables_tests.py
|
||||||
|
- devscripts/utils.py
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
env:
|
||||||
|
ACTIONLINT_VERSION: "1.7.7"
|
||||||
|
ACTIONLINT_SHA256SUM: 023070a287cd8cccd71515fedc843f1985bf96c436b7effaecce67290e7e0757
|
||||||
|
ACTIONLINT_REPO: https://github.com/rhysd/actionlint
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Check workflows
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.10" # Keep this in sync with release.yml's prepare job
|
||||||
|
- name: Install requirements
|
||||||
|
env:
|
||||||
|
ACTIONLINT_TARBALL: ${{ format('actionlint_{0}_linux_amd64.tar.gz', env.ACTIONLINT_VERSION) }}
|
||||||
|
run: |
|
||||||
|
python -m devscripts.install_deps -o --include test
|
||||||
|
sudo apt -y install shellcheck
|
||||||
|
python -m pip install -U pyflakes
|
||||||
|
curl -LO "${ACTIONLINT_REPO}/releases/download/v${ACTIONLINT_VERSION}/${ACTIONLINT_TARBALL}"
|
||||||
|
printf '%s %s' "${ACTIONLINT_SHA256SUM}" "${ACTIONLINT_TARBALL}" | sha256sum -c -
|
||||||
|
tar xvzf "${ACTIONLINT_TARBALL}" actionlint
|
||||||
|
chmod +x actionlint
|
||||||
|
- name: Run actionlint
|
||||||
|
run: |
|
||||||
|
./actionlint -color
|
||||||
|
- name: Check Docker shell scripts
|
||||||
|
run: |
|
||||||
|
shellcheck bundle/docker/linux/*.sh
|
||||||
|
- name: Test GHA devscripts
|
||||||
|
run: |
|
||||||
|
pytest -Werror --tb=short --color=yes devscripts/setup_variables_tests.py
|
||||||
+13
-3
@@ -12,6 +12,7 @@
|
|||||||
- [Is your question about yt-dlp?](#is-your-question-about-yt-dlp)
|
- [Is your question about yt-dlp?](#is-your-question-about-yt-dlp)
|
||||||
- [Are you willing to share account details if needed?](#are-you-willing-to-share-account-details-if-needed)
|
- [Are you willing to share account details if needed?](#are-you-willing-to-share-account-details-if-needed)
|
||||||
- [Is the website primarily used for piracy](#is-the-website-primarily-used-for-piracy)
|
- [Is the website primarily used for piracy](#is-the-website-primarily-used-for-piracy)
|
||||||
|
- [AUTOMATED CONTRIBUTIONS (AI / LLM) POLICY](#automated-contributions-ai--llm-policy)
|
||||||
- [DEVELOPER INSTRUCTIONS](#developer-instructions)
|
- [DEVELOPER INSTRUCTIONS](#developer-instructions)
|
||||||
- [Adding new feature or making overarching changes](#adding-new-feature-or-making-overarching-changes)
|
- [Adding new feature or making overarching changes](#adding-new-feature-or-making-overarching-changes)
|
||||||
- [Adding support for a new site](#adding-support-for-a-new-site)
|
- [Adding support for a new site](#adding-support-for-a-new-site)
|
||||||
@@ -134,6 +135,17 @@ While these steps won't necessarily ensure that no misuse of the account takes p
|
|||||||
We follow [youtube-dl's policy](https://github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free) to not support services that is primarily used for infringing copyright. Additionally, it has been decided to not to support porn sites that specialize in fakes. We also cannot support any service that serves only [DRM protected content](https://en.wikipedia.org/wiki/Digital_rights_management).
|
We follow [youtube-dl's policy](https://github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free) to not support services that is primarily used for infringing copyright. Additionally, it has been decided to not to support porn sites that specialize in fakes. We also cannot support any service that serves only [DRM protected content](https://en.wikipedia.org/wiki/Digital_rights_management).
|
||||||
|
|
||||||
|
|
||||||
|
# AUTOMATED CONTRIBUTIONS (AI / LLM) POLICY
|
||||||
|
|
||||||
|
Please refrain from submitting issues or pull requests that have been generated by an LLM or other fully-automated tools. Any submission that is in violation of this policy will be closed, and the submitter may be blocked from this repository without warning.
|
||||||
|
|
||||||
|
If you submit an issue, you need to understand what your issue description is saying. You need to be able to answer questions about your bug report or feature request. Using an AI tool to *proofread* your issue/comment text is acceptable. Using an AI tool to *write* your issue/comment text is unacceptable.
|
||||||
|
|
||||||
|
If you submit a pull request, you need to understand what every line of code you've changed does. If you can't explain why your PR is doing something, then do not submit it. Using an AI tool to generate entire lines of code is unacceptable.
|
||||||
|
|
||||||
|
The rationale behind this policy is that automated contributions are a waste of the maintainers' time. Humans spend their time and brainpower reviewing every submission. Issues or pull requests generated by automation tools create an imbalance of effort between the submitter and the reviewer. Nobody learns anything when a maintainer reviews code written by an LLM.
|
||||||
|
|
||||||
|
Additionally, AI-generated code conflicts with this project's license (Unlicense), since you cannot truly release code into the public domain if you didn't author it yourself.
|
||||||
|
|
||||||
|
|
||||||
# DEVELOPER INSTRUCTIONS
|
# DEVELOPER INSTRUCTIONS
|
||||||
@@ -768,12 +780,10 @@ view_count = int_or_none(video.get('views'))
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
# My pull request is labeled pending-fixes
|
## My pull request is labeled pending-fixes
|
||||||
|
|
||||||
The `pending-fixes` label is added when there are changes requested to a PR. When the necessary changes are made, the label should be removed. However, despite our best efforts, it may sometimes happen that the maintainer did not see the changes or forgot to remove the label. If your PR is still marked as `pending-fixes` a few days after all requested changes have been made, feel free to ping the maintainer who labeled your issue and ask them to re-review and remove the label.
|
The `pending-fixes` label is added when there are changes requested to a PR. When the necessary changes are made, the label should be removed. However, despite our best efforts, it may sometimes happen that the maintainer did not see the changes or forgot to remove the label. If your PR is still marked as `pending-fixes` a few days after all requested changes have been made, feel free to ping the maintainer who labeled your issue and ask them to re-review and remove the label.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# EMBEDDING YT-DLP
|
# EMBEDDING YT-DLP
|
||||||
See [README.md#embedding-yt-dlp](README.md#embedding-yt-dlp) for instructions on how to embed yt-dlp in another Python program
|
See [README.md#embedding-yt-dlp](README.md#embedding-yt-dlp) for instructions on how to embed yt-dlp in another Python program
|
||||||
|
|||||||
@@ -808,3 +808,13 @@ Randalix
|
|||||||
runarmod
|
runarmod
|
||||||
gitchasing
|
gitchasing
|
||||||
zakaryan2004
|
zakaryan2004
|
||||||
|
cdce8p
|
||||||
|
nicolaasjan
|
||||||
|
willsmillie
|
||||||
|
CasualYT31
|
||||||
|
cecilia-sanare
|
||||||
|
dhwz
|
||||||
|
robin-mu
|
||||||
|
shssoichiro
|
||||||
|
thanhtaivtt
|
||||||
|
uoag
|
||||||
|
|||||||
@@ -4,6 +4,99 @@
|
|||||||
# 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
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
### 2025.10.14
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- [Fix `prefer-vp9-sort` compat option](https://github.com/yt-dlp/yt-dlp/commit/a6673a8e82276ea529c1773ed09e5bc4a22e822a) ([#14603](https://github.com/yt-dlp/yt-dlp/issues/14603)) by [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **10play**
|
||||||
|
- [Handle geo-restriction errors](https://github.com/yt-dlp/yt-dlp/commit/ad55bfcfb700fbfc1364c04e3425761d6f95c0a7) ([#14618](https://github.com/yt-dlp/yt-dlp/issues/14618)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/eafedc21817bb0de20e9aaccd7151a1d4c4e1ebd) ([#14417](https://github.com/yt-dlp/yt-dlp/issues/14417)) by [seproDev](https://github.com/seproDev), [Sipherdrakon](https://github.com/Sipherdrakon)
|
||||||
|
- **abc.net.au**: [Support listen URLs](https://github.com/yt-dlp/yt-dlp/commit/0ea5d5882def84415f946907cfc00ab431c18fed) ([#14389](https://github.com/yt-dlp/yt-dlp/issues/14389)) by [uoag](https://github.com/uoag)
|
||||||
|
- **cbc.ca**: listen: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/df160ab18db523f6629f2e7e20123d7a3551df28) ([#14391](https://github.com/yt-dlp/yt-dlp/issues/14391)) by [uoag](https://github.com/uoag)
|
||||||
|
- **dropout**: [Update extractor for new domain](https://github.com/yt-dlp/yt-dlp/commit/8eb8695139dece6351aac10463df63b87b45b000) ([#14531](https://github.com/yt-dlp/yt-dlp/issues/14531)) by [cecilia-sanare](https://github.com/cecilia-sanare)
|
||||||
|
- **idagio**: [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/a98e7f9f58a9492d2cb216baa59c890ed8ce02f3) ([#14586](https://github.com/yt-dlp/yt-dlp/issues/14586)) by [robin-mu](https://github.com/robin-mu)
|
||||||
|
- **musescore**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/87be1bb96ac47abaaa4cfc6d7dd651e511b74551) ([#14598](https://github.com/yt-dlp/yt-dlp/issues/14598)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **prankcastpost**: [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/5d7678195a7d0c045a9fe0418383171a71a7ea43) ([#14445](https://github.com/yt-dlp/yt-dlp/issues/14445)) by [columndeeply](https://github.com/columndeeply)
|
||||||
|
- **slideslive**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/c2e124881f9aa02097589e853b3d3505e78372c4) ([#14619](https://github.com/yt-dlp/yt-dlp/issues/14619)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **soundcloud**: [Support new API URLs](https://github.com/yt-dlp/yt-dlp/commit/6d41aaf21c61a87e74564646abd0a8ee887e888d) ([#14449](https://github.com/yt-dlp/yt-dlp/issues/14449)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **tiktok**
|
||||||
|
- [Support browser impersonation](https://github.com/yt-dlp/yt-dlp/commit/5513036104ed9710f624c537fb3644b07a0680db) ([#14473](https://github.com/yt-dlp/yt-dlp/issues/14473)) by [bashonly](https://github.com/bashonly), [thanhtaivtt](https://github.com/thanhtaivtt)
|
||||||
|
- user: [Fix private account extraction](https://github.com/yt-dlp/yt-dlp/commit/cdc533b114c35ceb8a2e9dd3eb9c172a8737ae5e) ([#14585](https://github.com/yt-dlp/yt-dlp/issues/14585)) by [CasualYT31](https://github.com/CasualYT31)
|
||||||
|
- **vidyard**: [Extract chapters](https://github.com/yt-dlp/yt-dlp/commit/5f94f054907c12e68129cd9ac2508ed8aba1b223) ([#14478](https://github.com/yt-dlp/yt-dlp/issues/14478)) by [exterrestris](https://github.com/exterrestris)
|
||||||
|
- **xhamster**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/739125d40f8ede3beb7be68fc4df55bec0d226fd) ([#14446](https://github.com/yt-dlp/yt-dlp/issues/14446)) by [dhwz](https://github.com/dhwz), [dirkf](https://github.com/dirkf), [shssoichiro](https://github.com/shssoichiro)
|
||||||
|
- **youtube**
|
||||||
|
- [Detect experiment binding GVS PO Token to video id](https://github.com/yt-dlp/yt-dlp/commit/bd5ed90419eea18adfb2f0d8efa9d22b2029119f) ([#14471](https://github.com/yt-dlp/yt-dlp/issues/14471)) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
- tab: [Fix approximate timestamp extraction for feeds](https://github.com/yt-dlp/yt-dlp/commit/ccc25d6710a4aa373b7e15c558e07f8a2ffae5f3) ([#14539](https://github.com/yt-dlp/yt-dlp/issues/14539)) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
|
||||||
|
### 2025.09.26
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **twitch**: vod: [Fix `live_status` detection](https://github.com/yt-dlp/yt-dlp/commit/50e452fd7dfb8a648bd3b9aaabc8f94f37ce2051) ([#14457](https://github.com/yt-dlp/yt-dlp/issues/14457)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **youtube**
|
||||||
|
- [Fix player JS overrides](https://github.com/yt-dlp/yt-dlp/commit/b7b7910d96359a539b7997890342ab4a59dd685d) ([#14430](https://github.com/yt-dlp/yt-dlp/issues/14430)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
||||||
|
- [Improve PO token logging](https://github.com/yt-dlp/yt-dlp/commit/7df5acc546dccd32213c3a125d721e32b06d71b0) ([#14447](https://github.com/yt-dlp/yt-dlp/issues/14447)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- [Player client maintenance](https://github.com/yt-dlp/yt-dlp/commit/94c5622be96474ca3c637e52898c4daee4d8fb69) ([#14448](https://github.com/yt-dlp/yt-dlp/issues/14448)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- [Replace `tv_simply` with `web_safari` in default clients](https://github.com/yt-dlp/yt-dlp/commit/12b57d2858845c0c7fb33bf9aa8ed7be6905535d) ([#14465](https://github.com/yt-dlp/yt-dlp/issues/14465)) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
### 2025.09.23
|
||||||
|
|
||||||
|
#### Important changes
|
||||||
|
- **Several options have been deprecated**
|
||||||
|
In order to simplify the codebase and reduce maintenance burden, various options have been deprecated. Please remove them from your commands/configurations. [Read more](https://github.com/yt-dlp/yt-dlp/issues/14198)
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- **compat**: [Add `compat_datetime_from_timestamp`](https://github.com/yt-dlp/yt-dlp/commit/6a763a55d8a93b2a964ecf7699248ad342485412) ([#11902](https://github.com/yt-dlp/yt-dlp/issues/11902)) by [pzhlkj6612](https://github.com/pzhlkj6612), [seproDev](https://github.com/seproDev)
|
||||||
|
- **utils**
|
||||||
|
- `mimetype2ext`: [Recognize `vnd.dlna.mpeg-tts`](https://github.com/yt-dlp/yt-dlp/commit/98b6b0d339130e955f9d45ce67c0357c633c1627) ([#14388](https://github.com/yt-dlp/yt-dlp/issues/14388)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- `random_user_agent`: [Bump versions](https://github.com/yt-dlp/yt-dlp/commit/f3829463c728a5b5e62b3fc157e71c99b26edac7) ([#14317](https://github.com/yt-dlp/yt-dlp/issues/14317)) by [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **10play**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/067062bb87ac057e453ce9efdac7ca117a6a7da0) ([#14242](https://github.com/yt-dlp/yt-dlp/issues/14242)) by [Sipherdrakon](https://github.com/Sipherdrakon)
|
||||||
|
- **applepodcast**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/b2c01d0498653e0239c7226c5a7fcb614dd4dbc8) ([#14372](https://github.com/yt-dlp/yt-dlp/issues/14372)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **loco**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/f5cb721185e8725cf4eb4080e86aa9aa73ef25b3) ([#14256](https://github.com/yt-dlp/yt-dlp/issues/14256)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **mitele**: [Remove extractor](https://github.com/yt-dlp/yt-dlp/commit/820c6e244571557fcfc127d4b3680e2d07c04dca) ([#14348](https://github.com/yt-dlp/yt-dlp/issues/14348)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **newspicks**: [Warn when only preview is available](https://github.com/yt-dlp/yt-dlp/commit/9def9a4b0e958285e055eb350e5dd43b5c423336) ([#14197](https://github.com/yt-dlp/yt-dlp/issues/14197)) by [doe1080](https://github.com/doe1080)
|
||||||
|
- **onsen**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/17bfaa53edf5c52fce73cf0cef4592f929c2462d) ([#10971](https://github.com/yt-dlp/yt-dlp/issues/10971)) by [doe1080](https://github.com/doe1080)
|
||||||
|
- **pixivsketch**: [Remove extractors](https://github.com/yt-dlp/yt-dlp/commit/3d9a88bd8ef149d781c7e569e48e61551eda395e) ([#14196](https://github.com/yt-dlp/yt-dlp/issues/14196)) by [doe1080](https://github.com/doe1080)
|
||||||
|
- **smotrim**: [Rework extractors](https://github.com/yt-dlp/yt-dlp/commit/8cb037c0b06c2815080f87d61ea2e95c412785fc) ([#14200](https://github.com/yt-dlp/yt-dlp/issues/14200)) by [doe1080](https://github.com/doe1080), [swayll](https://github.com/swayll)
|
||||||
|
- **telecinco**: [Support browser impersonation](https://github.com/yt-dlp/yt-dlp/commit/e123a48f1155703d8709a4221a42bd45c0a2b3ce) ([#14351](https://github.com/yt-dlp/yt-dlp/issues/14351)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **tiktok**: live: [Fix room ID extraction](https://github.com/yt-dlp/yt-dlp/commit/5c1abcdc49b9d23e1dcb77b95d063cf2bf93e352) ([#14287](https://github.com/yt-dlp/yt-dlp/issues/14287)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **ttinglive**: [Adapt FlexTV extractor to new domain](https://github.com/yt-dlp/yt-dlp/commit/4bc19adc8798e7564513898cf34adc432c6c5709) ([#14375](https://github.com/yt-dlp/yt-dlp/issues/14375)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **tunein**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/7d9e48b22a780c2e8d2d2d68940d49fd2029ab70) ([#13981](https://github.com/yt-dlp/yt-dlp/issues/13981)) by [doe1080](https://github.com/doe1080)
|
||||||
|
- **twitch**: clips: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/f8750504c2f71b54586fb857d60dce4e354a13ea) ([#14397](https://github.com/yt-dlp/yt-dlp/issues/14397)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **vimeo**: [Fix login error handling](https://github.com/yt-dlp/yt-dlp/commit/679587dac7cd011a1472255e1f06efb017ba91b6) ([#14280](https://github.com/yt-dlp/yt-dlp/issues/14280)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **vk**
|
||||||
|
- [Support vksport URLs](https://github.com/yt-dlp/yt-dlp/commit/b81e9272dce5844e8fba371cb4b4fd95ad3ed819) ([#14341](https://github.com/yt-dlp/yt-dlp/issues/14341)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- uservideos: [Support alternate URL format](https://github.com/yt-dlp/yt-dlp/commit/bf5d18016b03a3f2fd5d3494d9efe85d3f8beeac) ([#14376](https://github.com/yt-dlp/yt-dlp/issues/14376)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **xhamster**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/a1c98226a4e869a34cc764a9dcf7a4558516308e) ([#14286](https://github.com/yt-dlp/yt-dlp/issues/14286)) by [nicolaasjan](https://github.com/nicolaasjan), [willsmillie](https://github.com/willsmillie) (With fixes in [677997d](https://github.com/yt-dlp/yt-dlp/commit/677997d84eaec0037397f7d935386daa3025b004) by [arand](https://github.com/arand), [thegymguy](https://github.com/thegymguy))
|
||||||
|
- **youtube**: [Force player `0004de42`](https://github.com/yt-dlp/yt-dlp/commit/7f5d9f8543d19590eeec9473d54fa00151afa78a) ([#14398](https://github.com/yt-dlp/yt-dlp/issues/14398)) by [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **build**
|
||||||
|
- [Fix cache warmer](https://github.com/yt-dlp/yt-dlp/commit/8597a4331e8535a246d777bb8397bdcab251766c) ([#14261](https://github.com/yt-dlp/yt-dlp/issues/14261)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Post-release workflow cleanup](https://github.com/yt-dlp/yt-dlp/commit/cd94e7004036e0149d7d3fa236c7dd44cf460788) ([#14250](https://github.com/yt-dlp/yt-dlp/issues/14250)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Refactor Linux build jobs](https://github.com/yt-dlp/yt-dlp/commit/e2d37bcc8e84be9ce0f67fc24cb830c13963d10f) ([#14275](https://github.com/yt-dlp/yt-dlp/issues/14275)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Use PyInstaller 6.16 for Windows](https://github.com/yt-dlp/yt-dlp/commit/df4b4e8ccf3385be6d2ad65465a0704c223dfdfb) ([#14318](https://github.com/yt-dlp/yt-dlp/issues/14318)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Use SPDX license identifier](https://github.com/yt-dlp/yt-dlp/commit/48a214bef4bfd5984362d3d24b09dce50ba449ea) ([#14260](https://github.com/yt-dlp/yt-dlp/issues/14260)) by [cdce8p](https://github.com/cdce8p)
|
||||||
|
- [Use new PyInstaller builds for Windows](https://github.com/yt-dlp/yt-dlp/commit/c8ede5f34d6c95c442b936bb01ecbcb724aefdef) ([#14273](https://github.com/yt-dlp/yt-dlp/issues/14273)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **ci**
|
||||||
|
- [Bump actions/setup-python to v6](https://github.com/yt-dlp/yt-dlp/commit/22ea0688ed6bcdbe4c51401a84239cda3decfc9c) ([#14282](https://github.com/yt-dlp/yt-dlp/issues/14282)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Improve workflow checks](https://github.com/yt-dlp/yt-dlp/commit/ae3923b6b23bc62115be55510d6b5842f7a46b5f) ([#14316](https://github.com/yt-dlp/yt-dlp/issues/14316)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Test and lint workflows](https://github.com/yt-dlp/yt-dlp/commit/7c9b10ebc83907d37f9f65ea9d4bd6f5e3bd1371) ([#14249](https://github.com/yt-dlp/yt-dlp/issues/14249)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Test with Python 3.14](https://github.com/yt-dlp/yt-dlp/commit/83b8409366d0f9554eaeae56394b244dab64a2cb) ([#13468](https://github.com/yt-dlp/yt-dlp/issues/13468)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **cleanup**
|
||||||
|
- [Bump ruff to 0.13.x](https://github.com/yt-dlp/yt-dlp/commit/ba8044685537e8e14adc6826fb4d730856fd2e2b) ([#14293](https://github.com/yt-dlp/yt-dlp/issues/14293)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Deprecate various options](https://github.com/yt-dlp/yt-dlp/commit/08d78996831bd8e1e3c2592d740c3def00bbf548) ([#13821](https://github.com/yt-dlp/yt-dlp/issues/13821)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- [Remove broken extractors](https://github.com/yt-dlp/yt-dlp/commit/65e90aea29cf3bfc9d1ae3e009fbf9a8db3a23c9) ([#14305](https://github.com/yt-dlp/yt-dlp/issues/14305)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Remove setup.cfg](https://github.com/yt-dlp/yt-dlp/commit/eb4b3a5fc7765a6cd0370ca44ccee0d7d5111dd7) ([#14314](https://github.com/yt-dlp/yt-dlp/issues/14314)) by [seproDev](https://github.com/seproDev) (With fixes in [8ab262c](https://github.com/yt-dlp/yt-dlp/commit/8ab262c66bd3e1d8874fb2d070068ba1f0d48f16) by [bashonly](https://github.com/bashonly))
|
||||||
|
- Miscellaneous: [2e81e29](https://github.com/yt-dlp/yt-dlp/commit/2e81e298cdce23afadb06a95836284acb38f7018) by [bashonly](https://github.com/bashonly), [doe1080](https://github.com/doe1080), [seproDev](https://github.com/seproDev)
|
||||||
|
- **docs**
|
||||||
|
- [Clarify license of PyInstaller-bundled executables](https://github.com/yt-dlp/yt-dlp/commit/e6e6b512141e66b1b36058966804fe59c02a2b4d) ([#14257](https://github.com/yt-dlp/yt-dlp/issues/14257)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- [Establish AI/LLM contribution policy](https://github.com/yt-dlp/yt-dlp/commit/8821682f15af59047bc1f92724ef8a9ba30d6f7e) ([#14194](https://github.com/yt-dlp/yt-dlp/issues/14194)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
||||||
|
- **test**: utils: [Fix `sanitize_path` test for Windows CPython 3.11](https://github.com/yt-dlp/yt-dlp/commit/a183837ec8bb5e28fe6eb3a9d77ea2d0d7a106bd) ([#13878](https://github.com/yt-dlp/yt-dlp/issues/13878)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
|
||||||
### 2025.09.05
|
### 2025.09.05
|
||||||
|
|
||||||
#### Core changes
|
#### Core changes
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ tar: yt-dlp.tar.gz
|
|||||||
# intended use: when building a source distribution,
|
# intended use: when building a source distribution,
|
||||||
# make pypi-files && python3 -m build -sn .
|
# make pypi-files && python3 -m build -sn .
|
||||||
pypi-files: AUTHORS Changelog.md LICENSE README.md README.txt supportedsites \
|
pypi-files: AUTHORS Changelog.md LICENSE README.md README.txt supportedsites \
|
||||||
completions yt-dlp.1 pyproject.toml setup.cfg devscripts/* test/*
|
completions yt-dlp.1 pyproject.toml devscripts/* test/*
|
||||||
|
|
||||||
.PHONY: all clean clean-all clean-test clean-dist clean-cache \
|
.PHONY: all clean clean-all clean-test clean-dist clean-cache \
|
||||||
completions completion-bash completion-fish completion-zsh \
|
completions completion-bash completion-fish completion-zsh \
|
||||||
@@ -159,7 +159,7 @@ yt-dlp.tar.gz: all
|
|||||||
README.md supportedsites.md Changelog.md LICENSE \
|
README.md supportedsites.md Changelog.md LICENSE \
|
||||||
CONTRIBUTING.md Collaborators.md CONTRIBUTORS AUTHORS \
|
CONTRIBUTING.md Collaborators.md CONTRIBUTORS AUTHORS \
|
||||||
Makefile yt-dlp.1 README.txt completions .gitignore \
|
Makefile yt-dlp.1 README.txt completions .gitignore \
|
||||||
setup.cfg yt-dlp yt_dlp pyproject.toml devscripts test
|
yt-dlp yt_dlp pyproject.toml devscripts test
|
||||||
|
|
||||||
AUTHORS: Changelog.md
|
AUTHORS: Changelog.md
|
||||||
@if [ -d '.git' ] && command -v git > /dev/null ; then \
|
@if [ -d '.git' ] && command -v git > /dev/null ; then \
|
||||||
|
|||||||
@@ -138,6 +138,17 @@ curl -L https://github.com/yt-dlp/yt-dlp/raw/master/public.key | gpg --import
|
|||||||
gpg --verify SHA2-256SUMS.sig SHA2-256SUMS
|
gpg --verify SHA2-256SUMS.sig SHA2-256SUMS
|
||||||
gpg --verify SHA2-512SUMS.sig SHA2-512SUMS
|
gpg --verify SHA2-512SUMS.sig SHA2-512SUMS
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Licensing
|
||||||
|
|
||||||
|
While yt-dlp is licensed under the [Unlicense](LICENSE), many of the release files contain code from other projects with different licenses.
|
||||||
|
|
||||||
|
Most notably, the PyInstaller-bundled executables include GPLv3+ licensed code, and as such the combined work is licensed under [GPLv3+](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||||
|
|
||||||
|
See [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt) for details.
|
||||||
|
|
||||||
|
The zipimport binary (`yt-dlp`), the source tarball (`yt-dlp.tar.gz`), and the PyPI source distribution & wheel only contain code licensed under the [Unlicense](LICENSE).
|
||||||
|
|
||||||
<!-- MANPAGE: END EXCLUDED SECTION -->
|
<!-- MANPAGE: END EXCLUDED SECTION -->
|
||||||
|
|
||||||
**Note**: The manpages, shell completion (autocomplete) files etc. are available inside the [source tarball](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)
|
**Note**: The manpages, shell completion (autocomplete) files etc. are available inside the [source tarball](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)
|
||||||
@@ -230,8 +241,6 @@ The following provide support for impersonating browser requests. This may be re
|
|||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
* [**avconv** and **avprobe**](https://www.libav.org) - Now **deprecated** alternative to ffmpeg. License [depends on the build](https://libav.org/legal)
|
|
||||||
* [**sponskrub**](https://github.com/faissaloo/SponSkrub) - For using the now **deprecated** [sponskrub options](#sponskrub-options). Licensed under [GPLv3+](https://github.com/faissaloo/SponSkrub/blob/master/LICENCE.md)
|
|
||||||
* [**rtmpdump**](http://rtmpdump.mplayerhq.hu) - For downloading `rtmp` streams. ffmpeg can be used instead with `--downloader ffmpeg`. Licensed under [GPLv2+](http://rtmpdump.mplayerhq.hu)
|
* [**rtmpdump**](http://rtmpdump.mplayerhq.hu) - For downloading `rtmp` streams. ffmpeg can be used instead with `--downloader ffmpeg`. Licensed under [GPLv2+](http://rtmpdump.mplayerhq.hu)
|
||||||
* [**mplayer**](http://mplayerhq.hu/design7/info.html) or [**mpv**](https://mpv.io) - For downloading `rstp`/`mms` streams. ffmpeg can be used instead with `--downloader ffmpeg`. Licensed under [GPLv2+](https://github.com/mpv-player/mpv/blob/master/Copyright)
|
* [**mplayer**](http://mplayerhq.hu/design7/info.html) or [**mpv**](https://mpv.io) - For downloading `rstp`/`mms` streams. ffmpeg can be used instead with `--downloader ffmpeg`. Licensed under [GPLv2+](https://github.com/mpv-player/mpv/blob/master/Copyright)
|
||||||
|
|
||||||
@@ -311,7 +320,6 @@ Tip: Use `CTRL`+`F` (or `Command`+`F`) to search by keywords
|
|||||||
playlist (default)
|
playlist (default)
|
||||||
--abort-on-error Abort downloading of further videos if an
|
--abort-on-error Abort downloading of further videos if an
|
||||||
error occurs (Alias: --no-ignore-errors)
|
error occurs (Alias: --no-ignore-errors)
|
||||||
--dump-user-agent Display the current user-agent and exit
|
|
||||||
--list-extractors List all supported extractors and exit
|
--list-extractors List all supported extractors and exit
|
||||||
--extractor-descriptions Output descriptions of all supported
|
--extractor-descriptions Output descriptions of all supported
|
||||||
extractors and exit
|
extractors and exit
|
||||||
@@ -562,8 +570,6 @@ Tip: Use `CTRL`+`F` (or `Command`+`F`) to search by keywords
|
|||||||
--playlist-random and --playlist-reverse
|
--playlist-random and --playlist-reverse
|
||||||
--no-lazy-playlist Process videos in the playlist only after
|
--no-lazy-playlist Process videos in the playlist only after
|
||||||
the entire playlist is parsed (default)
|
the entire playlist is parsed (default)
|
||||||
--xattr-set-filesize Set file xattribute ytdl.filesize with
|
|
||||||
expected file size
|
|
||||||
--hls-use-mpegts Use the mpegts container for HLS videos;
|
--hls-use-mpegts Use the mpegts container for HLS videos;
|
||||||
allowing some players to play the video
|
allowing some players to play the video
|
||||||
while downloading, and reducing the chance
|
while downloading, and reducing the chance
|
||||||
@@ -587,9 +593,9 @@ Tip: Use `CTRL`+`F` (or `Command`+`F`) to search by keywords
|
|||||||
use (optionally) prefixed by the protocols
|
use (optionally) prefixed by the protocols
|
||||||
(http, ftp, m3u8, dash, rstp, rtmp, mms) to
|
(http, ftp, m3u8, dash, rstp, rtmp, mms) to
|
||||||
use it for. Currently supports native,
|
use it for. Currently supports native,
|
||||||
aria2c, avconv, axel, curl, ffmpeg, httpie,
|
aria2c, axel, curl, ffmpeg, httpie, wget.
|
||||||
wget. You can use this option multiple times
|
You can use this option multiple times to
|
||||||
to set different downloaders for different
|
set different downloaders for different
|
||||||
protocols. E.g. --downloader aria2c
|
protocols. E.g. --downloader aria2c
|
||||||
--downloader "dash,m3u8:native" will use
|
--downloader "dash,m3u8:native" will use
|
||||||
aria2c for http/ftp downloads, and the
|
aria2c for http/ftp downloads, and the
|
||||||
@@ -1808,11 +1814,12 @@ The following extractors use this feature:
|
|||||||
#### 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/_base.py](https://github.com/yt-dlp/yt-dlp/blob/415b4c9f955b1a0391204bd24a7132590e7b3bdb/yt_dlp/extractor/youtube/_base.py#L402-L409) for the 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/_base.py](https://github.com/yt-dlp/yt-dlp/blob/415b4c9f955b1a0391204bd24a7132590e7b3bdb/yt_dlp/extractor/youtube/_base.py#L402-L409) for the 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 currently available clients are `web`, `web_safari`, `web_embedded`, `web_music`, `web_creator`, `mweb`, `ios`, `android`, `android_vr`, `tv`, `tv_simply` and `tv_embedded`. By default, `tv_simply,tv,web` is used, but `tv,web_safari,web` is used when authenticating with cookies and `tv,web_creator,web` is used with premium accounts. The `web_music` client is added for `music.youtube.com` URLs when logged-in cookies are used. The `web_embedded` client is added for age-restricted videos but only works if the video is embeddable. The `tv_embedded` and `web_creator` clients are added for age-restricted videos if account age-verification is required. Some clients, such as `web` and `web_music`, require a `po_token` for their formats to be downloadable. Some clients, such as `web_creator`, will only work with authentication. Not all clients support authentication via cookies. You can use `default` for the default clients, or you can use `all` for all clients (not recommended). You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=default,-ios`
|
* `player_client`: Clients to extract video data from. The currently available clients are `web`, `web_safari`, `web_embedded`, `web_music`, `web_creator`, `mweb`, `ios`, `android`, `android_vr`, `tv`, `tv_simply` and `tv_embedded`. By default, `tv,web_safari,web` is used, and `tv,web_creator,web` is used with premium accounts. The `web_music` client is added for `music.youtube.com` URLs when logged-in cookies are used. The `web_embedded` client is added for age-restricted videos but only works if the video is embeddable. The `tv_embedded` and `web_creator` clients are added for age-restricted videos if account age-verification is required. Some clients, such as `web` and `web_music`, require a `po_token` for their formats to be downloadable. Some clients, such as `web_creator`, will only work with authentication. Not all clients support authentication via cookies. You can use `default` for the default clients, or you can use `all` for all clients (not recommended). You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=default,-ios`
|
||||||
* `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), `initial_data` (skip initial data/next ep request). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause issues such as missing formats or metadata. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) and [#12826](https://github.com/yt-dlp/yt-dlp/issues/12826) 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), `initial_data` (skip initial data/next ep request). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause issues such as missing formats or metadata. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) and [#12826](https://github.com/yt-dlp/yt-dlp/issues/12826) for more details
|
||||||
* `webpage_skip`: Skip extraction of embedded webpage data. One or both of `player_response`, `initial_data`. These options are for testing purposes and don't skip any network requests
|
* `webpage_skip`: Skip extraction of embedded webpage data. One or both of `player_response`, `initial_data`. These options are for testing purposes and don't skip any network requests
|
||||||
* `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.
|
||||||
* `player_js_variant`: The player javascript variant to use for signature and nsig deciphering. The known variants are: `main`, `tce`, `tv`, `tv_es6`, `phone`, `tablet`. The default is `main`, and the others are for debugging purposes. You can use `actual` to go with what is prescribed by the site
|
* `player_js_variant`: The player javascript variant to use for n/sig deciphering. The known variants are: `main`, `tcc`, `tce`, `es5`, `es6`, `tv`, `tv_es6`, `phone`, `tablet`. The default is `main`, and the others are for debugging purposes. You can use `actual` to go with what is prescribed by the site
|
||||||
|
* `player_js_version`: The player javascript version to use for n/sig deciphering, in the format of `signature_timestamp@hash`. Currently, the default is to force `20348@0004de42`. You can use `actual` to go with what is prescribed by the site
|
||||||
* `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)
|
||||||
* `max_comments`: Limit the amount of comments to gather. Comma-separated list of integers representing `max-comments,max-parents,max-replies,max-replies-per-thread`. Default is `all,all,all,all`
|
* `max_comments`: Limit the amount of comments to gather. Comma-separated list of integers representing `max-comments,max-parents,max-replies,max-replies-per-thread`. Default is `all,all,all,all`
|
||||||
* E.g. `all,all,1000,10` will get a maximum of 1000 replies total, with up to 10 replies per thread. `1000,all,100` will get a maximum of 1000 comments, with a maximum of 100 replies total
|
* E.g. `all,all,1000,10` will get a maximum of 1000 replies total, with up to 10 replies per thread. `1000,all,100` will get a maximum of 1000 comments, with a maximum of 100 replies total
|
||||||
@@ -2207,7 +2214,6 @@ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|||||||
* Fix for [n-sig based throttling](https://github.com/ytdl-org/youtube-dl/issues/29326) **\***
|
* Fix for [n-sig based throttling](https://github.com/ytdl-org/youtube-dl/issues/29326) **\***
|
||||||
* Download livestreams from the start using `--live-from-start` (*experimental*)
|
* Download livestreams from the start using `--live-from-start` (*experimental*)
|
||||||
* Channel URLs download all uploads of the channel, including shorts and live
|
* Channel URLs download all uploads of the channel, including shorts and live
|
||||||
* Support for [logging in with OAuth](https://github.com/yt-dlp/yt-dlp/wiki/Extractors#logging-in-with-oauth)
|
|
||||||
|
|
||||||
* **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[+KEYRING][:PROFILE][::CONTAINER]`
|
* **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[+KEYRING][:PROFILE][::CONTAINER]`
|
||||||
|
|
||||||
@@ -2351,11 +2357,7 @@ While these options still work, their use is not recommended since there are oth
|
|||||||
--hls-prefer-native --downloader "m3u8:native"
|
--hls-prefer-native --downloader "m3u8:native"
|
||||||
--hls-prefer-ffmpeg --downloader "m3u8:ffmpeg"
|
--hls-prefer-ffmpeg --downloader "m3u8:ffmpeg"
|
||||||
--list-formats-old --compat-options list-formats (Alias: --no-list-formats-as-table)
|
--list-formats-old --compat-options list-formats (Alias: --no-list-formats-as-table)
|
||||||
--list-formats-as-table --compat-options -list-formats [Default] (Alias: --no-list-formats-old)
|
--list-formats-as-table --compat-options -list-formats [Default]
|
||||||
--youtube-skip-dash-manifest --extractor-args "youtube:skip=dash" (Alias: --no-youtube-include-dash-manifest)
|
|
||||||
--youtube-skip-hls-manifest --extractor-args "youtube:skip=hls" (Alias: --no-youtube-include-hls-manifest)
|
|
||||||
--youtube-include-dash-manifest Default (Alias: --no-youtube-skip-dash-manifest)
|
|
||||||
--youtube-include-hls-manifest Default (Alias: --no-youtube-skip-hls-manifest)
|
|
||||||
--geo-bypass --xff "default"
|
--geo-bypass --xff "default"
|
||||||
--no-geo-bypass --xff "never"
|
--no-geo-bypass --xff "never"
|
||||||
--geo-bypass-country CODE --xff CODE
|
--geo-bypass-country CODE --xff CODE
|
||||||
@@ -2366,18 +2368,13 @@ These options are not intended to be used by the end-user
|
|||||||
|
|
||||||
--test Download only part of video for testing extractors
|
--test Download only part of video for testing extractors
|
||||||
--load-pages Load pages dumped by --write-pages
|
--load-pages Load pages dumped by --write-pages
|
||||||
--youtube-print-sig-code For testing youtube signatures
|
|
||||||
--allow-unplayable-formats List unplayable formats also
|
--allow-unplayable-formats List unplayable formats also
|
||||||
--no-allow-unplayable-formats Default
|
--no-allow-unplayable-formats Default
|
||||||
|
|
||||||
#### Old aliases
|
#### Old aliases
|
||||||
These are aliases that are no longer documented for various reasons
|
These are aliases that are no longer documented for various reasons
|
||||||
|
|
||||||
--avconv-location --ffmpeg-location
|
|
||||||
--clean-infojson --clean-info-json
|
--clean-infojson --clean-info-json
|
||||||
--cn-verification-proxy URL --geo-verification-proxy URL
|
|
||||||
--dump-headers --print-traffic
|
|
||||||
--dump-intermediate-pages --dump-pages
|
|
||||||
--force-write-download-archive --force-write-archive
|
--force-write-download-archive --force-write-archive
|
||||||
--no-clean-infojson --no-clean-info-json
|
--no-clean-infojson --no-clean-info-json
|
||||||
--no-split-tracks --no-split-chapters
|
--no-split-tracks --no-split-chapters
|
||||||
@@ -2391,7 +2388,7 @@ These are aliases that are no longer documented for various reasons
|
|||||||
--yes-overwrites --force-overwrites
|
--yes-overwrites --force-overwrites
|
||||||
|
|
||||||
#### Sponskrub Options
|
#### Sponskrub Options
|
||||||
Support for [SponSkrub](https://github.com/faissaloo/SponSkrub) has been deprecated in favor of the `--sponsorblock` options
|
Support for [SponSkrub](https://github.com/faissaloo/SponSkrub) has been removed in favor of the `--sponsorblock` options
|
||||||
|
|
||||||
--sponskrub --sponsorblock-mark all
|
--sponskrub --sponsorblock-mark all
|
||||||
--no-sponskrub --no-sponsorblock
|
--no-sponskrub --no-sponsorblock
|
||||||
@@ -2413,6 +2410,17 @@ These options may no longer work as intended
|
|||||||
--no-include-ads Default
|
--no-include-ads Default
|
||||||
--write-annotations No supported site has annotations now
|
--write-annotations No supported site has annotations now
|
||||||
--no-write-annotations Default
|
--no-write-annotations Default
|
||||||
|
--avconv-location Removed alias for --ffmpeg-location
|
||||||
|
--cn-verification-proxy URL Removed alias for --geo-verification-proxy URL
|
||||||
|
--dump-headers Removed alias for --print-traffic
|
||||||
|
--dump-intermediate-pages Removed alias for --dump-pages
|
||||||
|
--youtube-skip-dash-manifest Removed alias for --extractor-args "youtube:skip=dash" (Alias: --no-youtube-include-dash-manifest)
|
||||||
|
--youtube-skip-hls-manifest Removed alias for --extractor-args "youtube:skip=hls" (Alias: --no-youtube-include-hls-manifest)
|
||||||
|
--youtube-include-dash-manifest Default (Alias: --no-youtube-skip-dash-manifest)
|
||||||
|
--youtube-include-hls-manifest Default (Alias: --no-youtube-skip-hls-manifest)
|
||||||
|
--youtube-print-sig-code Removed testing functionality
|
||||||
|
--dump-user-agent No longer supported
|
||||||
|
--xattr-set-filesize No longer supported
|
||||||
--compat-options seperate-video-versions No longer needed
|
--compat-options seperate-video-versions No longer needed
|
||||||
--compat-options no-youtube-prefer-utc-upload-date No longer supported
|
--compat-options no-youtube-prefer-utc-upload-date No longer supported
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,9 @@ services:
|
|||||||
CHANNEL: ${CHANNEL:?}
|
CHANNEL: ${CHANNEL:?}
|
||||||
ORIGIN: ${ORIGIN:?}
|
ORIGIN: ${ORIGIN:?}
|
||||||
VERSION:
|
VERSION:
|
||||||
|
PYTHON_VERSION:
|
||||||
|
SKIP_ONEDIR_BUILD:
|
||||||
|
SKIP_ONEFILE_BUILD:
|
||||||
volumes:
|
volumes:
|
||||||
- ../..:/yt-dlp
|
- ../..:/yt-dlp
|
||||||
|
|
||||||
@@ -26,6 +29,9 @@ services:
|
|||||||
VERIFYIMAGE: quay.io/pypa/manylinux2014_x86_64:latest
|
VERIFYIMAGE: quay.io/pypa/manylinux2014_x86_64:latest
|
||||||
environment:
|
environment:
|
||||||
EXE_NAME: ${EXE_NAME:?}
|
EXE_NAME: ${EXE_NAME:?}
|
||||||
|
UPDATE_TO:
|
||||||
|
SKIP_ONEDIR_BUILD:
|
||||||
|
SKIP_ONEFILE_BUILD:
|
||||||
volumes:
|
volumes:
|
||||||
- ../../dist:/build
|
- ../../dist:/build
|
||||||
|
|
||||||
@@ -42,6 +48,9 @@ services:
|
|||||||
CHANNEL: ${CHANNEL:?}
|
CHANNEL: ${CHANNEL:?}
|
||||||
ORIGIN: ${ORIGIN:?}
|
ORIGIN: ${ORIGIN:?}
|
||||||
VERSION:
|
VERSION:
|
||||||
|
PYTHON_VERSION:
|
||||||
|
SKIP_ONEDIR_BUILD:
|
||||||
|
SKIP_ONEFILE_BUILD:
|
||||||
volumes:
|
volumes:
|
||||||
- ../..:/yt-dlp
|
- ../..:/yt-dlp
|
||||||
|
|
||||||
@@ -55,7 +64,9 @@ services:
|
|||||||
VERIFYIMAGE: quay.io/pypa/manylinux2014_aarch64:latest
|
VERIFYIMAGE: quay.io/pypa/manylinux2014_aarch64:latest
|
||||||
environment:
|
environment:
|
||||||
EXE_NAME: ${EXE_NAME:?}
|
EXE_NAME: ${EXE_NAME:?}
|
||||||
SKIP_UPDATE_TO: "1" # TODO: remove when there is a glibc2.17 aarch64 release to --update-to
|
UPDATE_TO:
|
||||||
|
SKIP_ONEDIR_BUILD:
|
||||||
|
SKIP_ONEFILE_BUILD:
|
||||||
volumes:
|
volumes:
|
||||||
- ../../dist:/build
|
- ../../dist:/build
|
||||||
|
|
||||||
@@ -72,10 +83,12 @@ services:
|
|||||||
CHANNEL: ${CHANNEL:?}
|
CHANNEL: ${CHANNEL:?}
|
||||||
ORIGIN: ${ORIGIN:?}
|
ORIGIN: ${ORIGIN:?}
|
||||||
VERSION:
|
VERSION:
|
||||||
SKIP_ONEFILE_BUILD: "1"
|
PYTHON_VERSION:
|
||||||
|
SKIP_ONEDIR_BUILD:
|
||||||
|
SKIP_ONEFILE_BUILD:
|
||||||
volumes:
|
volumes:
|
||||||
- ../..:/yt-dlp
|
- ../..:/yt-dlp
|
||||||
- ~/yt-dlp-build-venv:/yt-dlp-build-venv
|
- ../../venv:/yt-dlp-build-venv
|
||||||
|
|
||||||
linux_armv7l_verify:
|
linux_armv7l_verify:
|
||||||
build:
|
build:
|
||||||
@@ -87,7 +100,9 @@ services:
|
|||||||
VERIFYIMAGE: arm32v7/debian:bullseye
|
VERIFYIMAGE: arm32v7/debian:bullseye
|
||||||
environment:
|
environment:
|
||||||
EXE_NAME: ${EXE_NAME:?}
|
EXE_NAME: ${EXE_NAME:?}
|
||||||
TEST_ONEDIR_BUILD: "1"
|
UPDATE_TO:
|
||||||
|
SKIP_ONEDIR_BUILD:
|
||||||
|
SKIP_ONEFILE_BUILD:
|
||||||
volumes:
|
volumes:
|
||||||
- ../../dist:/build
|
- ../../dist:/build
|
||||||
|
|
||||||
@@ -104,6 +119,9 @@ services:
|
|||||||
CHANNEL: ${CHANNEL:?}
|
CHANNEL: ${CHANNEL:?}
|
||||||
ORIGIN: ${ORIGIN:?}
|
ORIGIN: ${ORIGIN:?}
|
||||||
VERSION:
|
VERSION:
|
||||||
|
PYTHON_VERSION:
|
||||||
|
SKIP_ONEDIR_BUILD:
|
||||||
|
SKIP_ONEFILE_BUILD:
|
||||||
volumes:
|
volumes:
|
||||||
- ../..:/yt-dlp
|
- ../..:/yt-dlp
|
||||||
|
|
||||||
@@ -117,7 +135,9 @@ services:
|
|||||||
VERIFYIMAGE: alpine:3.22
|
VERIFYIMAGE: alpine:3.22
|
||||||
environment:
|
environment:
|
||||||
EXE_NAME: ${EXE_NAME:?}
|
EXE_NAME: ${EXE_NAME:?}
|
||||||
SKIP_UPDATE_TO: "1" # TODO: remove when there is a musllinux_aarch64 release to --update-to
|
UPDATE_TO:
|
||||||
|
SKIP_ONEDIR_BUILD:
|
||||||
|
SKIP_ONEFILE_BUILD:
|
||||||
volumes:
|
volumes:
|
||||||
- ../../dist:/build
|
- ../../dist:/build
|
||||||
|
|
||||||
@@ -134,6 +154,9 @@ services:
|
|||||||
CHANNEL: ${CHANNEL:?}
|
CHANNEL: ${CHANNEL:?}
|
||||||
ORIGIN: ${ORIGIN:?}
|
ORIGIN: ${ORIGIN:?}
|
||||||
VERSION:
|
VERSION:
|
||||||
|
PYTHON_VERSION:
|
||||||
|
SKIP_ONEDIR_BUILD:
|
||||||
|
SKIP_ONEFILE_BUILD:
|
||||||
EXCLUDE_CURL_CFFI: "1"
|
EXCLUDE_CURL_CFFI: "1"
|
||||||
volumes:
|
volumes:
|
||||||
- ../..:/yt-dlp
|
- ../..:/yt-dlp
|
||||||
@@ -148,6 +171,8 @@ services:
|
|||||||
VERIFYIMAGE: alpine:3.22
|
VERIFYIMAGE: alpine:3.22
|
||||||
environment:
|
environment:
|
||||||
EXE_NAME: ${EXE_NAME:?}
|
EXE_NAME: ${EXE_NAME:?}
|
||||||
SKIP_UPDATE_TO: "1" # TODO: remove when there is a musllinux_aarch64 release to --update-to
|
UPDATE_TO:
|
||||||
|
SKIP_ONEDIR_BUILD:
|
||||||
|
SKIP_ONEFILE_BUILD:
|
||||||
volumes:
|
volumes:
|
||||||
- ../../dist:/build
|
- ../../dist:/build
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -exuo pipefail
|
set -exuo pipefail
|
||||||
|
|
||||||
if [[ -z "${USE_PYTHON_VERSION:-}" ]]; then
|
if [[ -z "${PYTHON_VERSION:-}" ]]; then
|
||||||
USE_PYTHON_VERSION="3.13"
|
PYTHON_VERSION="3.13"
|
||||||
|
echo "Defaulting to using Python ${PYTHON_VERSION}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
function runpy {
|
function runpy {
|
||||||
"/opt/shared-cpython-${USE_PYTHON_VERSION}/bin/python${USE_PYTHON_VERSION}" "$@"
|
"/opt/shared-cpython-${PYTHON_VERSION}/bin/python${PYTHON_VERSION}" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
function venvpy {
|
function venvpy {
|
||||||
"python${USE_PYTHON_VERSION}" "$@"
|
"python${PYTHON_VERSION}" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
INCLUDES=(
|
INCLUDES=(
|
||||||
@@ -23,6 +24,7 @@ if [[ -z "${EXCLUDE_CURL_CFFI:-}" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
runpy -m venv /yt-dlp-build-venv
|
runpy -m venv /yt-dlp-build-venv
|
||||||
|
# shellcheck disable=SC1091
|
||||||
source /yt-dlp-build-venv/bin/activate
|
source /yt-dlp-build-venv/bin/activate
|
||||||
# Inside the venv we use venvpy instead of runpy
|
# Inside the venv we use venvpy instead of runpy
|
||||||
venvpy -m ensurepip --upgrade --default-pip
|
venvpy -m ensurepip --upgrade --default-pip
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
if [ -n "${TEST_ONEDIR_BUILD:-}" ]; then
|
if [ -n "${SKIP_ONEFILE_BUILD:-}" ]; then
|
||||||
|
if [ -n "${SKIP_ONEDIR_BUILD:-}" ]; then
|
||||||
|
echo "All executable builds were skipped"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "Extracting zip to verify onedir build"
|
echo "Extracting zip to verify onedir build"
|
||||||
if command -v python3 >/dev/null 2>&1; then
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
python3 -m zipfile -e "/build/${EXE_NAME}.zip" ./
|
python3 -m zipfile -e "/build/${EXE_NAME}.zip" ./
|
||||||
@@ -22,14 +26,17 @@ if [ -n "${TEST_ONEDIR_BUILD:-}" ]; then
|
|||||||
fi
|
fi
|
||||||
unzip "/build/${EXE_NAME}.zip" -d ./
|
unzip "/build/${EXE_NAME}.zip" -d ./
|
||||||
fi
|
fi
|
||||||
else
|
chmod +x "./${EXE_NAME}"
|
||||||
echo "Verifying onefile build"
|
"./${EXE_NAME}" -v || true
|
||||||
cp "/build/${EXE_NAME}" ./
|
"./${EXE_NAME}" --version
|
||||||
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Verifying onefile build"
|
||||||
|
cp "/build/${EXE_NAME}" ./
|
||||||
chmod +x "./${EXE_NAME}"
|
chmod +x "./${EXE_NAME}"
|
||||||
|
|
||||||
if [ -n "${SKIP_UPDATE_TO:-}" ] || [ -n "${TEST_ONEDIR_BUILD:-}" ]; then
|
if [ -z "${UPDATE_TO:-}" ]; then
|
||||||
"./${EXE_NAME}" -v || true
|
"./${EXE_NAME}" -v || true
|
||||||
"./${EXE_NAME}" --version
|
"./${EXE_NAME}" --version
|
||||||
exit 0
|
exit 0
|
||||||
@@ -37,7 +44,7 @@ fi
|
|||||||
|
|
||||||
cp "./${EXE_NAME}" "./${EXE_NAME}_downgraded"
|
cp "./${EXE_NAME}" "./${EXE_NAME}_downgraded"
|
||||||
version="$("./${EXE_NAME}" --version)"
|
version="$("./${EXE_NAME}" --version)"
|
||||||
"./${EXE_NAME}_downgraded" -v --update-to yt-dlp/yt-dlp@2023.03.04
|
"./${EXE_NAME}_downgraded" -v --update-to "${UPDATE_TO}"
|
||||||
downgraded_version="$("./${EXE_NAME}_downgraded" --version)"
|
downgraded_version="$("./${EXE_NAME}_downgraded" --version)"
|
||||||
if [ "${version}" = "${downgraded_version}" ]; then
|
if [ "${version}" = "${downgraded_version}" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -129,7 +129,6 @@ def windows_set_version(exe, version):
|
|||||||
StringStruct('FileDescription', 'yt-dlp%s' % (MACHINE and f' ({MACHINE})')),
|
StringStruct('FileDescription', 'yt-dlp%s' % (MACHINE and f' ({MACHINE})')),
|
||||||
StringStruct('FileVersion', version),
|
StringStruct('FileVersion', version),
|
||||||
StringStruct('InternalName', f'yt-dlp{suffix}'),
|
StringStruct('InternalName', f'yt-dlp{suffix}'),
|
||||||
StringStruct('LegalCopyright', 'pukkandan.ytdlp@gmail.com | UNLICENSE'),
|
|
||||||
StringStruct('OriginalFilename', f'yt-dlp{suffix}.exe'),
|
StringStruct('OriginalFilename', f'yt-dlp{suffix}.exe'),
|
||||||
StringStruct('ProductName', f'yt-dlp{suffix}'),
|
StringStruct('ProductName', f'yt-dlp{suffix}'),
|
||||||
StringStruct(
|
StringStruct(
|
||||||
|
|||||||
@@ -293,5 +293,10 @@
|
|||||||
"action": "add",
|
"action": "add",
|
||||||
"when": "c76ce28e06c816eb5b261dfb6aff6e69dd9b7382",
|
"when": "c76ce28e06c816eb5b261dfb6aff6e69dd9b7382",
|
||||||
"short": "[priority] **linux_armv7l_exe builds are being discontinued**\nThis release's `yt-dlp_linux_armv7l` binary could be the last one. [Read more](https://github.com/yt-dlp/yt-dlp/issues/13976)"
|
"short": "[priority] **linux_armv7l_exe builds are being discontinued**\nThis release's `yt-dlp_linux_armv7l` binary could be the last one. [Read more](https://github.com/yt-dlp/yt-dlp/issues/13976)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "add",
|
||||||
|
"when": "08d78996831bd8e1e3c2592d740c3def00bbf548",
|
||||||
|
"short": "[priority] **Several options have been deprecated**\nIn order to simplify the codebase and reduce maintenance burden, various options have been deprecated. Please remove them from your commands/configurations. [Read more](https://github.com/yt-dlp/yt-dlp/issues/14198)"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,316 @@
|
|||||||
|
import requests
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
DEFAULT_OUTPUT = 'THIRD_PARTY_LICENSES.txt'
|
||||||
|
CACHE_LOCATION = '.license_cache'
|
||||||
|
HEADER = '''THIRD-PARTY LICENSES
|
||||||
|
|
||||||
|
This file aggregates license texts of third-party components included with the yt-dlp PyInstaller-bundled executables.
|
||||||
|
yt-dlp itself is licensed under the Unlicense (see LICENSE file).
|
||||||
|
Source code for bundled third-party components is available from the original projects.
|
||||||
|
If you cannot obtain it, the maintainers will provide it as per license obligation; maintainer emails are listed in pyproject.toml.'''
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Dependency:
|
||||||
|
name: str
|
||||||
|
license_url: str
|
||||||
|
project_url: str = ''
|
||||||
|
license: str = ''
|
||||||
|
comment: str = ''
|
||||||
|
|
||||||
|
|
||||||
|
DEPENDENCIES: list[Dependency] = [
|
||||||
|
# Core runtime environment components
|
||||||
|
Dependency(
|
||||||
|
name='Python',
|
||||||
|
license='PSF-2.0',
|
||||||
|
license_url='https://raw.githubusercontent.com/python/cpython/refs/heads/main/LICENSE',
|
||||||
|
project_url='https://www.python.org/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='Microsoft Distributable Code',
|
||||||
|
license_url='https://raw.githubusercontent.com/python/cpython/refs/heads/main/PC/crtlicense.txt',
|
||||||
|
comment='Only included in Windows builds',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='bzip2',
|
||||||
|
license='bzip2-1.0.6',
|
||||||
|
license_url='https://gitlab.com/federicomenaquintero/bzip2/-/raw/master/COPYING',
|
||||||
|
project_url='https://sourceware.org/bzip2/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='libffi',
|
||||||
|
license='MIT',
|
||||||
|
license_url='https://raw.githubusercontent.com/libffi/libffi/refs/heads/master/LICENSE',
|
||||||
|
project_url='https://sourceware.org/libffi/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='OpenSSL 3.0+',
|
||||||
|
license='Apache-2.0',
|
||||||
|
license_url='https://raw.githubusercontent.com/openssl/openssl/refs/heads/master/LICENSE.txt',
|
||||||
|
project_url='https://www.openssl.org/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='SQLite',
|
||||||
|
license='Public Domain', # Technically does not need to be included
|
||||||
|
license_url='https://sqlite.org/src/raw/e108e1e69ae8e8a59e93c455654b8ac9356a11720d3345df2a4743e9590fb20d?at=LICENSE.md',
|
||||||
|
project_url='https://www.sqlite.org/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='liblzma',
|
||||||
|
license='0BSD', # Technically does not need to be included
|
||||||
|
license_url='https://raw.githubusercontent.com/tukaani-project/xz/refs/heads/master/COPYING',
|
||||||
|
project_url='https://tukaani.org/xz/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='mpdecimal',
|
||||||
|
license='BSD-2-Clause',
|
||||||
|
# No official repo URL
|
||||||
|
license_url='https://gist.githubusercontent.com/seproDev/9e5dbfc08af35c3f2463e64eb9b27161/raw/61f5a98bc1a4ad7d48b1c793fc3314d4d43c2ab1/mpdecimal_COPYRIGHT.txt',
|
||||||
|
project_url='https://www.bytereef.org/mpdecimal/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='zlib',
|
||||||
|
license='zlib',
|
||||||
|
license_url='https://raw.githubusercontent.com/madler/zlib/refs/heads/develop/LICENSE',
|
||||||
|
project_url='https://zlib.net/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='Expat',
|
||||||
|
license='MIT',
|
||||||
|
license_url='https://raw.githubusercontent.com/libexpat/libexpat/refs/heads/master/COPYING',
|
||||||
|
project_url='https://libexpat.github.io/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='ncurses',
|
||||||
|
license='X11-distribute-modifications-variant',
|
||||||
|
license_url='https://raw.githubusercontent.com/mirror/ncurses/refs/heads/master/COPYING',
|
||||||
|
comment='Only included in Linux/macOS builds',
|
||||||
|
project_url='https://invisible-island.net/ncurses/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='GNU Readline',
|
||||||
|
license='GPL-3.0-or-later',
|
||||||
|
license_url='https://tiswww.case.edu/php/chet/readline/COPYING',
|
||||||
|
comment='Only included in Linux builds',
|
||||||
|
project_url='https://www.gnu.org/software/readline/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='libstdc++',
|
||||||
|
license='GPL-3.0-with-GCC-exception',
|
||||||
|
license_url='https://raw.githubusercontent.com/gcc-mirror/gcc/refs/heads/master/COPYING.RUNTIME',
|
||||||
|
comment='Only included in Linux builds',
|
||||||
|
project_url='https://gcc.gnu.org/onlinedocs/libstdc++/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='libgcc',
|
||||||
|
license='GPL-3.0-with-GCC-exception',
|
||||||
|
license_url='https://raw.githubusercontent.com/gcc-mirror/gcc/refs/heads/master/COPYING.RUNTIME',
|
||||||
|
comment='Only included in Linux builds',
|
||||||
|
project_url='https://gcc.gnu.org/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='libuuid',
|
||||||
|
license='BSD-3-Clause',
|
||||||
|
license_url='https://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git/plain/lib/uuid/COPYING',
|
||||||
|
comment='Only included in Linux builds',
|
||||||
|
project_url='https://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git/tree/lib/uuid',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='libintl',
|
||||||
|
license='LGPL-2.1-or-later',
|
||||||
|
license_url='https://raw.githubusercontent.com/autotools-mirror/gettext/refs/heads/master/gettext-runtime/intl/COPYING.LIB',
|
||||||
|
comment='Only included in macOS builds',
|
||||||
|
project_url='https://www.gnu.org/software/gettext/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='libidn2',
|
||||||
|
license='LGPL-3.0-or-later',
|
||||||
|
license_url='https://gitlab.com/libidn/libidn2/-/raw/master/COPYING.LESSERv3',
|
||||||
|
comment='Only included in macOS builds',
|
||||||
|
project_url='https://www.gnu.org/software/libidn/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='libidn2 (Unicode character data files)',
|
||||||
|
license='Unicode-TOU AND Unicode-DFS-2016',
|
||||||
|
license_url='https://gitlab.com/libidn/libidn2/-/raw/master/COPYING.unicode',
|
||||||
|
comment='Only included in macOS builds',
|
||||||
|
project_url='https://www.gnu.org/software/libidn/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='libunistring',
|
||||||
|
license='LGPL-3.0-or-later',
|
||||||
|
license_url='https://gitweb.git.savannah.gnu.org/gitweb/?p=libunistring.git;a=blob_plain;f=COPYING.LIB;hb=HEAD',
|
||||||
|
comment='Only included in macOS builds',
|
||||||
|
project_url='https://www.gnu.org/software/libunistring/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='librtmp',
|
||||||
|
license='LGPL-2.1-or-later',
|
||||||
|
# No official repo URL
|
||||||
|
license_url='https://gist.githubusercontent.com/seproDev/31d8c691ccddebe37b8b379307cb232d/raw/053408e98547ea8c7d9ba3a80c965f33e163b881/librtmp_COPYING.txt',
|
||||||
|
comment='Only included in macOS builds',
|
||||||
|
project_url='https://rtmpdump.mplayerhq.hu/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='zstd',
|
||||||
|
license='BSD-3-Clause',
|
||||||
|
license_url='https://raw.githubusercontent.com/facebook/zstd/refs/heads/dev/LICENSE',
|
||||||
|
comment='Only included in macOS builds',
|
||||||
|
project_url='https://facebook.github.io/zstd/',
|
||||||
|
),
|
||||||
|
|
||||||
|
# Python packages
|
||||||
|
Dependency(
|
||||||
|
name='brotli',
|
||||||
|
license='MIT',
|
||||||
|
license_url='https://raw.githubusercontent.com/google/brotli/refs/heads/master/LICENSE',
|
||||||
|
project_url='https://brotli.org/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='curl_cffi',
|
||||||
|
license='MIT',
|
||||||
|
license_url='https://raw.githubusercontent.com/lexiforest/curl_cffi/refs/heads/main/LICENSE',
|
||||||
|
comment='Not included in `yt-dlp_x86` and `yt-dlp_musllinux_aarch64` builds',
|
||||||
|
project_url='https://curl-cffi.readthedocs.io/',
|
||||||
|
),
|
||||||
|
# Dependency of curl_cffi
|
||||||
|
Dependency(
|
||||||
|
name='curl-impersonate',
|
||||||
|
license='MIT',
|
||||||
|
license_url='https://raw.githubusercontent.com/lexiforest/curl-impersonate/refs/heads/main/LICENSE',
|
||||||
|
comment='Not included in `yt-dlp_x86` and `yt-dlp_musllinux_aarch64` builds',
|
||||||
|
project_url='https://github.com/lexiforest/curl-impersonate',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='cffi',
|
||||||
|
license='MIT-0', # Technically does not need to be included
|
||||||
|
license_url='https://raw.githubusercontent.com/python-cffi/cffi/refs/heads/main/LICENSE',
|
||||||
|
project_url='https://cffi.readthedocs.io/',
|
||||||
|
),
|
||||||
|
# Dependecy of cffi
|
||||||
|
Dependency(
|
||||||
|
name='pycparser',
|
||||||
|
license='BSD-3-Clause',
|
||||||
|
license_url='https://raw.githubusercontent.com/eliben/pycparser/refs/heads/main/LICENSE',
|
||||||
|
project_url='https://github.com/eliben/pycparser',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='mutagen',
|
||||||
|
license='GPL-2.0-or-later',
|
||||||
|
license_url='https://raw.githubusercontent.com/quodlibet/mutagen/refs/heads/main/COPYING',
|
||||||
|
project_url='https://mutagen.readthedocs.io/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='PyCryptodome',
|
||||||
|
license='Public Domain and BSD-2-Clause',
|
||||||
|
license_url='https://raw.githubusercontent.com/Legrandin/pycryptodome/refs/heads/master/LICENSE.rst',
|
||||||
|
project_url='https://www.pycryptodome.org/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='certifi',
|
||||||
|
license='MPL-2.0',
|
||||||
|
license_url='https://raw.githubusercontent.com/certifi/python-certifi/refs/heads/master/LICENSE',
|
||||||
|
project_url='https://github.com/certifi/python-certifi',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='requests',
|
||||||
|
license='Apache-2.0',
|
||||||
|
license_url='https://raw.githubusercontent.com/psf/requests/refs/heads/main/LICENSE',
|
||||||
|
project_url='https://requests.readthedocs.io/',
|
||||||
|
),
|
||||||
|
# Dependency of requests
|
||||||
|
Dependency(
|
||||||
|
name='charset-normalizer',
|
||||||
|
license='MIT',
|
||||||
|
license_url='https://raw.githubusercontent.com/jawah/charset_normalizer/refs/heads/master/LICENSE',
|
||||||
|
project_url='https://charset-normalizer.readthedocs.io/',
|
||||||
|
),
|
||||||
|
# Dependency of requests
|
||||||
|
Dependency(
|
||||||
|
name='idna',
|
||||||
|
license='BSD-3-Clause',
|
||||||
|
license_url='https://raw.githubusercontent.com/kjd/idna/refs/heads/master/LICENSE.md',
|
||||||
|
project_url='https://github.com/kjd/idna',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='urllib3',
|
||||||
|
license='MIT',
|
||||||
|
license_url='https://raw.githubusercontent.com/urllib3/urllib3/refs/heads/main/LICENSE.txt',
|
||||||
|
project_url='https://urllib3.readthedocs.io/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='SecretStorage',
|
||||||
|
license='BSD-3-Clause',
|
||||||
|
license_url='https://raw.githubusercontent.com/mitya57/secretstorage/refs/heads/master/LICENSE',
|
||||||
|
comment='Only included in Linux builds',
|
||||||
|
project_url='https://secretstorage.readthedocs.io/',
|
||||||
|
),
|
||||||
|
# Dependency of SecretStorage
|
||||||
|
Dependency(
|
||||||
|
name='cryptography',
|
||||||
|
license='Apache-2.0', # Also available as BSD-3-Clause
|
||||||
|
license_url='https://raw.githubusercontent.com/pyca/cryptography/refs/heads/main/LICENSE.APACHE',
|
||||||
|
comment='Only included in Linux builds',
|
||||||
|
project_url='https://cryptography.io/',
|
||||||
|
),
|
||||||
|
# Dependency of SecretStorage
|
||||||
|
Dependency(
|
||||||
|
name='Jeepney',
|
||||||
|
license='MIT',
|
||||||
|
license_url='https://gitlab.com/takluyver/jeepney/-/raw/master/LICENSE',
|
||||||
|
comment='Only included in Linux builds',
|
||||||
|
project_url='https://jeepney.readthedocs.io/',
|
||||||
|
),
|
||||||
|
Dependency(
|
||||||
|
name='websockets',
|
||||||
|
license='BSD-3-Clause',
|
||||||
|
license_url='https://raw.githubusercontent.com/python-websockets/websockets/refs/heads/main/LICENSE',
|
||||||
|
project_url='https://websockets.readthedocs.io/',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_text(dep: Dependency) -> str:
|
||||||
|
cache_dir = Path(CACHE_LOCATION)
|
||||||
|
cache_dir.mkdir(exist_ok=True)
|
||||||
|
url_hash = hashlib.sha256(dep.license_url.encode('utf-8')).hexdigest()
|
||||||
|
cache_file = cache_dir / f'{url_hash}.txt'
|
||||||
|
|
||||||
|
if cache_file.exists():
|
||||||
|
return cache_file.read_text()
|
||||||
|
|
||||||
|
# UA needed since some domains block requests default UA
|
||||||
|
req = requests.get(dep.license_url, headers={'User-Agent': 'yt-dlp license fetcher'})
|
||||||
|
req.raise_for_status()
|
||||||
|
text = req.text
|
||||||
|
cache_file.write_text(text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def build_output() -> str:
|
||||||
|
lines = [HEADER]
|
||||||
|
for d in DEPENDENCIES:
|
||||||
|
lines.append('\n')
|
||||||
|
lines.append('-' * 80)
|
||||||
|
header = f'{d.name}'
|
||||||
|
if d.license:
|
||||||
|
header += f' | {d.license}'
|
||||||
|
if d.comment:
|
||||||
|
header += f'\nNote: {d.comment}'
|
||||||
|
if d.project_url:
|
||||||
|
header += f'\nURL: {d.project_url}'
|
||||||
|
lines.append(header)
|
||||||
|
lines.append('-' * 80)
|
||||||
|
|
||||||
|
text = fetch_text(d)
|
||||||
|
lines.append(text.strip('\n') + '\n')
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
content = build_output()
|
||||||
|
Path(DEFAULT_OUTPUT).write_text(content)
|
||||||
@@ -8,7 +8,7 @@ def main():
|
|||||||
return # This is unused in yt-dlp
|
return # This is unused in yt-dlp
|
||||||
|
|
||||||
parser = optparse.OptionParser(usage='%prog INFILE OUTFILE')
|
parser = optparse.OptionParser(usage='%prog INFILE OUTFILE')
|
||||||
options, args = parser.parse_args()
|
_, args = parser.parse_args()
|
||||||
if len(args) != 2:
|
if len(args) != 2:
|
||||||
parser.error('Expected an input and an output filename')
|
parser.error('Expected an input and an output filename')
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# Allow direct execution
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -55,7 +54,7 @@ def _test(github_repository, note, repo_vars, repo_secrets, inputs, expected=Non
|
|||||||
assert result == exp, f'unexpected result: {github_repository} {note}'
|
assert result == exp, f'unexpected result: {github_repository} {note}'
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def test_setup_variables():
|
||||||
DEFAULT_VERSION_WITH_REVISION = dt.datetime.now(tz=dt.timezone.utc).strftime('%Y.%m.%d.%H%M%S')
|
DEFAULT_VERSION_WITH_REVISION = dt.datetime.now(tz=dt.timezone.utc).strftime('%Y.%m.%d.%H%M%S')
|
||||||
DEFAULT_VERSION = calculate_version()
|
DEFAULT_VERSION = calculate_version()
|
||||||
BASE_REPO_VARS = {
|
BASE_REPO_VARS = {
|
||||||
@@ -323,9 +322,3 @@ def main():
|
|||||||
'pypi_project': None,
|
'pypi_project': None,
|
||||||
'pypi_suffix': None,
|
'pypi_suffix': None,
|
||||||
}, ignore_revision=True)
|
}, ignore_revision=True)
|
||||||
|
|
||||||
print('all tests passed')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
|
|||||||
+8
-7
@@ -1,5 +1,5 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling"]
|
requires = ["hatchling>=1.27.0"]
|
||||||
build-backend = "hatchling.build"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
@@ -22,7 +22,8 @@ keywords = [
|
|||||||
"sponsorblock",
|
"sponsorblock",
|
||||||
"yt-dlp",
|
"yt-dlp",
|
||||||
]
|
]
|
||||||
license = {file = "LICENSE"}
|
license = "Unlicense"
|
||||||
|
license-files = ["LICENSE"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Topic :: Multimedia :: Video",
|
"Topic :: Multimedia :: Video",
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
@@ -34,10 +35,10 @@ classifiers = [
|
|||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
"Programming Language :: Python :: 3.13",
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Programming Language :: Python :: 3.14",
|
||||||
"Programming Language :: Python :: Implementation",
|
"Programming Language :: Python :: Implementation",
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
"License :: OSI Approved :: The Unlicense (Unlicense)",
|
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
@@ -63,7 +64,7 @@ secretstorage = [
|
|||||||
]
|
]
|
||||||
build = [
|
build = [
|
||||||
"build",
|
"build",
|
||||||
"hatchling",
|
"hatchling>=1.27.0",
|
||||||
"pip",
|
"pip",
|
||||||
"setuptools>=71.0.2,<81", # See https://github.com/pyinstaller/pyinstaller/issues/9149
|
"setuptools>=71.0.2,<81", # See https://github.com/pyinstaller/pyinstaller/issues/9149
|
||||||
"wheel",
|
"wheel",
|
||||||
@@ -75,7 +76,7 @@ dev = [
|
|||||||
]
|
]
|
||||||
static-analysis = [
|
static-analysis = [
|
||||||
"autopep8~=2.0",
|
"autopep8~=2.0",
|
||||||
"ruff~=0.12.0",
|
"ruff~=0.13.0",
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
"pytest~=8.1",
|
"pytest~=8.1",
|
||||||
@@ -107,7 +108,6 @@ include = [
|
|||||||
"/LICENSE", # included as license
|
"/LICENSE", # included as license
|
||||||
"/pyproject.toml", # included by default
|
"/pyproject.toml", # included by default
|
||||||
"/README.md", # included as readme
|
"/README.md", # included as readme
|
||||||
"/setup.cfg",
|
|
||||||
"/supportedsites.md",
|
"/supportedsites.md",
|
||||||
]
|
]
|
||||||
artifacts = [
|
artifacts = [
|
||||||
@@ -173,7 +173,8 @@ python = [
|
|||||||
"3.11",
|
"3.11",
|
||||||
"3.12",
|
"3.12",
|
||||||
"3.13",
|
"3.13",
|
||||||
"pypy3.10",
|
"3.14",
|
||||||
|
"pypy3.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
[flake8]
|
|
||||||
exclude = build,venv,.tox,.git,.pytest_cache
|
|
||||||
ignore = E402,E501,E731,E741,W503
|
|
||||||
max_line_length = 120
|
|
||||||
per_file_ignores =
|
|
||||||
devscripts/lazy_load_template.py: F401
|
|
||||||
|
|
||||||
|
|
||||||
[autoflake]
|
|
||||||
ignore-init-module-imports = true
|
|
||||||
ignore-pass-after-docstring = true
|
|
||||||
remove-all-unused-imports = true
|
|
||||||
remove-duplicate-keys = true
|
|
||||||
remove-unused-variables = true
|
|
||||||
|
|
||||||
|
|
||||||
[tox:tox]
|
|
||||||
skipsdist = true
|
|
||||||
envlist = py{39,310,311,312,313},pypy311
|
|
||||||
skip_missing_interpreters = true
|
|
||||||
|
|
||||||
[testenv] # tox
|
|
||||||
deps =
|
|
||||||
pytest
|
|
||||||
commands = pytest {posargs:"-m not download"}
|
|
||||||
passenv = HOME # For test_compat_expanduser
|
|
||||||
setenv =
|
|
||||||
# PYTHONWARNINGS = error # Catches PIP's warnings too
|
|
||||||
|
|
||||||
|
|
||||||
[isort]
|
|
||||||
py_version = 39
|
|
||||||
multi_line_output = VERTICAL_HANGING_INDENT
|
|
||||||
line_length = 80
|
|
||||||
reverse_relative = true
|
|
||||||
ensure_newline_before_comments = true
|
|
||||||
include_trailing_comma = true
|
|
||||||
known_first_party =
|
|
||||||
test
|
|
||||||
+16
-19
@@ -20,7 +20,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **3sat**
|
- **3sat**
|
||||||
- **4tube**
|
- **4tube**
|
||||||
- **56.com**
|
- **56.com**
|
||||||
- **6play**
|
|
||||||
- **7plus**
|
- **7plus**
|
||||||
- **8tracks**
|
- **8tracks**
|
||||||
- **9c9media**
|
- **9c9media**
|
||||||
@@ -243,6 +242,7 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **Canalsurmas**
|
- **Canalsurmas**
|
||||||
- **CaracolTvPlay**: [*caracoltv-play*](## "netrc machine")
|
- **CaracolTvPlay**: [*caracoltv-play*](## "netrc machine")
|
||||||
- **cbc.ca**
|
- **cbc.ca**
|
||||||
|
- **cbc.ca:listen**
|
||||||
- **cbc.ca:player**
|
- **cbc.ca:player**
|
||||||
- **cbc.ca:player:playlist**
|
- **cbc.ca:player:playlist**
|
||||||
- **CBS**: (**Currently broken**)
|
- **CBS**: (**Currently broken**)
|
||||||
@@ -299,7 +299,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **cpac**
|
- **cpac**
|
||||||
- **cpac:playlist**
|
- **cpac:playlist**
|
||||||
- **Cracked**
|
- **Cracked**
|
||||||
- **Crackle**
|
|
||||||
- **Craftsy**
|
- **Craftsy**
|
||||||
- **CrooksAndLiars**
|
- **CrooksAndLiars**
|
||||||
- **CrowdBunker**
|
- **CrowdBunker**
|
||||||
@@ -314,8 +313,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **curiositystream**: [*curiositystream*](## "netrc machine")
|
- **curiositystream**: [*curiositystream*](## "netrc machine")
|
||||||
- **curiositystream:collections**: [*curiositystream*](## "netrc machine")
|
- **curiositystream:collections**: [*curiositystream*](## "netrc machine")
|
||||||
- **curiositystream:series**: [*curiositystream*](## "netrc machine")
|
- **curiositystream:series**: [*curiositystream*](## "netrc machine")
|
||||||
- **cwtv**
|
|
||||||
- **cwtv:movie**
|
|
||||||
- **Cybrary**: [*cybrary*](## "netrc machine")
|
- **Cybrary**: [*cybrary*](## "netrc machine")
|
||||||
- **CybraryCourse**: [*cybrary*](## "netrc machine")
|
- **CybraryCourse**: [*cybrary*](## "netrc machine")
|
||||||
- **DacastPlaylist**
|
- **DacastPlaylist**
|
||||||
@@ -450,7 +447,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **Filmweb**
|
- **Filmweb**
|
||||||
- **FiveThirtyEight**
|
- **FiveThirtyEight**
|
||||||
- **FiveTV**
|
- **FiveTV**
|
||||||
- **FlexTV**
|
|
||||||
- **Flickr**
|
- **Flickr**
|
||||||
- **Floatplane**
|
- **Floatplane**
|
||||||
- **FloatplaneChannel**
|
- **FloatplaneChannel**
|
||||||
@@ -584,6 +580,11 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **Hypem**
|
- **Hypem**
|
||||||
- **Hytale**
|
- **Hytale**
|
||||||
- **Icareus**
|
- **Icareus**
|
||||||
|
- **IdagioAlbum**
|
||||||
|
- **IdagioPersonalPlaylist**
|
||||||
|
- **IdagioPlaylist**
|
||||||
|
- **IdagioRecording**
|
||||||
|
- **IdagioTrack**
|
||||||
- **IdolPlus**
|
- **IdolPlus**
|
||||||
- **iflix:episode**
|
- **iflix:episode**
|
||||||
- **IflixSeries**
|
- **IflixSeries**
|
||||||
@@ -798,7 +799,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **mirrativ**
|
- **mirrativ**
|
||||||
- **mirrativ:user**
|
- **mirrativ:user**
|
||||||
- **MirrorCoUK**
|
- **MirrorCoUK**
|
||||||
- **MiTele**: mitele.es
|
|
||||||
- **mixch**
|
- **mixch**
|
||||||
- **mixch:archive**
|
- **mixch:archive**
|
||||||
- **mixch:movie**
|
- **mixch:movie**
|
||||||
@@ -1009,6 +1009,7 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **onet.tv:channel**
|
- **onet.tv:channel**
|
||||||
- **OnetMVP**
|
- **OnetMVP**
|
||||||
- **OnionStudios**
|
- **OnionStudios**
|
||||||
|
- **onsen**: [*onsen*](## "netrc machine") インターネットラジオステーション<音泉>
|
||||||
- **Opencast**
|
- **Opencast**
|
||||||
- **OpencastPlaylist**
|
- **OpencastPlaylist**
|
||||||
- **openrec**
|
- **openrec**
|
||||||
@@ -1033,8 +1034,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **Panopto**
|
- **Panopto**
|
||||||
- **PanoptoList**
|
- **PanoptoList**
|
||||||
- **PanoptoPlaylist**
|
- **PanoptoPlaylist**
|
||||||
- **ParamountPlus**
|
|
||||||
- **ParamountPlusSeries**
|
|
||||||
- **ParamountPressExpress**
|
- **ParamountPressExpress**
|
||||||
- **Parler**: Posts on parler.com
|
- **Parler**: Posts on parler.com
|
||||||
- **parliamentlive.tv**: UK parliament videos
|
- **parliamentlive.tv**: UK parliament videos
|
||||||
@@ -1069,8 +1068,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **PinterestCollection**
|
- **PinterestCollection**
|
||||||
- **PiramideTV**
|
- **PiramideTV**
|
||||||
- **PiramideTVChannel**
|
- **PiramideTVChannel**
|
||||||
- **pixiv:sketch**
|
|
||||||
- **pixiv:sketch:user**
|
|
||||||
- **PlanetMarathi**
|
- **PlanetMarathi**
|
||||||
- **Platzi**: [*platzi*](## "netrc machine")
|
- **Platzi**: [*platzi*](## "netrc machine")
|
||||||
- **PlatziCourse**: [*platzi*](## "netrc machine")
|
- **PlatziCourse**: [*platzi*](## "netrc machine")
|
||||||
@@ -1257,7 +1254,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **rutube:person**: Rutube person videos
|
- **rutube:person**: Rutube person videos
|
||||||
- **rutube:playlist**: Rutube playlists
|
- **rutube:playlist**: Rutube playlists
|
||||||
- **rutube:tags**: Rutube tags
|
- **rutube:tags**: Rutube tags
|
||||||
- **RUTV**: RUTV.RU
|
|
||||||
- **Ruutu**: (**Currently broken**)
|
- **Ruutu**: (**Currently broken**)
|
||||||
- **Ruv**
|
- **Ruv**
|
||||||
- **ruv.is:spila**
|
- **ruv.is:spila**
|
||||||
@@ -1332,7 +1328,10 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **Slideshare**
|
- **Slideshare**
|
||||||
- **SlidesLive**
|
- **SlidesLive**
|
||||||
- **Slutload**
|
- **Slutload**
|
||||||
- **Smotrim**
|
- **smotrim**
|
||||||
|
- **smotrim:audio**
|
||||||
|
- **smotrim:live**
|
||||||
|
- **smotrim:playlist**
|
||||||
- **SnapchatSpotlight**
|
- **SnapchatSpotlight**
|
||||||
- **Snotr**
|
- **Snotr**
|
||||||
- **SoftWhiteUnderbelly**: [*softwhiteunderbelly*](## "netrc machine")
|
- **SoftWhiteUnderbelly**: [*softwhiteunderbelly*](## "netrc machine")
|
||||||
@@ -1370,8 +1369,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **Sport5**
|
- **Sport5**
|
||||||
- **SportBox**: (**Currently broken**)
|
- **SportBox**: (**Currently broken**)
|
||||||
- **SportDeutschland**
|
- **SportDeutschland**
|
||||||
- **spotify**: Spotify episodes (**Currently broken**)
|
|
||||||
- **spotify:show**: Spotify shows (**Currently broken**)
|
|
||||||
- **Spreaker**
|
- **Spreaker**
|
||||||
- **SpreakerShow**
|
- **SpreakerShow**
|
||||||
- **SpringboardPlatform**
|
- **SpringboardPlatform**
|
||||||
@@ -1510,15 +1507,17 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **TrueID**
|
- **TrueID**
|
||||||
- **TruNews**
|
- **TruNews**
|
||||||
- **Truth**
|
- **Truth**
|
||||||
|
- **ttinglive**: 띵라이브 (formerly FlexTV)
|
||||||
- **Tube8**: (**Currently broken**)
|
- **Tube8**: (**Currently broken**)
|
||||||
- **TubeTuGraz**: [*tubetugraz*](## "netrc machine") tube.tugraz.at
|
- **TubeTuGraz**: [*tubetugraz*](## "netrc machine") tube.tugraz.at
|
||||||
- **TubeTuGrazSeries**: [*tubetugraz*](## "netrc machine")
|
- **TubeTuGrazSeries**: [*tubetugraz*](## "netrc machine")
|
||||||
- **tubitv**: [*tubitv*](## "netrc machine")
|
- **tubitv**: [*tubitv*](## "netrc machine")
|
||||||
- **tubitv:series**
|
- **tubitv:series**
|
||||||
- **Tumblr**: [*tumblr*](## "netrc machine")
|
- **Tumblr**: [*tumblr*](## "netrc machine")
|
||||||
- **TuneInPodcast**
|
- **tunein:embed**
|
||||||
- **TuneInPodcastEpisode**
|
- **tunein:podcast**
|
||||||
- **TuneInStation**
|
- **tunein:podcast:program**
|
||||||
|
- **tunein:station**
|
||||||
- **tv.dfb.de**
|
- **tv.dfb.de**
|
||||||
- **TV2**
|
- **TV2**
|
||||||
- **TV2Article**
|
- **TV2Article**
|
||||||
@@ -1600,7 +1599,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **Varzesh3**: (**Currently broken**)
|
- **Varzesh3**: (**Currently broken**)
|
||||||
- **Vbox7**
|
- **Vbox7**
|
||||||
- **Veo**
|
- **Veo**
|
||||||
- **Vesti**: Вести.Ru (**Currently broken**)
|
|
||||||
- **Vevo**
|
- **Vevo**
|
||||||
- **VevoPlaylist**
|
- **VevoPlaylist**
|
||||||
- **VGTV**: VGTV, BTTV, FTV, Aftenposten and Aftonbladet
|
- **VGTV**: VGTV, BTTV, FTV, Aftenposten and Aftonbladet
|
||||||
@@ -1748,7 +1746,6 @@ The only reliable way to check if a site is supported is to try it.
|
|||||||
- **wykop:dig:comment**
|
- **wykop:dig:comment**
|
||||||
- **wykop:post**
|
- **wykop:post**
|
||||||
- **wykop:post:comment**
|
- **wykop:post:comment**
|
||||||
- **Xanimu**
|
|
||||||
- **XboxClips**
|
- **XboxClips**
|
||||||
- **XHamster**
|
- **XHamster**
|
||||||
- **XHamsterEmbed**
|
- **XHamsterEmbed**
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
"verbose": true,
|
"verbose": true,
|
||||||
"writedescription": false,
|
"writedescription": false,
|
||||||
"writeinfojson": true,
|
"writeinfojson": true,
|
||||||
"writeannotations": false,
|
|
||||||
"writelink": false,
|
"writelink": false,
|
||||||
"writeurllink": false,
|
"writeurllink": false,
|
||||||
"writewebloclink": false,
|
"writewebloclink": false,
|
||||||
|
|||||||
@@ -1945,7 +1945,7 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
|
|||||||
server_thread.daemon = True
|
server_thread.daemon = True
|
||||||
server_thread.start()
|
server_thread.start()
|
||||||
|
|
||||||
(content, urlh) = self.ie._download_webpage_handle(
|
content, _ = self.ie._download_webpage_handle(
|
||||||
f'http://127.0.0.1:{port}/teapot', None,
|
f'http://127.0.0.1:{port}/teapot', None,
|
||||||
expected_status=TEAPOT_RESPONSE_STATUS)
|
expected_status=TEAPOT_RESPONSE_STATUS)
|
||||||
self.assertEqual(content, TEAPOT_RESPONSE_BODY)
|
self.assertEqual(content, TEAPOT_RESPONSE_BODY)
|
||||||
|
|||||||
+41
-1
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# Allow direct execution
|
# Allow direct execution
|
||||||
|
import datetime as dt
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
@@ -12,7 +13,7 @@ import struct
|
|||||||
|
|
||||||
from yt_dlp import compat
|
from yt_dlp import compat
|
||||||
from yt_dlp.compat import urllib # isort: split
|
from yt_dlp.compat import urllib # isort: split
|
||||||
from yt_dlp.compat import compat_etree_fromstring, compat_expanduser
|
from yt_dlp.compat import compat_etree_fromstring, compat_expanduser, compat_datetime_from_timestamp
|
||||||
from yt_dlp.compat.urllib.request import getproxies
|
from yt_dlp.compat.urllib.request import getproxies
|
||||||
|
|
||||||
|
|
||||||
@@ -59,6 +60,45 @@ class TestCompat(unittest.TestCase):
|
|||||||
def test_struct_unpack(self):
|
def test_struct_unpack(self):
|
||||||
self.assertEqual(struct.unpack('!B', b'\x00'), (0,))
|
self.assertEqual(struct.unpack('!B', b'\x00'), (0,))
|
||||||
|
|
||||||
|
def test_compat_datetime_from_timestamp(self):
|
||||||
|
self.assertEqual(
|
||||||
|
compat_datetime_from_timestamp(0),
|
||||||
|
dt.datetime(1970, 1, 1, 0, 0, 0, tzinfo=dt.timezone.utc))
|
||||||
|
self.assertEqual(
|
||||||
|
compat_datetime_from_timestamp(1),
|
||||||
|
dt.datetime(1970, 1, 1, 0, 0, 1, tzinfo=dt.timezone.utc))
|
||||||
|
self.assertEqual(
|
||||||
|
compat_datetime_from_timestamp(3600),
|
||||||
|
dt.datetime(1970, 1, 1, 1, 0, 0, tzinfo=dt.timezone.utc))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
compat_datetime_from_timestamp(-1),
|
||||||
|
dt.datetime(1969, 12, 31, 23, 59, 59, tzinfo=dt.timezone.utc))
|
||||||
|
self.assertEqual(
|
||||||
|
compat_datetime_from_timestamp(-86400),
|
||||||
|
dt.datetime(1969, 12, 31, 0, 0, 0, tzinfo=dt.timezone.utc))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
compat_datetime_from_timestamp(0.5),
|
||||||
|
dt.datetime(1970, 1, 1, 0, 0, 0, 500000, tzinfo=dt.timezone.utc))
|
||||||
|
self.assertEqual(
|
||||||
|
compat_datetime_from_timestamp(1.000001),
|
||||||
|
dt.datetime(1970, 1, 1, 0, 0, 1, 1, tzinfo=dt.timezone.utc))
|
||||||
|
self.assertEqual(
|
||||||
|
compat_datetime_from_timestamp(-1.25),
|
||||||
|
dt.datetime(1969, 12, 31, 23, 59, 58, 750000, tzinfo=dt.timezone.utc))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
compat_datetime_from_timestamp(-1577923200),
|
||||||
|
dt.datetime(1920, 1, 1, 0, 0, 0, tzinfo=dt.timezone.utc))
|
||||||
|
self.assertEqual(
|
||||||
|
compat_datetime_from_timestamp(4102444800),
|
||||||
|
dt.datetime(2100, 1, 1, 0, 0, 0, tzinfo=dt.timezone.utc))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
compat_datetime_from_timestamp(173568960000),
|
||||||
|
dt.datetime(7470, 3, 8, 0, 0, 0, tzinfo=dt.timezone.utc))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class TestOverwrites(unittest.TestCase):
|
|||||||
'-o', 'test.webm',
|
'-o', 'test.webm',
|
||||||
'https://www.youtube.com/watch?v=jNQXAC9IVRw',
|
'https://www.youtube.com/watch?v=jNQXAC9IVRw',
|
||||||
], cwd=root_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
], cwd=root_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
sout, serr = outp.communicate()
|
sout, _ = outp.communicate()
|
||||||
self.assertTrue(b'has already been downloaded' in sout)
|
self.assertTrue(b'has already been downloaded' in sout)
|
||||||
# if the file has no content, it has not been redownloaded
|
# if the file has no content, it has not been redownloaded
|
||||||
self.assertTrue(os.path.getsize(download_file) < 1)
|
self.assertTrue(os.path.getsize(download_file) < 1)
|
||||||
@@ -41,7 +41,7 @@ class TestOverwrites(unittest.TestCase):
|
|||||||
'-o', 'test.webm',
|
'-o', 'test.webm',
|
||||||
'https://www.youtube.com/watch?v=jNQXAC9IVRw',
|
'https://www.youtube.com/watch?v=jNQXAC9IVRw',
|
||||||
], cwd=root_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
], cwd=root_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
sout, serr = outp.communicate()
|
sout, _ = outp.communicate()
|
||||||
self.assertTrue(b'has already been downloaded' not in sout)
|
self.assertTrue(b'has already been downloaded' not in sout)
|
||||||
# if the file has no content, it has not been redownloaded
|
# if the file has no content, it has not been redownloaded
|
||||||
self.assertTrue(os.path.getsize(download_file) > 1)
|
self.assertTrue(os.path.getsize(download_file) > 1)
|
||||||
|
|||||||
@@ -45,3 +45,8 @@ class TestGetWebPoContentBinding:
|
|||||||
def test_invalid_base64(self, pot_request):
|
def test_invalid_base64(self, pot_request):
|
||||||
pot_request.visitor_data = 'invalid-base64'
|
pot_request.visitor_data = 'invalid-base64'
|
||||||
assert get_webpo_content_binding(pot_request, bind_to_visitor_id=True) == (pot_request.visitor_data, ContentBindingType.VISITOR_DATA)
|
assert get_webpo_content_binding(pot_request, bind_to_visitor_id=True) == (pot_request.visitor_data, ContentBindingType.VISITOR_DATA)
|
||||||
|
|
||||||
|
def test_gvs_video_id_binding_experiment(self, pot_request):
|
||||||
|
pot_request.context = PoTokenContext.GVS
|
||||||
|
pot_request._gvs_bind_to_video_id = True
|
||||||
|
assert get_webpo_content_binding(pot_request) == ('example-video-id', ContentBindingType.VIDEO_ID)
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class TestPoTokenProvider:
|
|||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
PoTokenProviderRejectedRequest,
|
PoTokenProviderRejectedRequest,
|
||||||
match='External requests by "example" provider do not support proxy scheme "socks4". Supported proxy '
|
match=r'External requests by "example" provider do not support proxy scheme "socks4"\. Supported proxy '
|
||||||
'schemes: http, socks5h',
|
'schemes: http, socks5h',
|
||||||
):
|
):
|
||||||
provider.request_pot(pot_request)
|
provider.request_pot(pot_request)
|
||||||
|
|||||||
+23
-8
@@ -12,6 +12,7 @@ import datetime as dt
|
|||||||
import io
|
import io
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
|
import ntpath
|
||||||
import pickle
|
import pickle
|
||||||
import subprocess
|
import subprocess
|
||||||
import unittest
|
import unittest
|
||||||
@@ -101,11 +102,13 @@ from yt_dlp.utils import (
|
|||||||
remove_start,
|
remove_start,
|
||||||
render_table,
|
render_table,
|
||||||
replace_extension,
|
replace_extension,
|
||||||
|
datetime_round,
|
||||||
rot47,
|
rot47,
|
||||||
sanitize_filename,
|
sanitize_filename,
|
||||||
sanitize_path,
|
sanitize_path,
|
||||||
sanitize_url,
|
sanitize_url,
|
||||||
shell_quote,
|
shell_quote,
|
||||||
|
strftime_or_none,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
str_to_int,
|
str_to_int,
|
||||||
strip_jsonp,
|
strip_jsonp,
|
||||||
@@ -251,12 +254,6 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(sanitize_path('abc.../def...'), 'abc..#\\def..#')
|
self.assertEqual(sanitize_path('abc.../def...'), 'abc..#\\def..#')
|
||||||
self.assertEqual(sanitize_path('C:\\abc:%(title)s.%(ext)s'), 'C:\\abc#%(title)s.%(ext)s')
|
self.assertEqual(sanitize_path('C:\\abc:%(title)s.%(ext)s'), 'C:\\abc#%(title)s.%(ext)s')
|
||||||
|
|
||||||
# Check with nt._path_normpath if available
|
|
||||||
try:
|
|
||||||
from nt import _path_normpath as nt_path_normpath
|
|
||||||
except ImportError:
|
|
||||||
nt_path_normpath = None
|
|
||||||
|
|
||||||
for test, expected in [
|
for test, expected in [
|
||||||
('C:\\', 'C:\\'),
|
('C:\\', 'C:\\'),
|
||||||
('../abc', '..\\abc'),
|
('../abc', '..\\abc'),
|
||||||
@@ -274,8 +271,7 @@ class TestUtil(unittest.TestCase):
|
|||||||
result = sanitize_path(test)
|
result = sanitize_path(test)
|
||||||
assert result == expected, f'{test} was incorrectly resolved'
|
assert result == expected, f'{test} was incorrectly resolved'
|
||||||
assert result == sanitize_path(result), f'{test} changed after sanitizing again'
|
assert result == sanitize_path(result), f'{test} changed after sanitizing again'
|
||||||
if nt_path_normpath:
|
assert result == ntpath.normpath(test), f'{test} does not match ntpath.normpath'
|
||||||
assert result == nt_path_normpath(test), f'{test} does not match nt._path_normpath'
|
|
||||||
|
|
||||||
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')
|
||||||
@@ -409,6 +405,25 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(datetime_from_str('now+1day', precision='hour'), datetime_from_str('now+24hours', precision='auto'))
|
self.assertEqual(datetime_from_str('now+1day', precision='hour'), datetime_from_str('now+24hours', precision='auto'))
|
||||||
self.assertEqual(datetime_from_str('now+23hours', precision='hour'), datetime_from_str('now+23hours', precision='auto'))
|
self.assertEqual(datetime_from_str('now+23hours', precision='hour'), datetime_from_str('now+23hours', precision='auto'))
|
||||||
|
|
||||||
|
def test_datetime_round(self):
|
||||||
|
self.assertEqual(datetime_round(dt.datetime.strptime('1820-05-12T01:23:45Z', '%Y-%m-%dT%H:%M:%SZ')),
|
||||||
|
dt.datetime(1820, 5, 12, tzinfo=dt.timezone.utc))
|
||||||
|
self.assertEqual(datetime_round(dt.datetime.strptime('1969-12-31T23:34:45Z', '%Y-%m-%dT%H:%M:%SZ'), 'hour'),
|
||||||
|
dt.datetime(1970, 1, 1, 0, tzinfo=dt.timezone.utc))
|
||||||
|
self.assertEqual(datetime_round(dt.datetime.strptime('2024-12-25T01:23:45Z', '%Y-%m-%dT%H:%M:%SZ'), 'minute'),
|
||||||
|
dt.datetime(2024, 12, 25, 1, 24, tzinfo=dt.timezone.utc))
|
||||||
|
self.assertEqual(datetime_round(dt.datetime.strptime('2024-12-25T01:23:45.123Z', '%Y-%m-%dT%H:%M:%S.%fZ'), 'second'),
|
||||||
|
dt.datetime(2024, 12, 25, 1, 23, 45, tzinfo=dt.timezone.utc))
|
||||||
|
self.assertEqual(datetime_round(dt.datetime.strptime('2024-12-25T01:23:45.678Z', '%Y-%m-%dT%H:%M:%S.%fZ'), 'second'),
|
||||||
|
dt.datetime(2024, 12, 25, 1, 23, 46, tzinfo=dt.timezone.utc))
|
||||||
|
|
||||||
|
def test_strftime_or_none(self):
|
||||||
|
self.assertEqual(strftime_or_none(-4722192000), '18200512')
|
||||||
|
self.assertEqual(strftime_or_none(0), '19700101')
|
||||||
|
self.assertEqual(strftime_or_none(1735084800), '20241225')
|
||||||
|
# Throws OverflowError
|
||||||
|
self.assertEqual(strftime_or_none(1735084800000), None)
|
||||||
|
|
||||||
def test_daterange(self):
|
def test_daterange(self):
|
||||||
_20century = DateRange('19000101', '20000101')
|
_20century = DateRange('19000101', '20000101')
|
||||||
self.assertFalse('17890714' in _20century)
|
self.assertFalse('17890714' in _20century)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class TestVerboseOutput(unittest.TestCase):
|
|||||||
'--username', 'johnsmith@gmail.com',
|
'--username', 'johnsmith@gmail.com',
|
||||||
'--password', 'my_secret_password',
|
'--password', 'my_secret_password',
|
||||||
], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
sout, serr = outp.communicate()
|
_, serr = outp.communicate()
|
||||||
self.assertTrue(b'--username' in serr)
|
self.assertTrue(b'--username' in serr)
|
||||||
self.assertTrue(b'johnsmith' not in serr)
|
self.assertTrue(b'johnsmith' not in serr)
|
||||||
self.assertTrue(b'--password' in serr)
|
self.assertTrue(b'--password' in serr)
|
||||||
@@ -36,7 +36,7 @@ class TestVerboseOutput(unittest.TestCase):
|
|||||||
'-u', 'johnsmith@gmail.com',
|
'-u', 'johnsmith@gmail.com',
|
||||||
'-p', 'my_secret_password',
|
'-p', 'my_secret_password',
|
||||||
], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
sout, serr = outp.communicate()
|
_, serr = outp.communicate()
|
||||||
self.assertTrue(b'-u' in serr)
|
self.assertTrue(b'-u' in serr)
|
||||||
self.assertTrue(b'johnsmith' not in serr)
|
self.assertTrue(b'johnsmith' not in serr)
|
||||||
self.assertTrue(b'-p' in serr)
|
self.assertTrue(b'-p' in serr)
|
||||||
@@ -50,7 +50,7 @@ class TestVerboseOutput(unittest.TestCase):
|
|||||||
'--username=johnsmith@gmail.com',
|
'--username=johnsmith@gmail.com',
|
||||||
'--password=my_secret_password',
|
'--password=my_secret_password',
|
||||||
], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
sout, serr = outp.communicate()
|
_, serr = outp.communicate()
|
||||||
self.assertTrue(b'--username' in serr)
|
self.assertTrue(b'--username' in serr)
|
||||||
self.assertTrue(b'johnsmith' not in serr)
|
self.assertTrue(b'johnsmith' not in serr)
|
||||||
self.assertTrue(b'--password' in serr)
|
self.assertTrue(b'--password' in serr)
|
||||||
@@ -64,7 +64,7 @@ class TestVerboseOutput(unittest.TestCase):
|
|||||||
'-u=johnsmith@gmail.com',
|
'-u=johnsmith@gmail.com',
|
||||||
'-p=my_secret_password',
|
'-p=my_secret_password',
|
||||||
], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
], cwd=rootDir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
sout, serr = outp.communicate()
|
_, serr = outp.communicate()
|
||||||
self.assertTrue(b'-u' in serr)
|
self.assertTrue(b'-u' in serr)
|
||||||
self.assertTrue(b'johnsmith' not in serr)
|
self.assertTrue(b'johnsmith' not in serr)
|
||||||
self.assertTrue(b'-p' in serr)
|
self.assertTrue(b'-p' in serr)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import random
|
|||||||
import ssl
|
import ssl
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from yt_dlp import socks, traverse_obj
|
from yt_dlp import socks
|
||||||
from yt_dlp.cookies import YoutubeDLCookieJar
|
from yt_dlp.cookies import YoutubeDLCookieJar
|
||||||
from yt_dlp.dependencies import websockets
|
from yt_dlp.dependencies import websockets
|
||||||
from yt_dlp.networking import Request
|
from yt_dlp.networking import Request
|
||||||
@@ -32,6 +32,7 @@ from yt_dlp.networking.exceptions import (
|
|||||||
SSLError,
|
SSLError,
|
||||||
TransportError,
|
TransportError,
|
||||||
)
|
)
|
||||||
|
from yt_dlp.utils.traversal import traverse_obj
|
||||||
from yt_dlp.utils.networking import HTTPHeaderDict
|
from yt_dlp.utils.networking import HTTPHeaderDict
|
||||||
|
|
||||||
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# Allow direct execution
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
|
|
||||||
import xml.etree.ElementTree
|
|
||||||
|
|
||||||
import yt_dlp.extractor
|
|
||||||
import yt_dlp.YoutubeDL
|
|
||||||
from test.helper import get_params, is_download_test, try_rm
|
|
||||||
|
|
||||||
|
|
||||||
class YoutubeDL(yt_dlp.YoutubeDL):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.to_stderr = self.to_screen
|
|
||||||
|
|
||||||
|
|
||||||
params = get_params({
|
|
||||||
'writeannotations': True,
|
|
||||||
'skip_download': True,
|
|
||||||
'writeinfojson': False,
|
|
||||||
'format': 'flv',
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
TEST_ID = 'gr51aVj-mLg'
|
|
||||||
ANNOTATIONS_FILE = TEST_ID + '.annotations.xml'
|
|
||||||
EXPECTED_ANNOTATIONS = ['Speech bubble', 'Note', 'Title', 'Spotlight', 'Label']
|
|
||||||
|
|
||||||
|
|
||||||
@is_download_test
|
|
||||||
class TestAnnotations(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
# Clear old files
|
|
||||||
self.tearDown()
|
|
||||||
|
|
||||||
def test_info_json(self):
|
|
||||||
expected = list(EXPECTED_ANNOTATIONS) # Two annotations could have the same text.
|
|
||||||
ie = yt_dlp.extractor.YoutubeIE()
|
|
||||||
ydl = YoutubeDL(params)
|
|
||||||
ydl.add_info_extractor(ie)
|
|
||||||
ydl.download([TEST_ID])
|
|
||||||
self.assertTrue(os.path.exists(ANNOTATIONS_FILE))
|
|
||||||
annoxml = None
|
|
||||||
with open(ANNOTATIONS_FILE, encoding='utf-8') as annof:
|
|
||||||
annoxml = xml.etree.ElementTree.parse(annof)
|
|
||||||
self.assertTrue(annoxml is not None, 'Failed to parse annotations XML')
|
|
||||||
root = annoxml.getroot()
|
|
||||||
self.assertEqual(root.tag, 'document')
|
|
||||||
annotationsTag = root.find('annotations')
|
|
||||||
self.assertEqual(annotationsTag.tag, 'annotations')
|
|
||||||
annotations = annotationsTag.findall('annotation')
|
|
||||||
|
|
||||||
# Not all the annotations have TEXT children and the annotations are returned unsorted.
|
|
||||||
for a in annotations:
|
|
||||||
self.assertEqual(a.tag, 'annotation')
|
|
||||||
if a.get('type') == 'text':
|
|
||||||
textTag = a.find('TEXT')
|
|
||||||
text = textTag.text
|
|
||||||
self.assertTrue(text in expected) # assertIn only added in python 2.7
|
|
||||||
# remove the first occurrence, there could be more than one annotation with the same text
|
|
||||||
expected.remove(text)
|
|
||||||
# We should have seen (and removed) all the expected annotation texts.
|
|
||||||
self.assertEqual(len(expected), 0, 'Not all expected annotations were found.')
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
try_rm(ANNOTATIONS_FILE)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
+6
-55
@@ -304,7 +304,6 @@ class YoutubeDL:
|
|||||||
clean_infojson: Remove internal metadata from the infojson
|
clean_infojson: Remove internal metadata from the infojson
|
||||||
getcomments: Extract video comments. This will not be written to disk
|
getcomments: Extract video comments. This will not be written to disk
|
||||||
unless writeinfojson is also given
|
unless writeinfojson is also given
|
||||||
writeannotations: Write the video annotations to a .annotations.xml file
|
|
||||||
writethumbnail: Write the thumbnail image to a file
|
writethumbnail: Write the thumbnail image to a file
|
||||||
allow_playlist_files: Whether to write playlists' description, infojson etc
|
allow_playlist_files: Whether to write playlists' description, infojson etc
|
||||||
also to disk when using the 'write*' options
|
also to disk when using the 'write*' options
|
||||||
@@ -511,11 +510,11 @@ class YoutubeDL:
|
|||||||
the downloader (see yt_dlp/downloader/common.py):
|
the downloader (see yt_dlp/downloader/common.py):
|
||||||
nopart, updatetime, buffersize, ratelimit, throttledratelimit, min_filesize,
|
nopart, updatetime, buffersize, ratelimit, throttledratelimit, min_filesize,
|
||||||
max_filesize, test, noresizebuffer, retries, file_access_retries, fragment_retries,
|
max_filesize, test, noresizebuffer, retries, file_access_retries, fragment_retries,
|
||||||
continuedl, xattr_set_filesize, hls_use_mpegts, http_chunk_size,
|
continuedl, hls_use_mpegts, http_chunk_size, external_downloader_args,
|
||||||
external_downloader_args, concurrent_fragment_downloads, progress_delta.
|
concurrent_fragment_downloads, progress_delta.
|
||||||
|
|
||||||
The following options are used by the post processors:
|
The following options are used by the post processors:
|
||||||
ffmpeg_location: Location of the ffmpeg/avconv binary; either the path
|
ffmpeg_location: Location of the ffmpeg binary; either the path
|
||||||
to the binary or its containing directory.
|
to the binary or its containing directory.
|
||||||
postprocessor_args: A dictionary of postprocessor/executable keys (in lower case)
|
postprocessor_args: A dictionary of postprocessor/executable keys (in lower case)
|
||||||
and a list of additional command-line arguments for the
|
and a list of additional command-line arguments for the
|
||||||
@@ -566,32 +565,14 @@ class YoutubeDL:
|
|||||||
allsubtitles: - Use subtitleslangs = ['all']
|
allsubtitles: - Use subtitleslangs = ['all']
|
||||||
Downloads all the subtitles of the video
|
Downloads all the subtitles of the video
|
||||||
(requires writesubtitles or writeautomaticsub)
|
(requires writesubtitles or writeautomaticsub)
|
||||||
include_ads: - Doesn't work
|
|
||||||
Download ads as well
|
|
||||||
call_home: - Not implemented
|
|
||||||
Boolean, true if we are allowed to contact the
|
|
||||||
yt-dlp servers for debugging.
|
|
||||||
post_hooks: - Register a custom postprocessor
|
post_hooks: - Register a custom postprocessor
|
||||||
A list of functions that get called as the final step
|
A list of functions that get called as the final step
|
||||||
for each video file, after all postprocessors have been
|
for each video file, after all postprocessors have been
|
||||||
called. The filename will be passed as the only argument.
|
called. The filename will be passed as the only argument.
|
||||||
hls_prefer_native: - Use external_downloader = {'m3u8': 'native'} or {'m3u8': 'ffmpeg'}.
|
hls_prefer_native: - Use external_downloader = {'m3u8': 'native'} or {'m3u8': 'ffmpeg'}.
|
||||||
Use the native HLS downloader instead of ffmpeg/avconv
|
Use the native HLS downloader instead of ffmpeg
|
||||||
if True, otherwise use ffmpeg/avconv if False, otherwise
|
if True, otherwise use ffmpeg if False, otherwise
|
||||||
use downloader suggested by extractor if None.
|
use downloader suggested by extractor if None.
|
||||||
prefer_ffmpeg: - avconv support is deprecated
|
|
||||||
If False, use avconv instead of ffmpeg if both are available,
|
|
||||||
otherwise prefer ffmpeg.
|
|
||||||
youtube_include_dash_manifest: - Use extractor_args
|
|
||||||
If True (default), DASH manifests and related
|
|
||||||
data will be downloaded and processed by extractor.
|
|
||||||
You can reduce network I/O by disabling it if you don't
|
|
||||||
care about DASH. (only for youtube)
|
|
||||||
youtube_include_hls_manifest: - Use extractor_args
|
|
||||||
If True (default), HLS manifests and related
|
|
||||||
data will be downloaded and processed by extractor.
|
|
||||||
You can reduce network I/O by disabling it if you don't
|
|
||||||
care about HLS. (only for youtube)
|
|
||||||
no_color: Same as `color='no_color'`
|
no_color: Same as `color='no_color'`
|
||||||
no_overwrites: Same as `overwrites=False`
|
no_overwrites: Same as `overwrites=False`
|
||||||
"""
|
"""
|
||||||
@@ -750,10 +731,6 @@ class YoutubeDL:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if check_deprecated('cn_verification_proxy', '--cn-verification-proxy', '--geo-verification-proxy'):
|
|
||||||
if self.params.get('geo_verification_proxy') is None:
|
|
||||||
self.params['geo_verification_proxy'] = self.params['cn_verification_proxy']
|
|
||||||
|
|
||||||
check_deprecated('useid', '--id', '-o "%(id)s.%(ext)s"')
|
check_deprecated('useid', '--id', '-o "%(id)s.%(ext)s"')
|
||||||
|
|
||||||
for msg in self.params.get('_warnings', []):
|
for msg in self.params.get('_warnings', []):
|
||||||
@@ -2717,11 +2694,7 @@ class YoutubeDL:
|
|||||||
('modified_timestamp', 'modified_date'),
|
('modified_timestamp', 'modified_date'),
|
||||||
):
|
):
|
||||||
if info_dict.get(date_key) is None and info_dict.get(ts_key) is not None:
|
if info_dict.get(date_key) is None and info_dict.get(ts_key) is not None:
|
||||||
# Working around out-of-range timestamp values (e.g. negative ones on Windows,
|
info_dict[date_key] = strftime_or_none(info_dict[ts_key])
|
||||||
# see http://bugs.python.org/issue1646728)
|
|
||||||
with contextlib.suppress(ValueError, OverflowError, OSError):
|
|
||||||
upload_date = dt.datetime.fromtimestamp(info_dict[ts_key], dt.timezone.utc)
|
|
||||||
info_dict[date_key] = upload_date.strftime('%Y%m%d')
|
|
||||||
|
|
||||||
if not info_dict.get('release_year'):
|
if not info_dict.get('release_year'):
|
||||||
info_dict['release_year'] = traverse_obj(info_dict, ('release_date', {lambda x: int(x[:4])}))
|
info_dict['release_year'] = traverse_obj(info_dict, ('release_date', {lambda x: int(x[:4])}))
|
||||||
@@ -3339,28 +3312,6 @@ class YoutubeDL:
|
|||||||
elif _infojson_written is None:
|
elif _infojson_written is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Note: Annotations are deprecated
|
|
||||||
annofn = None
|
|
||||||
if self.params.get('writeannotations', False):
|
|
||||||
annofn = self.prepare_filename(info_dict, 'annotation')
|
|
||||||
if annofn:
|
|
||||||
if not self._ensure_dir_exists(annofn):
|
|
||||||
return
|
|
||||||
if not self.params.get('overwrites', True) and os.path.exists(annofn):
|
|
||||||
self.to_screen('[info] Video annotations are already present')
|
|
||||||
elif not info_dict.get('annotations'):
|
|
||||||
self.report_warning('There are no annotations to write.')
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
self.to_screen('[info] Writing video annotations to: ' + annofn)
|
|
||||||
with open(annofn, 'w', encoding='utf-8') as annofile:
|
|
||||||
annofile.write(info_dict['annotations'])
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
self.report_warning('There are no annotations to write.')
|
|
||||||
except OSError:
|
|
||||||
self.report_error('Cannot write annotations file: ' + annofn)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Write internet shortcut files
|
# Write internet shortcut files
|
||||||
def _write_link_file(link_type):
|
def _write_link_file(link_type):
|
||||||
url = try_get(info_dict['webpage_url'], iri_to_uri)
|
url = try_get(info_dict['webpage_url'], iri_to_uri)
|
||||||
|
|||||||
+6
-52
@@ -59,11 +59,9 @@ from .utils import (
|
|||||||
render_table,
|
render_table,
|
||||||
setproctitle,
|
setproctitle,
|
||||||
shell_quote,
|
shell_quote,
|
||||||
traverse_obj,
|
|
||||||
variadic,
|
variadic,
|
||||||
write_string,
|
write_string,
|
||||||
)
|
)
|
||||||
from .utils.networking import std_headers
|
|
||||||
from .utils._utils import _UnsafeExtensionError
|
from .utils._utils import _UnsafeExtensionError
|
||||||
from .YoutubeDL import YoutubeDL
|
from .YoutubeDL import YoutubeDL
|
||||||
|
|
||||||
@@ -157,7 +155,7 @@ def set_compat_opts(opts):
|
|||||||
if 'format-sort' in opts.compat_opts:
|
if 'format-sort' in opts.compat_opts:
|
||||||
opts.format_sort.extend(FormatSorter.ytdl_default)
|
opts.format_sort.extend(FormatSorter.ytdl_default)
|
||||||
elif 'prefer-vp9-sort' in opts.compat_opts:
|
elif 'prefer-vp9-sort' in opts.compat_opts:
|
||||||
opts.format_sort.extend(FormatSorter._prefer_vp9_sort)
|
FormatSorter.default = FormatSorter._prefer_vp9_sort
|
||||||
|
|
||||||
if 'mtime-by-default' in opts.compat_opts:
|
if 'mtime-by-default' in opts.compat_opts:
|
||||||
if opts.updatetime is None:
|
if opts.updatetime is None:
|
||||||
@@ -523,7 +521,6 @@ def validate_options(opts):
|
|||||||
|
|
||||||
if report_args_compat('post-processor', opts.postprocessor_args, 'default-compat', 'default'):
|
if report_args_compat('post-processor', opts.postprocessor_args, 'default-compat', 'default'):
|
||||||
opts.postprocessor_args['default'] = opts.postprocessor_args.pop('default-compat')
|
opts.postprocessor_args['default'] = opts.postprocessor_args.pop('default-compat')
|
||||||
opts.postprocessor_args.setdefault('sponskrub', [])
|
|
||||||
|
|
||||||
def report_conflict(arg1, opt1, arg2='--allow-unplayable-formats', opt2='allow_unplayable_formats',
|
def report_conflict(arg1, opt1, arg2='--allow-unplayable-formats', opt2='allow_unplayable_formats',
|
||||||
val1=NO_DEFAULT, val2=NO_DEFAULT, default=False):
|
val1=NO_DEFAULT, val2=NO_DEFAULT, default=False):
|
||||||
@@ -548,11 +545,6 @@ def validate_options(opts):
|
|||||||
'"--exec before_dl:"', 'exec_cmd', val2=opts.exec_cmd.get('before_dl'))
|
'"--exec before_dl:"', 'exec_cmd', val2=opts.exec_cmd.get('before_dl'))
|
||||||
report_conflict('--id', 'useid', '--output', 'outtmpl', val2=opts.outtmpl.get('default'))
|
report_conflict('--id', 'useid', '--output', 'outtmpl', val2=opts.outtmpl.get('default'))
|
||||||
report_conflict('--remux-video', 'remuxvideo', '--recode-video', 'recodevideo')
|
report_conflict('--remux-video', 'remuxvideo', '--recode-video', 'recodevideo')
|
||||||
report_conflict('--sponskrub', 'sponskrub', '--remove-chapters', 'remove_chapters')
|
|
||||||
report_conflict('--sponskrub', 'sponskrub', '--sponsorblock-mark', 'sponsorblock_mark')
|
|
||||||
report_conflict('--sponskrub', 'sponskrub', '--sponsorblock-remove', 'sponsorblock_remove')
|
|
||||||
report_conflict('--sponskrub-cut', 'sponskrub_cut', '--split-chapter', 'split_chapters',
|
|
||||||
val1=opts.sponskrub and opts.sponskrub_cut)
|
|
||||||
|
|
||||||
# Conflicts with --allow-unplayable-formats
|
# Conflicts with --allow-unplayable-formats
|
||||||
report_conflict('--embed-metadata', 'addmetadata')
|
report_conflict('--embed-metadata', 'addmetadata')
|
||||||
@@ -565,23 +557,15 @@ def validate_options(opts):
|
|||||||
report_conflict('--recode-video', 'recodevideo')
|
report_conflict('--recode-video', 'recodevideo')
|
||||||
report_conflict('--remove-chapters', 'remove_chapters', default=[])
|
report_conflict('--remove-chapters', 'remove_chapters', default=[])
|
||||||
report_conflict('--remux-video', 'remuxvideo')
|
report_conflict('--remux-video', 'remuxvideo')
|
||||||
report_conflict('--sponskrub', 'sponskrub')
|
|
||||||
report_conflict('--sponsorblock-remove', 'sponsorblock_remove', default=set())
|
report_conflict('--sponsorblock-remove', 'sponsorblock_remove', default=set())
|
||||||
report_conflict('--xattrs', 'xattrs')
|
report_conflict('--xattrs', 'xattrs')
|
||||||
|
|
||||||
# Fully deprecated options
|
if hasattr(opts, '_deprecated_options'):
|
||||||
def report_deprecation(val, old, new=None):
|
|
||||||
if not val:
|
|
||||||
return
|
|
||||||
deprecation_warnings.append(
|
deprecation_warnings.append(
|
||||||
f'{old} is deprecated and may be removed in a future version. Use {new} instead' if new
|
f'The following options have been deprecated: {", ".join(opts._deprecated_options)}\n'
|
||||||
else f'{old} is deprecated and may not work as expected')
|
'Please remove them from your command/configuration to avoid future errors.\n'
|
||||||
|
'See https://github.com/yt-dlp/yt-dlp/issues/14198 for more details')
|
||||||
report_deprecation(opts.sponskrub, '--sponskrub', '--sponsorblock-mark or --sponsorblock-remove')
|
del opts._deprecated_options
|
||||||
report_deprecation(not opts.prefer_ffmpeg, '--prefer-avconv', 'ffmpeg')
|
|
||||||
# report_deprecation(opts.include_ads, '--include-ads') # We may re-implement this in future
|
|
||||||
# report_deprecation(opts.call_home, '--call-home') # We may re-implement this in future
|
|
||||||
# report_deprecation(opts.writeannotations, '--write-annotations') # It's just that no website has it
|
|
||||||
|
|
||||||
# Dependent options
|
# Dependent options
|
||||||
opts.date = DateRange.day(opts.date) if opts.date else DateRange(opts.dateafter, opts.datebefore)
|
opts.date = DateRange.day(opts.date) if opts.date else DateRange(opts.dateafter, opts.datebefore)
|
||||||
@@ -712,21 +696,6 @@ def get_postprocessors(opts):
|
|||||||
'add_metadata': opts.addmetadata,
|
'add_metadata': opts.addmetadata,
|
||||||
'add_infojson': opts.embed_infojson,
|
'add_infojson': opts.embed_infojson,
|
||||||
}
|
}
|
||||||
# Deprecated
|
|
||||||
# This should be above EmbedThumbnail since sponskrub removes the thumbnail attachment
|
|
||||||
# but must be below EmbedSubtitle and FFmpegMetadata
|
|
||||||
# See https://github.com/yt-dlp/yt-dlp/issues/204 , https://github.com/faissaloo/SponSkrub/issues/29
|
|
||||||
# If opts.sponskrub is None, sponskrub is used, but it silently fails if the executable can't be found
|
|
||||||
if opts.sponskrub is not False:
|
|
||||||
yield {
|
|
||||||
'key': 'SponSkrub',
|
|
||||||
'path': opts.sponskrub_path,
|
|
||||||
'args': opts.sponskrub_args,
|
|
||||||
'cut': opts.sponskrub_cut,
|
|
||||||
'force': opts.sponskrub_force,
|
|
||||||
'ignoreerror': opts.sponskrub is None,
|
|
||||||
'_from_cli': True,
|
|
||||||
}
|
|
||||||
if opts.embedthumbnail:
|
if opts.embedthumbnail:
|
||||||
yield {
|
yield {
|
||||||
'key': 'EmbedThumbnail',
|
'key': 'EmbedThumbnail',
|
||||||
@@ -885,7 +854,6 @@ def parse_options(argv=None):
|
|||||||
'nopart': opts.nopart,
|
'nopart': opts.nopart,
|
||||||
'updatetime': opts.updatetime,
|
'updatetime': opts.updatetime,
|
||||||
'writedescription': opts.writedescription,
|
'writedescription': opts.writedescription,
|
||||||
'writeannotations': opts.writeannotations,
|
|
||||||
'writeinfojson': opts.writeinfojson,
|
'writeinfojson': opts.writeinfojson,
|
||||||
'allow_playlist_files': opts.allow_playlist_files,
|
'allow_playlist_files': opts.allow_playlist_files,
|
||||||
'clean_infojson': opts.clean_infojson,
|
'clean_infojson': opts.clean_infojson,
|
||||||
@@ -919,7 +887,6 @@ def parse_options(argv=None):
|
|||||||
'max_views': opts.max_views,
|
'max_views': opts.max_views,
|
||||||
'daterange': opts.date,
|
'daterange': opts.date,
|
||||||
'cachedir': opts.cachedir,
|
'cachedir': opts.cachedir,
|
||||||
'youtube_print_sig_code': opts.youtube_print_sig_code,
|
|
||||||
'age_limit': opts.age_limit,
|
'age_limit': opts.age_limit,
|
||||||
'download_archive': opts.download_archive,
|
'download_archive': opts.download_archive,
|
||||||
'break_on_existing': opts.break_on_existing,
|
'break_on_existing': opts.break_on_existing,
|
||||||
@@ -937,13 +904,9 @@ def parse_options(argv=None):
|
|||||||
'socket_timeout': opts.socket_timeout,
|
'socket_timeout': opts.socket_timeout,
|
||||||
'bidi_workaround': opts.bidi_workaround,
|
'bidi_workaround': opts.bidi_workaround,
|
||||||
'debug_printtraffic': opts.debug_printtraffic,
|
'debug_printtraffic': opts.debug_printtraffic,
|
||||||
'prefer_ffmpeg': opts.prefer_ffmpeg,
|
|
||||||
'include_ads': opts.include_ads,
|
|
||||||
'default_search': opts.default_search,
|
'default_search': opts.default_search,
|
||||||
'dynamic_mpd': opts.dynamic_mpd,
|
'dynamic_mpd': opts.dynamic_mpd,
|
||||||
'extractor_args': opts.extractor_args,
|
'extractor_args': opts.extractor_args,
|
||||||
'youtube_include_dash_manifest': opts.youtube_include_dash_manifest,
|
|
||||||
'youtube_include_hls_manifest': opts.youtube_include_hls_manifest,
|
|
||||||
'encoding': opts.encoding,
|
'encoding': opts.encoding,
|
||||||
'extract_flat': opts.extract_flat,
|
'extract_flat': opts.extract_flat,
|
||||||
'live_from_start': opts.live_from_start,
|
'live_from_start': opts.live_from_start,
|
||||||
@@ -955,7 +918,6 @@ def parse_options(argv=None):
|
|||||||
'fixup': opts.fixup,
|
'fixup': opts.fixup,
|
||||||
'source_address': opts.source_address,
|
'source_address': opts.source_address,
|
||||||
'impersonate': opts.impersonate,
|
'impersonate': opts.impersonate,
|
||||||
'call_home': opts.call_home,
|
|
||||||
'sleep_interval_requests': opts.sleep_interval_requests,
|
'sleep_interval_requests': opts.sleep_interval_requests,
|
||||||
'sleep_interval': opts.sleep_interval,
|
'sleep_interval': opts.sleep_interval,
|
||||||
'max_sleep_interval': opts.max_sleep_interval,
|
'max_sleep_interval': opts.max_sleep_interval,
|
||||||
@@ -965,7 +927,6 @@ def parse_options(argv=None):
|
|||||||
'force_keyframes_at_cuts': opts.force_keyframes_at_cuts,
|
'force_keyframes_at_cuts': opts.force_keyframes_at_cuts,
|
||||||
'list_thumbnails': opts.list_thumbnails,
|
'list_thumbnails': opts.list_thumbnails,
|
||||||
'playlist_items': opts.playlist_items,
|
'playlist_items': opts.playlist_items,
|
||||||
'xattr_set_filesize': opts.xattr_set_filesize,
|
|
||||||
'match_filter': opts.match_filter,
|
'match_filter': opts.match_filter,
|
||||||
'color': opts.color,
|
'color': opts.color,
|
||||||
'ffmpeg_location': opts.ffmpeg_location,
|
'ffmpeg_location': opts.ffmpeg_location,
|
||||||
@@ -974,7 +935,6 @@ def parse_options(argv=None):
|
|||||||
'hls_split_discontinuity': opts.hls_split_discontinuity,
|
'hls_split_discontinuity': opts.hls_split_discontinuity,
|
||||||
'external_downloader_args': opts.external_downloader_args,
|
'external_downloader_args': opts.external_downloader_args,
|
||||||
'postprocessor_args': opts.postprocessor_args,
|
'postprocessor_args': opts.postprocessor_args,
|
||||||
'cn_verification_proxy': opts.cn_verification_proxy,
|
|
||||||
'geo_verification_proxy': opts.geo_verification_proxy,
|
'geo_verification_proxy': opts.geo_verification_proxy,
|
||||||
'geo_bypass': opts.geo_bypass,
|
'geo_bypass': opts.geo_bypass,
|
||||||
'geo_bypass_country': opts.geo_bypass_country,
|
'geo_bypass_country': opts.geo_bypass_country,
|
||||||
@@ -992,12 +952,6 @@ def _real_main(argv=None):
|
|||||||
|
|
||||||
parser, opts, all_urls, ydl_opts = parse_options(argv)
|
parser, opts, all_urls, ydl_opts = parse_options(argv)
|
||||||
|
|
||||||
# Dump user agent
|
|
||||||
if opts.dump_user_agent:
|
|
||||||
ua = traverse_obj(opts.headers, 'User-Agent', casesense=False, default=std_headers['User-Agent'])
|
|
||||||
write_string(f'{ua}\n', out=sys.stdout)
|
|
||||||
return
|
|
||||||
|
|
||||||
if print_extractor_information(opts, all_urls):
|
if print_extractor_information(opts, all_urls):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import datetime as dt
|
||||||
import os
|
import os
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
@@ -27,6 +28,13 @@ def compat_ord(c):
|
|||||||
return c if isinstance(c, int) else ord(c)
|
return c if isinstance(c, int) else ord(c)
|
||||||
|
|
||||||
|
|
||||||
|
def compat_datetime_from_timestamp(timestamp):
|
||||||
|
# Calling dt.datetime.fromtimestamp with negative timestamps throws error in Windows
|
||||||
|
# Ref: https://github.com/yt-dlp/yt-dlp/issues/5185, https://github.com/python/cpython/issues/81708,
|
||||||
|
# https://github.com/yt-dlp/yt-dlp/issues/6706#issuecomment-1496842642
|
||||||
|
return (dt.datetime.fromtimestamp(0, dt.timezone.utc) + dt.timedelta(seconds=timestamp))
|
||||||
|
|
||||||
|
|
||||||
# Python 3.8+ does not honor %HOME% on windows, but this breaks compatibility with youtube-dl
|
# Python 3.8+ does not honor %HOME% on windows, but this breaks compatibility with youtube-dl
|
||||||
# See https://github.com/yt-dlp/yt-dlp/issues/792
|
# See https://github.com/yt-dlp/yt-dlp/issues/792
|
||||||
# https://docs.python.org/3/library/os.path.html#os.path.expanduser
|
# https://docs.python.org/3/library/os.path.html#os.path.expanduser
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ class FileDownloader:
|
|||||||
test: Download only first bytes to test the downloader.
|
test: Download only first bytes to test the downloader.
|
||||||
min_filesize: Skip files smaller than this size
|
min_filesize: Skip files smaller than this size
|
||||||
max_filesize: Skip files larger than this size
|
max_filesize: Skip files larger than this size
|
||||||
xattr_set_filesize: Set ytdl.filesize user xattribute with expected size.
|
|
||||||
progress_delta: The minimum time between progress output, in seconds
|
progress_delta: The minimum time between progress output, in seconds
|
||||||
external_downloader_args: A dictionary of downloader keys (in lower case)
|
external_downloader_args: A dictionary of downloader keys (in lower case)
|
||||||
and a list of additional command-line arguments for the
|
and a list of additional command-line arguments for the
|
||||||
|
|||||||
@@ -563,7 +563,7 @@ class FFmpegFD(ExternalFD):
|
|||||||
f'{cookie.name}={cookie.value}; path={cookie.path}; domain={cookie.domain};\r\n'
|
f'{cookie.name}={cookie.value}; path={cookie.path}; domain={cookie.domain};\r\n'
|
||||||
for cookie in cookies)])
|
for cookie in cookies)])
|
||||||
if fmt.get('http_headers') and is_http:
|
if fmt.get('http_headers') and is_http:
|
||||||
# Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv:
|
# Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg:
|
||||||
# [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
|
# [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
|
||||||
args.extend(['-headers', ''.join(f'{key}: {val}\r\n' for key, val in fmt['http_headers'].items())])
|
args.extend(['-headers', ''.join(f'{key}: {val}\r\n' for key, val in fmt['http_headers'].items())])
|
||||||
|
|
||||||
@@ -654,10 +654,6 @@ class FFmpegFD(ExternalFD):
|
|||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
|
||||||
class AVconvFD(FFmpegFD):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
_BY_NAME = {
|
_BY_NAME = {
|
||||||
klass.get_basename(): klass
|
klass.get_basename(): klass
|
||||||
for name, klass in globals().items()
|
for name, klass in globals().items()
|
||||||
|
|||||||
@@ -149,14 +149,14 @@ class FlvReader(io.BytesIO):
|
|||||||
segments_count = self.read_unsigned_char()
|
segments_count = self.read_unsigned_char()
|
||||||
segments = []
|
segments = []
|
||||||
for _ in range(segments_count):
|
for _ in range(segments_count):
|
||||||
box_size, box_type, box_data = self.read_box_info()
|
_box_size, box_type, box_data = self.read_box_info()
|
||||||
assert box_type == b'asrt'
|
assert box_type == b'asrt'
|
||||||
segment = FlvReader(box_data).read_asrt()
|
segment = FlvReader(box_data).read_asrt()
|
||||||
segments.append(segment)
|
segments.append(segment)
|
||||||
fragments_run_count = self.read_unsigned_char()
|
fragments_run_count = self.read_unsigned_char()
|
||||||
fragments = []
|
fragments = []
|
||||||
for _ in range(fragments_run_count):
|
for _ in range(fragments_run_count):
|
||||||
box_size, box_type, box_data = self.read_box_info()
|
_box_size, box_type, box_data = self.read_box_info()
|
||||||
assert box_type == b'afrt'
|
assert box_type == b'afrt'
|
||||||
fragments.append(FlvReader(box_data).read_afrt())
|
fragments.append(FlvReader(box_data).read_afrt())
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ class FlvReader(io.BytesIO):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def read_bootstrap_info(self):
|
def read_bootstrap_info(self):
|
||||||
total_size, box_type, box_data = self.read_box_info()
|
_, box_type, box_data = self.read_box_info()
|
||||||
assert box_type == b'abst'
|
assert box_type == b'abst'
|
||||||
return FlvReader(box_data).read_abst()
|
return FlvReader(box_data).read_abst()
|
||||||
|
|
||||||
@@ -324,9 +324,9 @@ class F4mFD(FragmentFD):
|
|||||||
if requested_bitrate is None or len(formats) == 1:
|
if requested_bitrate is None or len(formats) == 1:
|
||||||
# get the best format
|
# get the best format
|
||||||
formats = sorted(formats, key=lambda f: f[0])
|
formats = sorted(formats, key=lambda f: f[0])
|
||||||
rate, media = formats[-1]
|
_, media = formats[-1]
|
||||||
else:
|
else:
|
||||||
rate, media = next(filter(
|
_, media = next(filter(
|
||||||
lambda f: int(f[0]) == requested_bitrate, formats))
|
lambda f: int(f[0]) == requested_bitrate, formats))
|
||||||
|
|
||||||
# Prefer baseURL for relative URLs as per 11.2 of F4M 3.0 spec.
|
# Prefer baseURL for relative URLs as per 11.2 of F4M 3.0 spec.
|
||||||
|
|||||||
@@ -13,12 +13,9 @@ from ..utils import (
|
|||||||
ContentTooShortError,
|
ContentTooShortError,
|
||||||
RetryManager,
|
RetryManager,
|
||||||
ThrottledDownload,
|
ThrottledDownload,
|
||||||
XAttrMetadataError,
|
|
||||||
XAttrUnavailableError,
|
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_http_range,
|
parse_http_range,
|
||||||
try_call,
|
try_call,
|
||||||
write_xattr,
|
|
||||||
)
|
)
|
||||||
from ..utils.networking import HTTPHeaderDict
|
from ..utils.networking import HTTPHeaderDict
|
||||||
|
|
||||||
@@ -273,12 +270,6 @@ class HttpFD(FileDownloader):
|
|||||||
self.report_error(f'unable to open for writing: {err}')
|
self.report_error(f'unable to open for writing: {err}')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.params.get('xattr_set_filesize', False) and data_len is not None:
|
|
||||||
try:
|
|
||||||
write_xattr(ctx.tmpfilename, 'user.ytdl.filesize', str(data_len).encode())
|
|
||||||
except (XAttrUnavailableError, XAttrMetadataError) as err:
|
|
||||||
self.report_error(f'unable to set filesize xattr: {err}')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ctx.stream.write(data_block)
|
ctx.stream.write(data_block)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
|
|||||||
@@ -337,6 +337,7 @@ from .cbc import (
|
|||||||
CBCGemIE,
|
CBCGemIE,
|
||||||
CBCGemLiveIE,
|
CBCGemLiveIE,
|
||||||
CBCGemPlaylistIE,
|
CBCGemPlaylistIE,
|
||||||
|
CBCListenIE,
|
||||||
CBCPlayerIE,
|
CBCPlayerIE,
|
||||||
CBCPlayerPlaylistIE,
|
CBCPlayerPlaylistIE,
|
||||||
)
|
)
|
||||||
@@ -424,7 +425,6 @@ from .cpac import (
|
|||||||
CPACPlaylistIE,
|
CPACPlaylistIE,
|
||||||
)
|
)
|
||||||
from .cracked import CrackedIE
|
from .cracked import CrackedIE
|
||||||
from .crackle import CrackleIE
|
|
||||||
from .craftsy import CraftsyIE
|
from .craftsy import CraftsyIE
|
||||||
from .crooksandliars import CrooksAndLiarsIE
|
from .crooksandliars import CrooksAndLiarsIE
|
||||||
from .crowdbunker import (
|
from .crowdbunker import (
|
||||||
@@ -444,10 +444,6 @@ from .curiositystream import (
|
|||||||
CuriosityStreamIE,
|
CuriosityStreamIE,
|
||||||
CuriosityStreamSeriesIE,
|
CuriosityStreamSeriesIE,
|
||||||
)
|
)
|
||||||
from .cwtv import (
|
|
||||||
CWTVIE,
|
|
||||||
CWTVMovieIE,
|
|
||||||
)
|
|
||||||
from .cybrary import (
|
from .cybrary import (
|
||||||
CybraryCourseIE,
|
CybraryCourseIE,
|
||||||
CybraryIE,
|
CybraryIE,
|
||||||
@@ -828,6 +824,13 @@ from .ichinanalive import (
|
|||||||
IchinanaLiveIE,
|
IchinanaLiveIE,
|
||||||
IchinanaLiveVODIE,
|
IchinanaLiveVODIE,
|
||||||
)
|
)
|
||||||
|
from .idagio import (
|
||||||
|
IdagioAlbumIE,
|
||||||
|
IdagioPersonalPlaylistIE,
|
||||||
|
IdagioPlaylistIE,
|
||||||
|
IdagioRecordingIE,
|
||||||
|
IdagioTrackIE,
|
||||||
|
)
|
||||||
from .idolplus import IdolPlusIE
|
from .idolplus import IdolPlusIE
|
||||||
from .ign import (
|
from .ign import (
|
||||||
IGNIE,
|
IGNIE,
|
||||||
@@ -1141,7 +1144,6 @@ from .mit import (
|
|||||||
OCWMITIE,
|
OCWMITIE,
|
||||||
TechTVMITIE,
|
TechTVMITIE,
|
||||||
)
|
)
|
||||||
from .mitele import MiTeleIE
|
|
||||||
from .mixch import (
|
from .mixch import (
|
||||||
MixchArchiveIE,
|
MixchArchiveIE,
|
||||||
MixchIE,
|
MixchIE,
|
||||||
@@ -1433,6 +1435,7 @@ from .onet import (
|
|||||||
OnetPlIE,
|
OnetPlIE,
|
||||||
)
|
)
|
||||||
from .onionstudios import OnionStudiosIE
|
from .onionstudios import OnionStudiosIE
|
||||||
|
from .onsen import OnsenIE
|
||||||
from .opencast import (
|
from .opencast import (
|
||||||
OpencastIE,
|
OpencastIE,
|
||||||
OpencastPlaylistIE,
|
OpencastPlaylistIE,
|
||||||
@@ -1466,10 +1469,6 @@ from .panopto import (
|
|||||||
PanoptoListIE,
|
PanoptoListIE,
|
||||||
PanoptoPlaylistIE,
|
PanoptoPlaylistIE,
|
||||||
)
|
)
|
||||||
from .paramountplus import (
|
|
||||||
ParamountPlusIE,
|
|
||||||
ParamountPlusSeriesIE,
|
|
||||||
)
|
|
||||||
from .parler import ParlerIE
|
from .parler import ParlerIE
|
||||||
from .parlview import ParlviewIE
|
from .parlview import ParlviewIE
|
||||||
from .parti import (
|
from .parti import (
|
||||||
@@ -1523,10 +1522,6 @@ from .piramidetv import (
|
|||||||
PiramideTVChannelIE,
|
PiramideTVChannelIE,
|
||||||
PiramideTVIE,
|
PiramideTVIE,
|
||||||
)
|
)
|
||||||
from .pixivsketch import (
|
|
||||||
PixivSketchIE,
|
|
||||||
PixivSketchUserIE,
|
|
||||||
)
|
|
||||||
from .planetmarathi import PlanetMarathiIE
|
from .planetmarathi import PlanetMarathiIE
|
||||||
from .platzi import (
|
from .platzi import (
|
||||||
PlatziCourseIE,
|
PlatziCourseIE,
|
||||||
@@ -1783,7 +1778,6 @@ from .rutube import (
|
|||||||
RutubePlaylistIE,
|
RutubePlaylistIE,
|
||||||
RutubeTagsIE,
|
RutubeTagsIE,
|
||||||
)
|
)
|
||||||
from .rutv import RUTVIE
|
|
||||||
from .ruutu import RuutuIE
|
from .ruutu import RuutuIE
|
||||||
from .ruv import (
|
from .ruv import (
|
||||||
RuvIE,
|
RuvIE,
|
||||||
@@ -1853,7 +1847,6 @@ from .simplecast import (
|
|||||||
SimplecastPodcastIE,
|
SimplecastPodcastIE,
|
||||||
)
|
)
|
||||||
from .sina import SinaIE
|
from .sina import SinaIE
|
||||||
from .sixplay import SixPlayIE
|
|
||||||
from .skeb import SkebIE
|
from .skeb import SkebIE
|
||||||
from .sky import (
|
from .sky import (
|
||||||
SkyNewsIE,
|
SkyNewsIE,
|
||||||
@@ -1881,7 +1874,12 @@ from .skynewsau import SkyNewsAUIE
|
|||||||
from .slideshare import SlideshareIE
|
from .slideshare import SlideshareIE
|
||||||
from .slideslive import SlidesLiveIE
|
from .slideslive import SlidesLiveIE
|
||||||
from .slutload import SlutloadIE
|
from .slutload import SlutloadIE
|
||||||
from .smotrim import SmotrimIE
|
from .smotrim import (
|
||||||
|
SmotrimAudioIE,
|
||||||
|
SmotrimIE,
|
||||||
|
SmotrimLiveIE,
|
||||||
|
SmotrimPlaylistIE,
|
||||||
|
)
|
||||||
from .snapchat import SnapchatSpotlightIE
|
from .snapchat import SnapchatSpotlightIE
|
||||||
from .snotr import SnotrIE
|
from .snotr import SnotrIE
|
||||||
from .softwhiteunderbelly import SoftWhiteUnderbellyIE
|
from .softwhiteunderbelly import SoftWhiteUnderbellyIE
|
||||||
@@ -1929,10 +1927,6 @@ from .spiegel import SpiegelIE
|
|||||||
from .sport5 import Sport5IE
|
from .sport5 import Sport5IE
|
||||||
from .sportbox import SportBoxIE
|
from .sportbox import SportBoxIE
|
||||||
from .sportdeutschland import SportDeutschlandIE
|
from .sportdeutschland import SportDeutschlandIE
|
||||||
from .spotify import (
|
|
||||||
SpotifyIE,
|
|
||||||
SpotifyShowIE,
|
|
||||||
)
|
|
||||||
from .spreaker import (
|
from .spreaker import (
|
||||||
SpreakerIE,
|
SpreakerIE,
|
||||||
SpreakerShowIE,
|
SpreakerShowIE,
|
||||||
@@ -2153,6 +2147,7 @@ from .tubitv import (
|
|||||||
)
|
)
|
||||||
from .tumblr import TumblrIE
|
from .tumblr import TumblrIE
|
||||||
from .tunein import (
|
from .tunein import (
|
||||||
|
TuneInEmbedIE,
|
||||||
TuneInPodcastEpisodeIE,
|
TuneInPodcastEpisodeIE,
|
||||||
TuneInPodcastIE,
|
TuneInPodcastIE,
|
||||||
TuneInShortenerIE,
|
TuneInShortenerIE,
|
||||||
@@ -2287,7 +2282,6 @@ from .utreon import UtreonIE
|
|||||||
from .varzesh3 import Varzesh3IE
|
from .varzesh3 import Varzesh3IE
|
||||||
from .vbox7 import Vbox7IE
|
from .vbox7 import Vbox7IE
|
||||||
from .veo import VeoIE
|
from .veo import VeoIE
|
||||||
from .vesti import VestiIE
|
|
||||||
from .vevo import (
|
from .vevo import (
|
||||||
VevoIE,
|
VevoIE,
|
||||||
VevoPlaylistIE,
|
VevoPlaylistIE,
|
||||||
@@ -2476,7 +2470,6 @@ from .wykop import (
|
|||||||
WykopPostCommentIE,
|
WykopPostCommentIE,
|
||||||
WykopPostIE,
|
WykopPostIE,
|
||||||
)
|
)
|
||||||
from .xanimu import XanimuIE
|
|
||||||
from .xboxclips import XboxClipsIE
|
from .xboxclips import XboxClipsIE
|
||||||
from .xhamster import (
|
from .xhamster import (
|
||||||
XHamsterEmbedIE,
|
XHamsterEmbedIE,
|
||||||
|
|||||||
+18
-6
@@ -21,7 +21,7 @@ from ..utils import (
|
|||||||
|
|
||||||
class ABCIE(InfoExtractor):
|
class ABCIE(InfoExtractor):
|
||||||
IE_NAME = 'abc.net.au'
|
IE_NAME = 'abc.net.au'
|
||||||
_VALID_URL = r'https?://(?:www\.)?abc\.net\.au/(?:news|btn)/(?:[^/]+/){1,4}(?P<id>\d{5,})'
|
_VALID_URL = r'https?://(?:www\.)?abc\.net\.au/(?:news|btn|listen)/(?:[^/?#]+/){1,4}(?P<id>\d{5,})'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.abc.net.au/news/2014-11-05/australia-to-staff-ebola-treatment-centre-in-sierra-leone/5868334',
|
'url': 'http://www.abc.net.au/news/2014-11-05/australia-to-staff-ebola-treatment-centre-in-sierra-leone/5868334',
|
||||||
@@ -53,8 +53,9 @@ class ABCIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '6880080',
|
'id': '6880080',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'NAB lifts interest rates, following Westpac and CBA',
|
'title': 'NAB lifts interest rates, following Westpac and CBA - ABC listen',
|
||||||
'description': 'md5:f13d8edc81e462fce4a0437c7dc04728',
|
'description': 'md5:f13d8edc81e462fce4a0437c7dc04728',
|
||||||
|
'thumbnail': r're:https://live-production\.wcms\.abc-cdn\.net\.au/2193d7437c84b25eafd6360c82b5fa21',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.abc.net.au/news/2015-10-19/6866214',
|
'url': 'http://www.abc.net.au/news/2015-10-19/6866214',
|
||||||
@@ -64,8 +65,9 @@ class ABCIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '10527914',
|
'id': '10527914',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'WWI Centenary',
|
'title': 'WWI Centenary - Behind The News',
|
||||||
'description': 'md5:c2379ec0ca84072e86b446e536954546',
|
'description': 'md5:fa4405939ff750fade46ff0cd4c66a52',
|
||||||
|
'thumbnail': r're:https://live-production\.wcms\.abc-cdn\.net\.au/bcc3433c97bf992dff32ec5a768713c9',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.abc.net.au/news/programs/the-world/2020-06-10/black-lives-matter-protests-spawn-support-for/12342074',
|
'url': 'https://www.abc.net.au/news/programs/the-world/2020-06-10/black-lives-matter-protests-spawn-support-for/12342074',
|
||||||
@@ -73,7 +75,8 @@ class ABCIE(InfoExtractor):
|
|||||||
'id': '12342074',
|
'id': '12342074',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Black Lives Matter protests spawn support for Papuans in Indonesia',
|
'title': 'Black Lives Matter protests spawn support for Papuans in Indonesia',
|
||||||
'description': 'md5:2961a17dc53abc558589ccd0fb8edd6f',
|
'description': 'md5:625257209f2d14ce23cb4e3785da9beb',
|
||||||
|
'thumbnail': r're:https://live-production\.wcms\.abc-cdn\.net\.au/7ee6f190de6d7dbb04203e514bfae9ec',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.abc.net.au/btn/newsbreak/btn-newsbreak-20200814/12560476',
|
'url': 'https://www.abc.net.au/btn/newsbreak/btn-newsbreak-20200814/12560476',
|
||||||
@@ -93,7 +96,16 @@ class ABCIE(InfoExtractor):
|
|||||||
'title': 'Wagner Group retreating from Russia, leader Prigozhin to move to Belarus',
|
'title': 'Wagner Group retreating from Russia, leader Prigozhin to move to Belarus',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'description': 'Wagner troops leave Rostov-on-Don and\xa0Yevgeny Prigozhin will move to Belarus under a deal brokered by Belarusian President Alexander Lukashenko to end the mutiny.',
|
'description': 'Wagner troops leave Rostov-on-Don and\xa0Yevgeny Prigozhin will move to Belarus under a deal brokered by Belarusian President Alexander Lukashenko to end the mutiny.',
|
||||||
'thumbnail': 'https://live-production.wcms.abc-cdn.net.au/0c170f5b57f0105c432f366c0e8e267b?impolicy=wcms_crop_resize&cropH=2813&cropW=5000&xPos=0&yPos=249&width=862&height=485',
|
'thumbnail': r're:https://live-production\.wcm\.abc-cdn\.net\.au/0c170f5b57f0105c432f366c0e8e267b',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.abc.net.au/listen/programs/the-followers-madness-of-two/presents-followers-madness-of-two/105697646',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '105697646',
|
||||||
|
'title': 'INTRODUCING — The Followers: Madness of Two - ABC listen',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'description': 'md5:2310cd0d440a4e01656abea15db8d1f3',
|
||||||
|
'thumbnail': r're:https://live-production\.wcms\.abc-cdn\.net\.au/90d7078214e5d66553ffb7fcf0da0cda',
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
clean_html,
|
||||||
clean_podcast_url,
|
clean_podcast_url,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
@@ -17,7 +18,7 @@ class ApplePodcastsIE(InfoExtractor):
|
|||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'Ferreck Dawn - To The Break of Dawn 117',
|
'title': 'Ferreck Dawn - To The Break of Dawn 117',
|
||||||
'episode': 'Ferreck Dawn - To The Break of Dawn 117',
|
'episode': 'Ferreck Dawn - To The Break of Dawn 117',
|
||||||
'description': 'md5:1fc571102f79dbd0a77bfd71ffda23bc',
|
'description': 'md5:8c4f5c2c30af17ed6a98b0b9daf15b76',
|
||||||
'upload_date': '20240812',
|
'upload_date': '20240812',
|
||||||
'timestamp': 1723449600,
|
'timestamp': 1723449600,
|
||||||
'duration': 3596,
|
'duration': 3596,
|
||||||
@@ -58,7 +59,7 @@ class ApplePodcastsIE(InfoExtractor):
|
|||||||
r'<script [^>]*\bid=["\']serialized-server-data["\'][^>]*>', webpage,
|
r'<script [^>]*\bid=["\']serialized-server-data["\'][^>]*>', webpage,
|
||||||
'server data', episode_id, contains_pattern=r'\[{(?s:.+)}\]')[0]['data']
|
'server data', episode_id, contains_pattern=r'\[{(?s:.+)}\]')[0]['data']
|
||||||
model_data = traverse_obj(server_data, (
|
model_data = traverse_obj(server_data, (
|
||||||
'headerButtonItems', lambda _, v: v['$kind'] == 'bookmark' and v['modelType'] == 'EpisodeOffer',
|
'headerButtonItems', lambda _, v: v['$kind'] == 'share' and v['modelType'] == 'EpisodeLockup',
|
||||||
'model', {dict}, any))
|
'model', {dict}, any))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -68,7 +69,8 @@ class ApplePodcastsIE(InfoExtractor):
|
|||||||
or self._yield_json_ld(webpage, episode_id, fatal=False), episode_id, fatal=False),
|
or self._yield_json_ld(webpage, episode_id, fatal=False), episode_id, fatal=False),
|
||||||
**traverse_obj(model_data, {
|
**traverse_obj(model_data, {
|
||||||
'title': ('title', {str}),
|
'title': ('title', {str}),
|
||||||
'url': ('streamUrl', {clean_podcast_url}),
|
'description': ('summary', {clean_html}),
|
||||||
|
'url': ('playAction', 'episodeOffer', 'streamUrl', {clean_podcast_url}),
|
||||||
'timestamp': ('releaseDate', {parse_iso8601}),
|
'timestamp': ('releaseDate', {parse_iso8601}),
|
||||||
'duration': ('duration', {int_or_none}),
|
'duration': ('duration', {int_or_none}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1366,7 +1366,7 @@ class BilibiliSpaceVideoIE(BilibiliSpaceBaseIE):
|
|||||||
else:
|
else:
|
||||||
yield self.url_result(f'https://www.bilibili.com/video/{entry["bvid"]}', BiliBiliIE, entry['bvid'])
|
yield self.url_result(f'https://www.bilibili.com/video/{entry["bvid"]}', BiliBiliIE, entry['bvid'])
|
||||||
|
|
||||||
metadata, paged_list = self._extract_playlist(fetch_page, get_metadata, get_entries)
|
_, paged_list = self._extract_playlist(fetch_page, get_metadata, get_entries)
|
||||||
return self.playlist_result(paged_list, playlist_id)
|
return self.playlist_result(paged_list, playlist_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -1400,7 +1400,7 @@ class BilibiliSpaceAudioIE(BilibiliSpaceBaseIE):
|
|||||||
for entry in page_data.get('data') or []:
|
for entry in page_data.get('data') or []:
|
||||||
yield self.url_result(f'https://www.bilibili.com/audio/au{entry["id"]}', BilibiliAudioIE, entry['id'])
|
yield self.url_result(f'https://www.bilibili.com/audio/au{entry["id"]}', BilibiliAudioIE, entry['id'])
|
||||||
|
|
||||||
metadata, paged_list = self._extract_playlist(fetch_page, get_metadata, get_entries)
|
_, paged_list = self._extract_playlist(fetch_page, get_metadata, get_entries)
|
||||||
return self.playlist_result(paged_list, playlist_id)
|
return self.playlist_result(paged_list, playlist_id)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ class BrainPOPLegacyBaseIE(BrainPOPBaseIE):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
slug, display_id = self._match_valid_url(url).group('slug', 'id')
|
display_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
topic_data = self._search_json(
|
topic_data = self._search_json(
|
||||||
r'var\s+content\s*=\s*', webpage, 'content data',
|
r'var\s+content\s*=\s*', webpage, 'content data',
|
||||||
|
|||||||
+61
-5
@@ -31,7 +31,7 @@ from ..utils.traversal import require, traverse_obj, trim_str
|
|||||||
|
|
||||||
class CBCIE(InfoExtractor):
|
class CBCIE(InfoExtractor):
|
||||||
IE_NAME = 'cbc.ca'
|
IE_NAME = 'cbc.ca'
|
||||||
_VALID_URL = r'https?://(?:www\.)?cbc\.ca/(?!player/)(?:[^/]+/)+(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://(?:www\.)?cbc\.ca/(?!player/|listen/|i/caffeine/syndicate/)(?:[^/?#]+/)+(?P<id>[^/?#]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# with mediaId
|
# with mediaId
|
||||||
'url': 'http://www.cbc.ca/22minutes/videos/clips-season-23/don-cherry-play-offs',
|
'url': 'http://www.cbc.ca/22minutes/videos/clips-season-23/don-cherry-play-offs',
|
||||||
@@ -112,10 +112,6 @@ class CBCIE(InfoExtractor):
|
|||||||
'playlist_mincount': 6,
|
'playlist_mincount': 6,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def suitable(cls, url):
|
|
||||||
return False if CBCPlayerIE.suitable(url) else super().suitable(url)
|
|
||||||
|
|
||||||
def _extract_player_init(self, player_init, display_id):
|
def _extract_player_init(self, player_init, display_id):
|
||||||
player_info = self._parse_json(player_init, display_id, js_to_json)
|
player_info = self._parse_json(player_init, display_id, js_to_json)
|
||||||
media_id = player_info.get('mediaId')
|
media_id = player_info.get('mediaId')
|
||||||
@@ -913,3 +909,63 @@ class CBCGemLiveIE(InfoExtractor):
|
|||||||
'thumbnail': ('images', 'card', 'url'),
|
'thumbnail': ('images', 'card', 'url'),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CBCListenIE(InfoExtractor):
|
||||||
|
IE_NAME = 'cbc.ca:listen'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?cbc\.ca/listen/(?:cbc-podcasts|live-radio)/[\w-]+/[\w-]+/(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.cbc.ca/listen/cbc-podcasts/1353-the-naked-emperor/episode/16142603-introducing-understood-who-broke-the-internet',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '16142603',
|
||||||
|
'title': 'Introducing Understood: Who Broke the Internet?',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'description': 'md5:c605117500084e43f08a950adc6a708c',
|
||||||
|
'duration': 229,
|
||||||
|
'timestamp': 1745812800,
|
||||||
|
'release_timestamp': 1745827200,
|
||||||
|
'release_date': '20250428',
|
||||||
|
'upload_date': '20250428',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.cbc.ca/listen/live-radio/1-64-the-house/clip/16170773-should-canada-suck-stand-donald-trump',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '16170773',
|
||||||
|
'title': 'Should Canada suck up or stand up to Donald Trump?',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'description': 'md5:7385194f1cdda8df27ba3764b35e7976',
|
||||||
|
'duration': 3159,
|
||||||
|
'timestamp': 1758340800,
|
||||||
|
'release_timestamp': 1758254400,
|
||||||
|
'release_date': '20250919',
|
||||||
|
'upload_date': '20250920',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
response = self._download_json(
|
||||||
|
f'https://www.cbc.ca/listen/api/v1/clips/{video_id}', video_id, fatal=False)
|
||||||
|
data = traverse_obj(response, ('data', {dict}))
|
||||||
|
if not data:
|
||||||
|
self.report_warning('API failed to return data. Falling back to webpage parsing')
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
preloaded_state = self._search_json(
|
||||||
|
r'window\.__PRELOADED_STATE__\s*=', webpage, 'preloaded state',
|
||||||
|
video_id, transform_source=js_to_json)
|
||||||
|
data = traverse_obj(preloaded_state, (
|
||||||
|
('podcastDetailData', 'showDetailData'), ..., 'episodes',
|
||||||
|
lambda _, v: str(v['clipID']) == video_id, any, {require('episode data')}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
**traverse_obj(data, {
|
||||||
|
'url': (('src', 'url'), {url_or_none}, any),
|
||||||
|
'title': ('title', {str}),
|
||||||
|
'description': ('description', {str}),
|
||||||
|
'release_timestamp': ('releasedAt', {int_or_none(scale=1000)}),
|
||||||
|
'timestamp': ('airdate', {int_or_none(scale=1000)}),
|
||||||
|
'duration': ('duration', {int_or_none}),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import zlib
|
|||||||
|
|
||||||
from .anvato import AnvatoIE
|
from .anvato import AnvatoIE
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .paramountplus import ParamountPlusIE
|
|
||||||
from ..networking import HEADRequest
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
UserNotLive,
|
UserNotLive,
|
||||||
@@ -132,13 +130,7 @@ class CBSNewsEmbedIE(CBSNewsBaseIE):
|
|||||||
video_id = item['mpxRefId']
|
video_id = item['mpxRefId']
|
||||||
video_url = self._get_video_url(item)
|
video_url = self._get_video_url(item)
|
||||||
if not video_url:
|
if not video_url:
|
||||||
# Old embeds redirect user to ParamountPlus but most links are 404
|
raise ExtractorError('This video is no longer available', expected=True)
|
||||||
pplus_url = f'https://www.paramountplus.com/shows/video/{video_id}'
|
|
||||||
try:
|
|
||||||
self._request_webpage(HEADRequest(pplus_url), video_id)
|
|
||||||
return self.url_result(pplus_url, ParamountPlusIE)
|
|
||||||
except ExtractorError:
|
|
||||||
self.raise_no_formats('This video is no longer available', True, video_id)
|
|
||||||
|
|
||||||
return self._extract_video(item, video_url, video_id)
|
return self._extract_video(item, video_url, video_id)
|
||||||
|
|
||||||
|
|||||||
@@ -5,18 +5,6 @@ from ..utils import ExtractorError, make_archive_id, url_basename
|
|||||||
class CellebriteIE(VidyardBaseIE):
|
class CellebriteIE(VidyardBaseIE):
|
||||||
_VALID_URL = r'https?://cellebrite\.com/(?:\w+)?/(?P<id>[\w-]+)'
|
_VALID_URL = r'https?://cellebrite\.com/(?:\w+)?/(?P<id>[\w-]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://cellebrite.com/en/collect-data-from-android-devices-with-cellebrite-ufed/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'ZqmUss3dQfEMGpauambPuH',
|
|
||||||
'display_id': '16025876',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Ask the Expert: Chat Capture - Collect Data from Android Devices in Cellebrite UFED',
|
|
||||||
'description': 'md5:dee48fe12bbae5c01fe6a053f7676da4',
|
|
||||||
'thumbnail': 'https://cellebrite.com/wp-content/uploads/2021/05/Chat-Capture-1024x559.png',
|
|
||||||
'duration': 455.979,
|
|
||||||
'_old_archive_ids': ['cellebrite 16025876'],
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://cellebrite.com/en/how-to-lawfully-collect-the-maximum-amount-of-data-from-android-devices/',
|
'url': 'https://cellebrite.com/en/how-to-lawfully-collect-the-maximum-amount-of-data-from-android-devices/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'QV1U8a2yzcxigw7VFnqKyg',
|
'id': 'QV1U8a2yzcxigw7VFnqKyg',
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ class CNNIndonesiaIE(InfoExtractor):
|
|||||||
return merge_dicts(json_ld_data, {
|
return merge_dicts(json_ld_data, {
|
||||||
'_type': 'url_transparent',
|
'_type': 'url_transparent',
|
||||||
'url': embed_url,
|
'url': embed_url,
|
||||||
|
'id': video_id,
|
||||||
'upload_date': upload_date,
|
'upload_date': upload_date,
|
||||||
'tags': try_call(lambda: self._html_search_meta('keywords', webpage).split(', ')),
|
'tags': try_call(lambda: self._html_search_meta('keywords', webpage).split(', ')),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,243 +0,0 @@
|
|||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..networking.exceptions import HTTPError
|
|
||||||
from ..utils import (
|
|
||||||
ExtractorError,
|
|
||||||
determine_ext,
|
|
||||||
float_or_none,
|
|
||||||
int_or_none,
|
|
||||||
orderedSet,
|
|
||||||
parse_age_limit,
|
|
||||||
parse_duration,
|
|
||||||
url_or_none,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CrackleIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'(?:crackle:|https?://(?:(?:www|m)\.)?(?:sony)?crackle\.com/(?:playlist/\d+/|(?:[^/]+/)+))(?P<id>\d+)'
|
|
||||||
_TESTS = [{
|
|
||||||
# Crackle is available in the United States and territories
|
|
||||||
'url': 'https://www.crackle.com/thanksgiving/2510064',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '2510064',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Touch Football',
|
|
||||||
'description': 'md5:cfbb513cf5de41e8b56d7ab756cff4df',
|
|
||||||
'duration': 1398,
|
|
||||||
'view_count': int,
|
|
||||||
'average_rating': 0,
|
|
||||||
'age_limit': 17,
|
|
||||||
'genre': 'Comedy',
|
|
||||||
'creator': 'Daniel Powell',
|
|
||||||
'artist': 'Chris Elliott, Amy Sedaris',
|
|
||||||
'release_year': 2016,
|
|
||||||
'series': 'Thanksgiving',
|
|
||||||
'episode': 'Touch Football',
|
|
||||||
'season_number': 1,
|
|
||||||
'episode_number': 1,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'expected_warnings': [
|
|
||||||
'Trying with a list of known countries',
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.sonycrackle.com/thanksgiving/2510064',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
_MEDIA_FILE_SLOTS = {
|
|
||||||
'360p.mp4': {
|
|
||||||
'width': 640,
|
|
||||||
'height': 360,
|
|
||||||
},
|
|
||||||
'480p.mp4': {
|
|
||||||
'width': 768,
|
|
||||||
'height': 432,
|
|
||||||
},
|
|
||||||
'480p_1mbps.mp4': {
|
|
||||||
'width': 852,
|
|
||||||
'height': 480,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def _download_json(self, url, *args, **kwargs):
|
|
||||||
# Authorization generation algorithm is reverse engineered from:
|
|
||||||
# https://www.sonycrackle.com/static/js/main.ea93451f.chunk.js
|
|
||||||
timestamp = time.strftime('%Y%m%d%H%M', time.gmtime())
|
|
||||||
h = hmac.new(b'IGSLUQCBDFHEOIFM', '|'.join([url, timestamp]).encode(), hashlib.sha1).hexdigest().upper()
|
|
||||||
headers = {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Authorization': '|'.join([h, timestamp, '117', '1']),
|
|
||||||
}
|
|
||||||
return InfoExtractor._download_json(self, url, *args, headers=headers, **kwargs)
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id = self._match_id(url)
|
|
||||||
|
|
||||||
geo_bypass_country = self.get_param('geo_bypass_country', None)
|
|
||||||
countries = orderedSet((geo_bypass_country, 'US', 'AU', 'CA', 'AS', 'FM', 'GU', 'MP', 'PR', 'PW', 'MH', 'VI', ''))
|
|
||||||
num_countries, num = len(countries) - 1, 0
|
|
||||||
|
|
||||||
media = {}
|
|
||||||
for num, country in enumerate(countries):
|
|
||||||
if num == 1: # start hard-coded list
|
|
||||||
self.report_warning('%s. Trying with a list of known countries' % (
|
|
||||||
f'Unable to obtain video formats from {geo_bypass_country} API' if geo_bypass_country
|
|
||||||
else 'No country code was given using --geo-bypass-country'))
|
|
||||||
elif num == num_countries: # end of list
|
|
||||||
geo_info = self._download_json(
|
|
||||||
'https://web-api-us.crackle.com/Service.svc/geo/country',
|
|
||||||
video_id, fatal=False, note='Downloading geo-location information from crackle API',
|
|
||||||
errnote='Unable to fetch geo-location information from crackle') or {}
|
|
||||||
country = geo_info.get('CountryCode')
|
|
||||||
if country is None:
|
|
||||||
continue
|
|
||||||
self.to_screen(f'{self.IE_NAME} identified country as {country}')
|
|
||||||
if country in countries:
|
|
||||||
self.to_screen(f'Downloading from {country} API was already attempted. Skipping...')
|
|
||||||
continue
|
|
||||||
|
|
||||||
if country is None:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
media = self._download_json(
|
|
||||||
f'https://web-api-us.crackle.com/Service.svc/details/media/{video_id}/{country}?disableProtocols=true',
|
|
||||||
video_id, note=f'Downloading media JSON from {country} API',
|
|
||||||
errnote='Unable to download media JSON')
|
|
||||||
except ExtractorError as e:
|
|
||||||
# 401 means geo restriction, trying next country
|
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status == 401:
|
|
||||||
continue
|
|
||||||
raise
|
|
||||||
|
|
||||||
status = media.get('status')
|
|
||||||
if status.get('messageCode') != '0':
|
|
||||||
raise ExtractorError(
|
|
||||||
'{} said: {} {} - {}'.format(
|
|
||||||
self.IE_NAME, status.get('messageCodeDescription'), status.get('messageCode'), status.get('message')),
|
|
||||||
expected=True)
|
|
||||||
|
|
||||||
# Found video formats
|
|
||||||
if isinstance(media.get('MediaURLs'), list):
|
|
||||||
break
|
|
||||||
|
|
||||||
ignore_no_formats = self.get_param('ignore_no_formats_error')
|
|
||||||
|
|
||||||
if not media or (not media.get('MediaURLs') and not ignore_no_formats):
|
|
||||||
raise ExtractorError(
|
|
||||||
'Unable to access the crackle API. Try passing your country code '
|
|
||||||
'to --geo-bypass-country. If it still does not work and the '
|
|
||||||
'video is available in your country')
|
|
||||||
title = media['Title']
|
|
||||||
|
|
||||||
formats, subtitles = [], {}
|
|
||||||
has_drm = False
|
|
||||||
for e in media.get('MediaURLs') or []:
|
|
||||||
if e.get('UseDRM'):
|
|
||||||
has_drm = True
|
|
||||||
format_url = url_or_none(e.get('DRMPath'))
|
|
||||||
else:
|
|
||||||
format_url = url_or_none(e.get('Path'))
|
|
||||||
if not format_url:
|
|
||||||
continue
|
|
||||||
ext = determine_ext(format_url)
|
|
||||||
if ext == 'm3u8':
|
|
||||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
|
||||||
format_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
|
||||||
m3u8_id='hls', fatal=False)
|
|
||||||
formats.extend(fmts)
|
|
||||||
subtitles = self._merge_subtitles(subtitles, subs)
|
|
||||||
elif ext == 'mpd':
|
|
||||||
fmts, subs = self._extract_mpd_formats_and_subtitles(
|
|
||||||
format_url, video_id, mpd_id='dash', fatal=False)
|
|
||||||
formats.extend(fmts)
|
|
||||||
subtitles = self._merge_subtitles(subtitles, subs)
|
|
||||||
elif format_url.endswith('.ism/Manifest'):
|
|
||||||
fmts, subs = self._extract_ism_formats_and_subtitles(
|
|
||||||
format_url, video_id, ism_id='mss', fatal=False)
|
|
||||||
formats.extend(fmts)
|
|
||||||
subtitles = self._merge_subtitles(subtitles, subs)
|
|
||||||
else:
|
|
||||||
mfs_path = e.get('Type')
|
|
||||||
mfs_info = self._MEDIA_FILE_SLOTS.get(mfs_path)
|
|
||||||
if not mfs_info:
|
|
||||||
continue
|
|
||||||
formats.append({
|
|
||||||
'url': format_url,
|
|
||||||
'format_id': 'http-' + mfs_path.split('.')[0],
|
|
||||||
'width': mfs_info['width'],
|
|
||||||
'height': mfs_info['height'],
|
|
||||||
})
|
|
||||||
if not formats and has_drm:
|
|
||||||
self.report_drm(video_id)
|
|
||||||
|
|
||||||
description = media.get('Description')
|
|
||||||
duration = int_or_none(media.get(
|
|
||||||
'DurationInSeconds')) or parse_duration(media.get('Duration'))
|
|
||||||
view_count = int_or_none(media.get('CountViews'))
|
|
||||||
average_rating = float_or_none(media.get('UserRating'))
|
|
||||||
age_limit = parse_age_limit(media.get('Rating'))
|
|
||||||
genre = media.get('Genre')
|
|
||||||
release_year = int_or_none(media.get('ReleaseYear'))
|
|
||||||
creator = media.get('Directors')
|
|
||||||
artist = media.get('Cast')
|
|
||||||
|
|
||||||
if media.get('MediaTypeDisplayValue') == 'Full Episode':
|
|
||||||
series = media.get('ShowName')
|
|
||||||
episode = title
|
|
||||||
season_number = int_or_none(media.get('Season'))
|
|
||||||
episode_number = int_or_none(media.get('Episode'))
|
|
||||||
else:
|
|
||||||
series = episode = season_number = episode_number = None
|
|
||||||
|
|
||||||
cc_files = media.get('ClosedCaptionFiles')
|
|
||||||
if isinstance(cc_files, list):
|
|
||||||
for cc_file in cc_files:
|
|
||||||
if not isinstance(cc_file, dict):
|
|
||||||
continue
|
|
||||||
cc_url = url_or_none(cc_file.get('Path'))
|
|
||||||
if not cc_url:
|
|
||||||
continue
|
|
||||||
lang = cc_file.get('Locale') or 'en'
|
|
||||||
subtitles.setdefault(lang, []).append({'url': cc_url})
|
|
||||||
|
|
||||||
thumbnails = []
|
|
||||||
images = media.get('Images')
|
|
||||||
if isinstance(images, list):
|
|
||||||
for image_key, image_url in images.items():
|
|
||||||
mobj = re.search(r'Img_(\d+)[xX](\d+)', image_key)
|
|
||||||
if not mobj:
|
|
||||||
continue
|
|
||||||
thumbnails.append({
|
|
||||||
'url': image_url,
|
|
||||||
'width': int(mobj.group(1)),
|
|
||||||
'height': int(mobj.group(2)),
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'duration': duration,
|
|
||||||
'view_count': view_count,
|
|
||||||
'average_rating': average_rating,
|
|
||||||
'age_limit': age_limit,
|
|
||||||
'genre': genre,
|
|
||||||
'creator': creator,
|
|
||||||
'artist': artist,
|
|
||||||
'release_year': release_year,
|
|
||||||
'series': series,
|
|
||||||
'episode': episode,
|
|
||||||
'season_number': season_number,
|
|
||||||
'episode_number': episode_number,
|
|
||||||
'thumbnails': thumbnails,
|
|
||||||
'subtitles': subtitles,
|
|
||||||
'formats': formats,
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import (
|
|
||||||
ExtractorError,
|
|
||||||
int_or_none,
|
|
||||||
parse_age_limit,
|
|
||||||
parse_iso8601,
|
|
||||||
parse_qs,
|
|
||||||
smuggle_url,
|
|
||||||
str_or_none,
|
|
||||||
update_url_query,
|
|
||||||
)
|
|
||||||
from ..utils.traversal import traverse_obj
|
|
||||||
|
|
||||||
|
|
||||||
class CWTVIE(InfoExtractor):
|
|
||||||
IE_NAME = 'cwtv'
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?cw(?:tv(?:pr)?|seed)\.com/(?:shows/)?(?:[^/]+/)+[^?]*\?.*\b(?:play|watch|guid)=(?P<id>[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.cwtv.com/shows/continuum/a-stitch-in-time/?play=9149a1e1-4cb2-46d7-81b2-47d35bbd332b',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '9149a1e1-4cb2-46d7-81b2-47d35bbd332b',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'A Stitch in Time',
|
|
||||||
'description': r're:(?s)City Protective Services officer Kiera Cameron is transported from 2077.+',
|
|
||||||
'thumbnail': r're:https?://.+\.jpe?g',
|
|
||||||
'duration': 2632,
|
|
||||||
'timestamp': 1736928000,
|
|
||||||
'uploader': 'CWTV',
|
|
||||||
'chapters': 'count:5',
|
|
||||||
'series': 'Continuum',
|
|
||||||
'season_number': 1,
|
|
||||||
'episode_number': 1,
|
|
||||||
'age_limit': 14,
|
|
||||||
'upload_date': '20250115',
|
|
||||||
'season': 'Season 1',
|
|
||||||
'episode': 'Episode 1',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'http://cwtv.com/shows/arrow/legends-of-yesterday/?play=6b15e985-9345-4f60-baf8-56e96be57c63',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '6b15e985-9345-4f60-baf8-56e96be57c63',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Legends of Yesterday',
|
|
||||||
'description': r're:(?s)Oliver and Barry Allen take Kendra Saunders and Carter Hall to a remote.+',
|
|
||||||
'duration': 2665,
|
|
||||||
'series': 'Arrow',
|
|
||||||
'season_number': 4,
|
|
||||||
'season': '4',
|
|
||||||
'episode_number': 8,
|
|
||||||
'upload_date': '20151203',
|
|
||||||
'timestamp': 1449122100,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'skip': 'redirect to http://cwtv.com/shows/arrow/',
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.cwseed.com/shows/whose-line-is-it-anyway/jeff-davis-4/?play=24282b12-ead2-42f2-95ad-26770c2c6088',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '24282b12-ead2-42f2-95ad-26770c2c6088',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Jeff Davis 4',
|
|
||||||
'description': 'Jeff Davis is back to make you laugh.',
|
|
||||||
'duration': 1263,
|
|
||||||
'series': 'Whose Line Is It Anyway?',
|
|
||||||
'season_number': 11,
|
|
||||||
'episode_number': 20,
|
|
||||||
'upload_date': '20151006',
|
|
||||||
'timestamp': 1444107300,
|
|
||||||
'age_limit': 14,
|
|
||||||
'uploader': 'CWTV',
|
|
||||||
'thumbnail': r're:https?://.+\.jpe?g',
|
|
||||||
'chapters': 'count:4',
|
|
||||||
'episode': 'Episode 20',
|
|
||||||
'season': 'Season 11',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'http://cwtv.com/thecw/chroniclesofcisco/?play=8adebe35-f447-465f-ab52-e863506ff6d6',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'http://cwtvpr.com/the-cw/video?watch=9eee3f60-ef4e-440b-b3b2-49428ac9c54e',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'http://cwtv.com/shows/arrow/legends-of-yesterday/?watch=6b15e985-9345-4f60-baf8-56e96be57c63',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.cwtv.com/movies/play/?guid=0a8e8b5b-1356-41d5-9a6a-4eda1a6feb6c',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id = self._match_id(url)
|
|
||||||
data = self._download_json(
|
|
||||||
f'https://images.cwtv.com/feed/app-2/video-meta/apiversion_22/device_android/guid_{video_id}', video_id)
|
|
||||||
if traverse_obj(data, 'result') != 'ok':
|
|
||||||
raise ExtractorError(traverse_obj(data, (('error_msg', 'msg'), {str}, any)), expected=True)
|
|
||||||
video_data = data['video']
|
|
||||||
title = video_data['title']
|
|
||||||
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'))
|
|
||||||
episode = str_or_none(video_data.get('episode'))
|
|
||||||
if episode and season:
|
|
||||||
episode = episode[len(season):]
|
|
||||||
|
|
||||||
return {
|
|
||||||
'_type': 'url_transparent',
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'url': smuggle_url(mpx_url, {'force_smil_url': True}),
|
|
||||||
'description': video_data.get('description_long'),
|
|
||||||
'duration': int_or_none(video_data.get('duration_secs')),
|
|
||||||
'series': video_data.get('series_name'),
|
|
||||||
'season_number': int_or_none(season),
|
|
||||||
'episode_number': int_or_none(episode),
|
|
||||||
'timestamp': parse_iso8601(video_data.get('start_time')),
|
|
||||||
'age_limit': parse_age_limit(video_data.get('rating')),
|
|
||||||
'ie_key': 'ThePlatform',
|
|
||||||
'thumbnail': video_data.get('large_thumbnail'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CWTVMovieIE(InfoExtractor):
|
|
||||||
IE_NAME = 'cwtv:movie'
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?cwtv\.com/shows/(?P<id>[\w-]+)/?\?(?:[^#]+&)?viewContext=Movies'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.cwtv.com/shows/the-crush/?viewContext=Movies+Swimlane',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '0a8e8b5b-1356-41d5-9a6a-4eda1a6feb6c',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'The Crush',
|
|
||||||
'upload_date': '20241112',
|
|
||||||
'description': 'md5:1549acd90dff4a8273acd7284458363e',
|
|
||||||
'chapters': 'count:9',
|
|
||||||
'timestamp': 1731398400,
|
|
||||||
'age_limit': 16,
|
|
||||||
'duration': 5337,
|
|
||||||
'series': 'The Crush',
|
|
||||||
'season': 'Season 1',
|
|
||||||
'uploader': 'CWTV',
|
|
||||||
'season_number': 1,
|
|
||||||
'episode': 'Episode 1',
|
|
||||||
'episode_number': 1,
|
|
||||||
'thumbnail': r're:https?://.+\.jpe?g',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
_UUID_RE = r'[\da-f]{8}-(?:[\da-f]{4}-){3}[\da-f]{12}'
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
display_id = self._match_id(url)
|
|
||||||
webpage = self._download_webpage(url, display_id)
|
|
||||||
app_url = (
|
|
||||||
self._html_search_meta('al:ios:url', webpage, default=None)
|
|
||||||
or self._html_search_meta('al:android:url', webpage, default=None))
|
|
||||||
video_id = (
|
|
||||||
traverse_obj(parse_qs(app_url), ('video_id', 0, {lambda x: re.fullmatch(self._UUID_RE, x)}, 0))
|
|
||||||
or self._search_regex([
|
|
||||||
rf'CWTV\.Site\.curPlayingGUID\s*=\s*["\']({self._UUID_RE})',
|
|
||||||
rf'CWTV\.Site\.viewInAppURL\s*=\s*["\']/shows/[\w-]+/watch-in-app/\?play=({self._UUID_RE})',
|
|
||||||
], webpage, 'video ID'))
|
|
||||||
|
|
||||||
return self.url_result(
|
|
||||||
f'https://www.cwtv.com/shows/{display_id}/{display_id}/?play={video_id}', CWTVIE, video_id)
|
|
||||||
+26
-26
@@ -18,15 +18,15 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class DropoutIE(InfoExtractor):
|
class DropoutIE(InfoExtractor):
|
||||||
_LOGIN_URL = 'https://www.dropout.tv/login'
|
_LOGIN_URL = 'https://watch.dropout.tv/login'
|
||||||
_NETRC_MACHINE = 'dropout'
|
_NETRC_MACHINE = 'dropout'
|
||||||
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?dropout\.tv/(?:[^/]+/)*videos/(?P<id>[^/]+)/?$'
|
_VALID_URL = r'https?://(?:watch\.)?dropout\.tv/(?:[^/?#]+/)*videos/(?P<id>[^/?#]+)/?(?:[?#]|$)'
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
'url': 'https://www.dropout.tv/game-changer/season:2/videos/yes-or-no',
|
'url': 'https://watch.dropout.tv/game-changer/season:2/videos/yes-or-no',
|
||||||
'note': 'Episode in a series',
|
'note': 'Episode in a series',
|
||||||
'md5': '5e000fdfd8d8fa46ff40456f1c2af04a',
|
'md5': '4b76963f904f8bc4ba22dcf0e66ada06',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '738153',
|
'id': '738153',
|
||||||
'display_id': 'yes-or-no',
|
'display_id': 'yes-or-no',
|
||||||
@@ -45,35 +45,35 @@ class DropoutIE(InfoExtractor):
|
|||||||
'uploader_url': 'https://vimeo.com/user80538407',
|
'uploader_url': 'https://vimeo.com/user80538407',
|
||||||
'uploader': 'OTT Videos',
|
'uploader': 'OTT Videos',
|
||||||
},
|
},
|
||||||
'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'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'https://www.dropout.tv/dimension-20-fantasy-high/season:1/videos/episode-1',
|
'url': 'https://watch.dropout.tv/tablepop-presents-megadungeon-live/season:1/videos/enter-through-the-gift-shop',
|
||||||
'note': 'Episode in a series (missing release_date)',
|
'note': 'Episode in a series (missing release_date)',
|
||||||
'md5': '712caf7c191f1c47c8f1879520c2fa5c',
|
'md5': 'b08fb03050585ea25cd7ee092db9134c',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '320562',
|
'id': '624270',
|
||||||
'display_id': 'episode-1',
|
'display_id': 'enter-through-the-gift-shop',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'The Beginning Begins',
|
'title': 'Enter Through the Gift Shop',
|
||||||
'description': 'The cast introduces their PCs, including a neurotic elf, a goblin PI, and a corn-worshipping cleric.',
|
'description': 'A new adventuring party explores a gift shop and runs into a friendly orc -- and some angry goblins.',
|
||||||
'thumbnail': 'https://vhx.imgix.net/chuncensoredstaging/assets/4421ed0d-f630-4c88-9004-5251b2b8adfa.jpg',
|
'thumbnail': 'https://vhx.imgix.net/chuncensoredstaging/assets/a1d876c3-3dee-4cd0-87c6-27a851b1d0ec.jpg',
|
||||||
'series': 'Dimension 20: Fantasy High',
|
'series': 'TablePop Presents: MEGADUNGEON LIVE!',
|
||||||
'season_number': 1,
|
'season_number': 1,
|
||||||
'season': 'Season 1',
|
'season': 'Season 1',
|
||||||
'episode_number': 1,
|
'episode_number': 1,
|
||||||
'episode': 'The Beginning Begins',
|
'episode': 'Enter Through the Gift Shop',
|
||||||
'duration': 6838,
|
'duration': 7101,
|
||||||
'uploader_id': 'user80538407',
|
'uploader_id': 'user80538407',
|
||||||
'uploader_url': 'https://vimeo.com/user80538407',
|
'uploader_url': 'https://vimeo.com/user80538407',
|
||||||
'uploader': 'OTT Videos',
|
'uploader': 'OTT Videos',
|
||||||
},
|
},
|
||||||
'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'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'https://www.dropout.tv/videos/misfits-magic-holiday-special',
|
'url': 'https://watch.dropout.tv/videos/misfits-magic-holiday-special',
|
||||||
'note': 'Episode not in a series',
|
'note': 'Episode not in a series',
|
||||||
'md5': 'c30fa18999c5880d156339f13c953a26',
|
'md5': '1e6428f7756b02c93b573d39ddd789fe',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1915774',
|
'id': '1915774',
|
||||||
'display_id': 'misfits-magic-holiday-special',
|
'display_id': 'misfits-magic-holiday-special',
|
||||||
@@ -87,7 +87,7 @@ class DropoutIE(InfoExtractor):
|
|||||||
'uploader_url': 'https://vimeo.com/user80538407',
|
'uploader_url': 'https://vimeo.com/user80538407',
|
||||||
'uploader': 'OTT Videos',
|
'uploader': 'OTT Videos',
|
||||||
},
|
},
|
||||||
'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'],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ class DropoutIE(InfoExtractor):
|
|||||||
display_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
|
|
||||||
webpage = None
|
webpage = None
|
||||||
if self._get_cookies('https://www.dropout.tv').get('_session'):
|
if self._get_cookies('https://watch.dropout.tv').get('_session'):
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
if not webpage or '<div id="watch-unauthorized"' in webpage:
|
if not webpage or '<div id="watch-unauthorized"' in webpage:
|
||||||
login_err = self._login(display_id)
|
login_err = self._login(display_id)
|
||||||
@@ -148,7 +148,7 @@ class DropoutIE(InfoExtractor):
|
|||||||
return {
|
return {
|
||||||
'_type': 'url_transparent',
|
'_type': 'url_transparent',
|
||||||
'ie_key': VHXEmbedIE.ie_key(),
|
'ie_key': VHXEmbedIE.ie_key(),
|
||||||
'url': VHXEmbedIE._smuggle_referrer(embed_url, 'https://www.dropout.tv'),
|
'url': VHXEmbedIE._smuggle_referrer(embed_url, 'https://watch.dropout.tv'),
|
||||||
'id': self._search_regex(r'embed\.vhx\.tv/videos/(.+?)\?', embed_url, 'id'),
|
'id': self._search_regex(r'embed\.vhx\.tv/videos/(.+?)\?', embed_url, 'id'),
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
@@ -167,10 +167,10 @@ class DropoutIE(InfoExtractor):
|
|||||||
|
|
||||||
class DropoutSeasonIE(InfoExtractor):
|
class DropoutSeasonIE(InfoExtractor):
|
||||||
_PAGE_SIZE = 24
|
_PAGE_SIZE = 24
|
||||||
_VALID_URL = r'https?://(?:www\.)?dropout\.tv/(?P<id>[^\/$&?#]+)(?:/?$|/season:(?P<season>[0-9]+)/?$)'
|
_VALID_URL = r'https?://(?:watch\.)?dropout\.tv/(?P<id>[^\/$&?#]+)(?:/?$|/season:(?P<season>[0-9]+)/?$)'
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
'url': 'https://www.dropout.tv/dimension-20-fantasy-high/season:1',
|
'url': 'https://watch.dropout.tv/dimension-20-fantasy-high/season:1',
|
||||||
'note': 'Multi-season series with the season in the url',
|
'note': 'Multi-season series with the season in the url',
|
||||||
'playlist_count': 24,
|
'playlist_count': 24,
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -179,7 +179,7 @@ class DropoutSeasonIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'https://www.dropout.tv/dimension-20-fantasy-high',
|
'url': 'https://watch.dropout.tv/dimension-20-fantasy-high',
|
||||||
'note': 'Multi-season series with the season not in the url',
|
'note': 'Multi-season series with the season not in the url',
|
||||||
'playlist_count': 24,
|
'playlist_count': 24,
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -188,7 +188,7 @@ class DropoutSeasonIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'https://www.dropout.tv/dimension-20-shriek-week',
|
'url': 'https://watch.dropout.tv/dimension-20-shriek-week',
|
||||||
'note': 'Single-season series',
|
'note': 'Single-season series',
|
||||||
'playlist_count': 4,
|
'playlist_count': 4,
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -197,7 +197,7 @@ class DropoutSeasonIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'https://www.dropout.tv/breaking-news-no-laugh-newsroom/season:3',
|
'url': 'https://watch.dropout.tv/breaking-news-no-laugh-newsroom/season:3',
|
||||||
'note': 'Multi-season series with season in the url that requires pagination',
|
'note': 'Multi-season series with season in the url that requires pagination',
|
||||||
'playlist_count': 25,
|
'playlist_count': 25,
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class FifaIE(InfoExtractor):
|
class FifaIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://www\.fifa\.com/fifaplus/(?P<locale>\w{2})/watch/([^#?]+/)?(?P<id>\w+)'
|
_VALID_URL = r'https?://www\.fifa\.com/fifaplus/\w{2}/watch/([^#?]+/)?(?P<id>\w+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.fifa.com/fifaplus/en/watch/7on10qPcnyLajDDU3ntg6y',
|
'url': 'https://www.fifa.com/fifaplus/en/watch/7on10qPcnyLajDDU3ntg6y',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -51,7 +51,7 @@ class FifaIE(InfoExtractor):
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id, locale = self._match_valid_url(url).group('id', 'locale')
|
video_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
preconnect_link = self._search_regex(
|
preconnect_link = self._search_regex(
|
||||||
|
|||||||
@@ -3,15 +3,19 @@ from ..networking.exceptions import HTTPError
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
UserNotLive,
|
UserNotLive,
|
||||||
|
int_or_none,
|
||||||
|
join_nonempty,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
traverse_obj,
|
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class FlexTVIE(InfoExtractor):
|
class FlexTVIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?flextv\.co\.kr/channels/(?P<id>\d+)/live'
|
IE_NAME = 'ttinglive'
|
||||||
|
IE_DESC = '띵라이브 (formerly FlexTV)'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?(?:ttinglive\.com|flextv\.co\.kr)/channels/(?P<id>\d+)/live'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.flextv.co.kr/channels/231638/live',
|
'url': 'https://www.flextv.co.kr/channels/231638/live',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -36,21 +40,32 @@ class FlexTVIE(InfoExtractor):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
stream_data = self._download_json(
|
stream_data = self._download_json(
|
||||||
f'https://api.flextv.co.kr/api/channels/{channel_id}/stream',
|
f'https://api.ttinglive.com/api/channels/{channel_id}/stream',
|
||||||
channel_id, query={'option': 'all'})
|
channel_id, query={'option': 'all'})
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status == 400:
|
if isinstance(e.cause, HTTPError) and e.cause.status == 400:
|
||||||
raise UserNotLive(video_id=channel_id)
|
raise UserNotLive(video_id=channel_id)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
playlist_url = stream_data['sources'][0]['url']
|
formats = []
|
||||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
for stream in traverse_obj(stream_data, ('sources', ..., {dict})):
|
||||||
playlist_url, channel_id, 'mp4')
|
if stream.get('format') == 'ivs' and url_or_none(stream.get('url')):
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
stream['url'], channel_id, 'mp4', live=True, fatal=False, m3u8_id='ivs'))
|
||||||
|
for format_type in ['hls', 'flv']:
|
||||||
|
for data in traverse_obj(stream, (
|
||||||
|
'urlDetail', format_type, 'resolution', lambda _, v: url_or_none(v['url']))):
|
||||||
|
formats.append({
|
||||||
|
'format_id': join_nonempty(format_type, data.get('suffixName'), delim=''),
|
||||||
|
'url': data['url'],
|
||||||
|
'height': int_or_none(data.get('resolution')),
|
||||||
|
'ext': 'mp4' if format_type == 'hls' else 'flv',
|
||||||
|
'protocol': 'm3u8_native' if format_type == 'hls' else 'http',
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': channel_id,
|
'id': channel_id,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': subtitles,
|
|
||||||
'is_live': True,
|
'is_live': True,
|
||||||
**traverse_obj(stream_data, {
|
**traverse_obj(stream_data, {
|
||||||
'title': ('stream', 'title', {str}),
|
'title': ('stream', 'title', {str}),
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import int_or_none, unified_timestamp, url_or_none
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
|
class IdagioTrackIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/recordings/\d+\?(?:[^#]+&)?trackId=(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://app.idagio.com/recordings/30576934?trackId=30576943',
|
||||||
|
'md5': '15148bd71804b2450a2508931a116b56',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '30576943',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': 'Theme. Andante',
|
||||||
|
'duration': 82,
|
||||||
|
'composers': ['Edward Elgar'],
|
||||||
|
'artists': ['Vasily Petrenko', 'Royal Liverpool Philharmonic Orchestra'],
|
||||||
|
'genres': ['Orchestral', 'Other Orchestral Music'],
|
||||||
|
'track': 'Theme. Andante',
|
||||||
|
'timestamp': 1554474370,
|
||||||
|
'upload_date': '20190405',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://app.idagio.com/recordings/20514467?trackId=20514478&utm_source=pcl',
|
||||||
|
'md5': '3acef2ea0feadf889123b70e5a1e7fa7',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '20514478',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': 'I. Adagio sostenuto',
|
||||||
|
'duration': 316,
|
||||||
|
'composers': ['Ludwig van Beethoven'],
|
||||||
|
'artists': [],
|
||||||
|
'genres': ['Keyboard', 'Sonata (Keyboard)'],
|
||||||
|
'track': 'I. Adagio sostenuto',
|
||||||
|
'timestamp': 1518076337,
|
||||||
|
'upload_date': '20180208',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
track_id = self._match_id(url)
|
||||||
|
track_info = self._download_json(
|
||||||
|
f'https://api.idagio.com/v2.0/metadata/tracks/{track_id}',
|
||||||
|
track_id, fatal=False, expected_status=406)
|
||||||
|
if traverse_obj(track_info, 'error_code') == 'idagio.error.blocked.location':
|
||||||
|
self.raise_geo_restricted()
|
||||||
|
|
||||||
|
content_info = self._download_json(
|
||||||
|
f'https://api.idagio.com/v1.8/content/track/{track_id}', track_id,
|
||||||
|
query={
|
||||||
|
'quality': '0',
|
||||||
|
'format': '2',
|
||||||
|
'client_type': 'web-4',
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'ext': 'mp3',
|
||||||
|
'vcodec': 'none',
|
||||||
|
'id': track_id,
|
||||||
|
'url': traverse_obj(content_info, ('url', {url_or_none})),
|
||||||
|
**traverse_obj(track_info, ('result', {
|
||||||
|
'title': ('piece', 'title', {str}),
|
||||||
|
'timestamp': ('recording', 'created_at', {int_or_none(scale=1000)}),
|
||||||
|
'location': ('recording', 'location', {str}),
|
||||||
|
'duration': ('duration', {int_or_none}),
|
||||||
|
'track': ('piece', 'title', {str}),
|
||||||
|
'artists': ('recording', ('conductor', ('ensembles', ...), ('soloists', ...)), 'name', {str}, filter),
|
||||||
|
'composers': ('piece', 'workpart', 'work', 'composer', 'name', {str}, filter, all, filter),
|
||||||
|
'genres': ('piece', 'workpart', 'work', ('genre', 'subgenre'), 'title', {str}, filter),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class IdagioPlaylistBaseIE(InfoExtractor):
|
||||||
|
"""Subclasses must set _API_URL_TMPL and define _parse_playlist_metadata"""
|
||||||
|
_PLAYLIST_ID_KEY = 'id' # vs. 'display_id'
|
||||||
|
|
||||||
|
def _entries(self, playlist_info):
|
||||||
|
for track_data in traverse_obj(playlist_info, ('tracks', lambda _, v: v['id'] and v['recording']['id'])):
|
||||||
|
track_id = track_data['id']
|
||||||
|
recording_id = track_data['recording']['id']
|
||||||
|
yield self.url_result(
|
||||||
|
f'https://app.idagio.com/recordings/{recording_id}?trackId={track_id}',
|
||||||
|
ie=IdagioTrackIE, video_id=track_id)
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
playlist_id = self._match_id(url)
|
||||||
|
playlist_info = self._download_json(
|
||||||
|
self._API_URL_TMPL.format(playlist_id), playlist_id)['result']
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'playlist',
|
||||||
|
self._PLAYLIST_ID_KEY: playlist_id,
|
||||||
|
'entries': self._entries(playlist_info),
|
||||||
|
**self._parse_playlist_metadata(playlist_info),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class IdagioRecordingIE(IdagioPlaylistBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/recordings/(?P<id>\d+)(?![^#]*[&?]trackId=\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://app.idagio.com/recordings/30576934',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '30576934',
|
||||||
|
'title': 'Variations on an Original Theme op. 36',
|
||||||
|
'composers': ['Edward Elgar'],
|
||||||
|
'artists': ['Vasily Petrenko', 'Royal Liverpool Philharmonic Orchestra'],
|
||||||
|
'genres': ['Orchestral', 'Other Orchestral Music'],
|
||||||
|
'timestamp': 1554474370,
|
||||||
|
'modified_timestamp': 1554474370,
|
||||||
|
'modified_date': '20190405',
|
||||||
|
'upload_date': '20190405',
|
||||||
|
},
|
||||||
|
'playlist_count': 15,
|
||||||
|
}]
|
||||||
|
_API_URL_TMPL = 'https://api.idagio.com/v2.0/metadata/recordings/{}'
|
||||||
|
|
||||||
|
def _parse_playlist_metadata(self, playlist_info):
|
||||||
|
return traverse_obj(playlist_info, {
|
||||||
|
'title': ('work', 'title', {str}),
|
||||||
|
'timestamp': ('created_at', {int_or_none(scale=1000)}),
|
||||||
|
'modified_timestamp': ('created_at', {int_or_none(scale=1000)}),
|
||||||
|
'location': ('location', {str}),
|
||||||
|
'artists': (('conductor', ('ensembles', ...), ('soloists', ...)), 'name', {str}),
|
||||||
|
'composers': ('work', 'composer', 'name', {str}, all),
|
||||||
|
'genres': ('work', ('genre', 'subgenre'), 'title', {str}),
|
||||||
|
'tags': ('tags', ..., {str}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class IdagioAlbumIE(IdagioPlaylistBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/albums/(?P<id>[\w-]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://app.idagio.com/albums/elgar-enigma-variations-in-the-south-serenade-for-strings',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'a9f139b8-f70d-4b8a-a9a4-5fe8d35eaf9c',
|
||||||
|
'display_id': 'elgar-enigma-variations-in-the-south-serenade-for-strings',
|
||||||
|
'title': 'Elgar: Enigma Variations, In the South, Serenade for Strings',
|
||||||
|
'description': '',
|
||||||
|
'thumbnail': 'https://idagio-images.global.ssl.fastly.net/albums/880040420521/main.jpg',
|
||||||
|
'artists': ['Vasily Petrenko', 'Royal Liverpool Philharmonic Orchestra', 'Edward Elgar'],
|
||||||
|
'timestamp': 1553817600,
|
||||||
|
'upload_date': '20190329',
|
||||||
|
'modified_timestamp': 1562566559.0,
|
||||||
|
'modified_date': '20190708',
|
||||||
|
},
|
||||||
|
'playlist_count': 19,
|
||||||
|
}, {
|
||||||
|
'url': 'https://app.idagio.com/albums/brahms-ein-deutsches-requiem-3B403DF6-62D7-4A42-807B-47173F3E0192',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2862ad4e-4a61-45ad-9ce4-7fcf0c2626fe',
|
||||||
|
'display_id': 'brahms-ein-deutsches-requiem-3B403DF6-62D7-4A42-807B-47173F3E0192',
|
||||||
|
'title': 'Brahms: Ein deutsches Requiem',
|
||||||
|
'description': '',
|
||||||
|
'thumbnail': 'https://idagio-images.global.ssl.fastly.net/albums/3149020954522/main.jpg',
|
||||||
|
'tags': ['recent-release'],
|
||||||
|
'artists': ['Sabine Devieilhe', 'Stéphane Degout', 'Raphaël Pichon', 'Pygmalion', 'Johannes Brahms'],
|
||||||
|
'timestamp': 1760054400,
|
||||||
|
'upload_date': '20251010',
|
||||||
|
'modified_timestamp': 1760101611,
|
||||||
|
'modified_date': '20251010',
|
||||||
|
},
|
||||||
|
'playlist_count': 7,
|
||||||
|
}]
|
||||||
|
_API_URL_TMPL = 'https://api.idagio.com/v2.0/metadata/albums/{}'
|
||||||
|
_PLAYLIST_ID_KEY = 'display_id'
|
||||||
|
|
||||||
|
def _parse_playlist_metadata(self, playlist_info):
|
||||||
|
return traverse_obj(playlist_info, {
|
||||||
|
'id': ('id', {str}),
|
||||||
|
'title': ('title', {str}),
|
||||||
|
'timestamp': ('publishDate', {unified_timestamp}),
|
||||||
|
'modified_timestamp': ('lastModified', {unified_timestamp}),
|
||||||
|
'thumbnail': ('imageUrl', {url_or_none}),
|
||||||
|
'description': ('description', {str}),
|
||||||
|
'artists': ('participants', ..., 'name', {str}),
|
||||||
|
'tags': ('tags', ..., {str}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class IdagioPlaylistIE(IdagioPlaylistBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/playlists/(?!personal/)(?P<id>[\w-]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://app.idagio.com/playlists/beethoven-the-most-beautiful-piano-music',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '31652bec-8c5b-460e-a3f0-cf1f69817f53',
|
||||||
|
'display_id': 'beethoven-the-most-beautiful-piano-music',
|
||||||
|
'title': 'Beethoven: the most beautiful piano music',
|
||||||
|
'description': 'md5:d41bb04b8896bb69377f5c2cd9345ad1',
|
||||||
|
'thumbnail': r're:https://.+/playlists/31652bec-8c5b-460e-a3f0-cf1f69817f53/main\.jpg',
|
||||||
|
'creators': ['IDAGIO'],
|
||||||
|
},
|
||||||
|
'playlist_mincount': 16, # one entry is geo-restricted
|
||||||
|
}]
|
||||||
|
_API_URL_TMPL = 'https://api.idagio.com/v2.0/playlists/{}'
|
||||||
|
_PLAYLIST_ID_KEY = 'display_id'
|
||||||
|
|
||||||
|
def _parse_playlist_metadata(self, playlist_info):
|
||||||
|
return traverse_obj(playlist_info, {
|
||||||
|
'id': ('id', {str}),
|
||||||
|
'title': ('title', {str}),
|
||||||
|
'thumbnail': ('imageUrl', {url_or_none}),
|
||||||
|
'description': ('description', {str}),
|
||||||
|
'creators': ('curator', 'name', {str}, all),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class IdagioPersonalPlaylistIE(IdagioPlaylistBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/playlists/personal/(?P<id>[\da-f-]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://app.idagio.com/playlists/personal/99dad72e-7b3a-45a4-b216-867c08046ed8',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '99dad72e-7b3a-45a4-b216-867c08046ed8',
|
||||||
|
'title': 'Test',
|
||||||
|
'creators': ['1a6f16a6-4514-4d0c-b481-3a9877835626'],
|
||||||
|
'thumbnail': r're:https://.+/artists/86371/main\.jpg',
|
||||||
|
'timestamp': 1602859138,
|
||||||
|
'modified_timestamp': 1755616667,
|
||||||
|
'upload_date': '20201016',
|
||||||
|
'modified_date': '20250819',
|
||||||
|
},
|
||||||
|
'playlist_count': 100,
|
||||||
|
}]
|
||||||
|
_API_URL_TMPL = 'https://api.idagio.com/v1.0/personal-playlists/{}'
|
||||||
|
|
||||||
|
def _parse_playlist_metadata(self, playlist_info):
|
||||||
|
return traverse_obj(playlist_info, {
|
||||||
|
'title': ('title', {str}),
|
||||||
|
'thumbnail': ('image_url', {url_or_none}),
|
||||||
|
'creators': ('user_id', {str}, all),
|
||||||
|
'timestamp': ('created_at', {int_or_none(scale=1000)}),
|
||||||
|
'modified_timestamp': ('updated_at', {int_or_none(scale=1000)}),
|
||||||
|
})
|
||||||
@@ -37,7 +37,7 @@ class LocoIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://loco.com/stream/c64916eb-10fb-46a9-9a19-8c4b7ed064e7',
|
'url': 'https://loco.com/stream/c64916eb-10fb-46a9-9a19-8c4b7ed064e7',
|
||||||
'md5': '45ebc8a47ee1c2240178757caf8881b5',
|
'md5': '8b9bda03eba4d066928ae8d71f19befb',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'c64916eb-10fb-46a9-9a19-8c4b7ed064e7',
|
'id': 'c64916eb-10fb-46a9-9a19-8c4b7ed064e7',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -55,9 +55,9 @@ class LocoIE(InfoExtractor):
|
|||||||
'tags': ['Gameplay'],
|
'tags': ['Gameplay'],
|
||||||
'series': 'GTA 5',
|
'series': 'GTA 5',
|
||||||
'timestamp': 1740612872,
|
'timestamp': 1740612872,
|
||||||
'modified_timestamp': 1740613037,
|
'modified_timestamp': 1750948439,
|
||||||
'upload_date': '20250226',
|
'upload_date': '20250226',
|
||||||
'modified_date': '20250226',
|
'modified_date': '20250626',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# Requires video authorization
|
# Requires video authorization
|
||||||
@@ -123,8 +123,8 @@ class LocoIE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_type, video_id = self._match_valid_url(url).group('type', 'id')
|
video_type, video_id = self._match_valid_url(url).group('type', 'id')
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
stream = traverse_obj(self._search_nextjs_data(webpage, video_id), (
|
stream = traverse_obj(self._search_nextjs_v13_data(webpage, video_id), (
|
||||||
'props', 'pageProps', ('liveStreamData', 'stream', 'liveStream'), {dict}, any, {require('stream info')}))
|
..., (None, 'ssrData'), ('liveStreamData', 'stream', 'liveStream'), {dict}, any, {require('stream info')}))
|
||||||
|
|
||||||
if access_token := self._get_access_token(video_id):
|
if access_token := self._get_access_token(video_id):
|
||||||
self._request_webpage(
|
self._request_webpage(
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
from .telecinco import TelecincoBaseIE
|
|
||||||
from ..utils import (
|
|
||||||
int_or_none,
|
|
||||||
parse_iso8601,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MiTeleIE(TelecincoBaseIE):
|
|
||||||
IE_DESC = 'mitele.es'
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?mitele\.es/(?:[^/]+/)+(?P<id>[^/]+)/player'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'http://www.mitele.es/programas-tv/diario-de/57b0dfb9c715da65618b4afa/player',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'FhYW1iNTE6J6H7NkQRIEzfne6t2quqPg',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Diario de La redacción Programa 144',
|
|
||||||
'description': 'md5:07c35a7b11abb05876a6a79185b58d27',
|
|
||||||
'series': 'Diario de',
|
|
||||||
'season': 'Season 14',
|
|
||||||
'season_number': 14,
|
|
||||||
'episode': 'Tor, la web invisible',
|
|
||||||
'episode_number': 3,
|
|
||||||
'thumbnail': r're:(?i)^https?://.*\.jpg$',
|
|
||||||
'duration': 2913,
|
|
||||||
'age_limit': 16,
|
|
||||||
'timestamp': 1471209401,
|
|
||||||
'upload_date': '20160814',
|
|
||||||
},
|
|
||||||
'skip': 'HTTP Error 404 Not Found',
|
|
||||||
}, {
|
|
||||||
# no explicit title
|
|
||||||
'url': 'http://www.mitele.es/programas-tv/cuarto-milenio/57b0de3dc915da14058b4876/player',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'oyNG1iNTE6TAPP-JmCjbwfwJqqMMX3Vq',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Cuarto Milenio Temporada 6 Programa 226',
|
|
||||||
'description': 'md5:5ff132013f0cd968ffbf1f5f3538a65f',
|
|
||||||
'series': 'Cuarto Milenio',
|
|
||||||
'season': 'Season 6',
|
|
||||||
'season_number': 6,
|
|
||||||
'episode': 'Episode 24',
|
|
||||||
'episode_number': 24,
|
|
||||||
'thumbnail': r're:(?i)^https?://.*\.jpg$',
|
|
||||||
'duration': 7313,
|
|
||||||
'age_limit': 12,
|
|
||||||
'timestamp': 1471209021,
|
|
||||||
'upload_date': '20160814',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'skip': 'HTTP Error 404 Not Found',
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.mitele.es/programas-tv/horizonte/temporada-5/programa-171-40_013480051/player/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '7adbe22e-cd41-4787-afa4-36f3da7c2c6f',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Horizonte Temporada 5 Programa 171',
|
|
||||||
'description': 'md5:97f1fb712c5ac27e5693a8b3c5c0c6e3',
|
|
||||||
'episode': 'Las Zonas de Bajas Emisiones, a debate',
|
|
||||||
'episode_number': 171,
|
|
||||||
'season': 'Season 5',
|
|
||||||
'season_number': 5,
|
|
||||||
'series': 'Horizonte',
|
|
||||||
'duration': 7012,
|
|
||||||
'upload_date': '20240927',
|
|
||||||
'timestamp': 1727416450,
|
|
||||||
'thumbnail': 'https://album.mediaset.es/eimg/2024/09/27/horizonte-171_9f02.jpg',
|
|
||||||
'age_limit': 12,
|
|
||||||
},
|
|
||||||
'params': {'geo_bypass_country': 'ES'},
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.mitele.es/series-online/la-que-se-avecina/57aac5c1c915da951a8b45ed/player',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.mitele.es/programas-tv/diario-de/la-redaccion/programa-144-40_1006364575251/player/',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
display_id = self._match_id(url)
|
|
||||||
webpage = self._download_webpage(url, display_id)
|
|
||||||
pre_player = self._search_json(
|
|
||||||
r'window\.\$REACTBASE_STATE\.prePlayer_mtweb\s*=',
|
|
||||||
webpage, 'Pre Player', display_id)['prePlayer']
|
|
||||||
title = pre_player['title']
|
|
||||||
video_info = self._parse_content(pre_player['video'], url)
|
|
||||||
content = pre_player.get('content') or {}
|
|
||||||
info = content.get('info') or {}
|
|
||||||
|
|
||||||
video_info.update({
|
|
||||||
'title': title,
|
|
||||||
'description': info.get('synopsis'),
|
|
||||||
'series': content.get('title'),
|
|
||||||
'season_number': int_or_none(info.get('season_number')),
|
|
||||||
'episode': content.get('subtitle'),
|
|
||||||
'episode_number': int_or_none(info.get('episode_number')),
|
|
||||||
'duration': int_or_none(info.get('duration')),
|
|
||||||
'age_limit': int_or_none(info.get('rating')),
|
|
||||||
'timestamp': parse_iso8601(pre_player.get('publishedTime')),
|
|
||||||
})
|
|
||||||
return video_info
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import hashlib
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
@@ -9,10 +11,10 @@ class MuseScoreIE(InfoExtractor):
|
|||||||
'id': '142975',
|
'id': '142975',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'WA Mozart Marche Turque (Turkish March fingered)',
|
'title': 'WA Mozart Marche Turque (Turkish March fingered)',
|
||||||
'description': 'md5:7ede08230e4eaabd67a4a98bb54d07be',
|
'description': 'md5:0ca4cf6b79d7f5868a1fee74097394ab',
|
||||||
'thumbnail': r're:https?://(?:www\.)?musescore\.com/.*\.png[^$]+',
|
'thumbnail': r're:https?://cdn\.ustatik\.com/musescore/.*\.jpg',
|
||||||
'uploader': 'PapyPiano',
|
'uploader': 'PapyPiano',
|
||||||
'creator': 'Wolfgang Amadeus Mozart',
|
'creators': ['Wolfgang Amadeus Mozart'],
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://musescore.com/user/36164500/scores/6837638',
|
'url': 'https://musescore.com/user/36164500/scores/6837638',
|
||||||
@@ -20,10 +22,10 @@ class MuseScoreIE(InfoExtractor):
|
|||||||
'id': '6837638',
|
'id': '6837638',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'Sweet Child O\' Mine – Guns N\' Roses sweet child',
|
'title': 'Sweet Child O\' Mine – Guns N\' Roses sweet child',
|
||||||
'description': 'md5:4dca71191c14abc312a0a4192492eace',
|
'description': 'md5:2cd49bd6b4e48a75a3c469d4775d5079',
|
||||||
'thumbnail': r're:https?://(?:www\.)?musescore\.com/.*\.png[^$]+',
|
'thumbnail': r're:https?://cdn\.ustatik\.com/musescore/.*\.png',
|
||||||
'uploader': 'roxbelviolin',
|
'uploader': 'roxbelviolin',
|
||||||
'creator': 'Guns N´Roses Arr. Roxbel Violin',
|
'creators': ['Guns N´Roses Arr. Roxbel Violin'],
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://musescore.com/classicman/fur-elise',
|
'url': 'https://musescore.com/classicman/fur-elise',
|
||||||
@@ -31,22 +33,28 @@ class MuseScoreIE(InfoExtractor):
|
|||||||
'id': '33816',
|
'id': '33816',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'Für Elise – Beethoven',
|
'title': 'Für Elise – Beethoven',
|
||||||
'description': 'md5:49515a3556d5ecaf9fa4b2514064ac34',
|
'description': 'md5:e37b241c0280b33e9ac25651b815d06e',
|
||||||
'thumbnail': r're:https?://(?:www\.)?musescore\.com/.*\.png[^$]+',
|
'thumbnail': r're:https?://cdn\.ustatik\.com/musescore/.*\.jpg',
|
||||||
'uploader': 'ClassicMan',
|
'uploader': 'ClassicMan',
|
||||||
'creator': 'Ludwig van Beethoven (1770–1827)',
|
'creators': ['Ludwig van Beethoven (1770–1827)'],
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://musescore.com/minh_cuteee/scores/6555384',
|
'url': 'https://musescore.com/minh_cuteee/scores/6555384',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _generate_auth_token(video_id):
|
||||||
|
return hashlib.md5((video_id + 'mp30gs').encode()).hexdigest()[:4]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
webpage = self._download_webpage(url, None)
|
webpage = self._download_webpage(url, None)
|
||||||
url = self._og_search_url(webpage) or url
|
url = self._og_search_url(webpage) or url
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
mp3_url = self._download_json(f'https://musescore.com/api/jmuse?id={video_id}&index=0&type=mp3&v2=1', video_id,
|
mp3_url = self._download_json(
|
||||||
headers={'authorization': '63794e5461e4cfa046edfbdddfccc1ac16daffd2'})['info']['url']
|
'https://musescore.com/api/jmuse', video_id,
|
||||||
|
headers={'authorization': self._generate_auth_token(video_id)},
|
||||||
|
query={'id': video_id, 'index': '0', 'type': 'mp3'})['info']['url']
|
||||||
formats = [{
|
formats = [{
|
||||||
'url': mp3_url,
|
'url': mp3_url,
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
@@ -57,7 +65,7 @@ class MuseScoreIE(InfoExtractor):
|
|||||||
'id': video_id,
|
'id': video_id,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'title': self._og_search_title(webpage),
|
'title': self._og_search_title(webpage),
|
||||||
'description': self._og_search_description(webpage),
|
'description': self._html_search_meta('description', webpage, 'description'),
|
||||||
'thumbnail': self._og_search_thumbnail(webpage),
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
'uploader': self._html_search_meta('musescore:author', webpage, 'uploader'),
|
'uploader': self._html_search_meta('musescore:author', webpage, 'uploader'),
|
||||||
'creator': self._html_search_meta('musescore:composer', webpage, 'composer'),
|
'creator': self._html_search_meta('musescore:composer', webpage, 'composer'),
|
||||||
|
|||||||
@@ -50,7 +50,14 @@ class NewsPicksIE(InfoExtractor):
|
|||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
fragment = self._search_nextjs_data(webpage, video_id)['props']['pageProps']['fragment']
|
fragment = self._search_nextjs_data(webpage, video_id)['props']['pageProps']['fragment']
|
||||||
m3u8_url = traverse_obj(fragment, ('movie', 'movieUrl', {url_or_none}, {require('m3u8 URL')}))
|
movie = fragment['movie']
|
||||||
|
|
||||||
|
if traverse_obj(movie, ('viewable', {str})) == 'PARTIAL_FREE' and not traverse_obj(movie, ('canWatch', {bool})):
|
||||||
|
self.report_warning(
|
||||||
|
'Full video is for Premium members. Without cookies, '
|
||||||
|
f'only the preview is downloaded. {self._login_hint()}', video_id)
|
||||||
|
|
||||||
|
m3u8_url = traverse_obj(movie, ('movieUrl', {url_or_none}, {require('m3u8 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')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -59,12 +66,12 @@ class NewsPicksIE(InfoExtractor):
|
|||||||
'series': traverse_obj(fragment, ('series', 'title', {str})),
|
'series': traverse_obj(fragment, ('series', 'title', {str})),
|
||||||
'series_id': series_id,
|
'series_id': series_id,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
**traverse_obj(fragment, ('movie', {
|
**traverse_obj(movie, {
|
||||||
'title': ('title', {str}),
|
'title': ('title', {str}),
|
||||||
'cast': ('relatedUsers', ..., 'displayName', {str}, filter, all, filter),
|
'cast': ('relatedUsers', ..., 'displayName', {str}, filter, all, filter),
|
||||||
'description': ('explanation', {clean_html}),
|
'description': ('explanation', {clean_html}),
|
||||||
'release_timestamp': ('onAirStartDate', {parse_iso8601}),
|
'release_timestamp': ('onAirStartDate', {parse_iso8601}),
|
||||||
'thumbnail': (('image', 'coverImageUrl'), {url_or_none}, any),
|
'thumbnail': (('image', 'coverImageUrl'), {url_or_none}, any),
|
||||||
'timestamp': ('published', {parse_iso8601}),
|
'timestamp': ('published', {parse_iso8601}),
|
||||||
})),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class NownessSeriesIE(NownessBaseIE):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id, series = self._api_request(url, 'series/getBySlug/%s')
|
_, series = self._api_request(url, 'series/getBySlug/%s')
|
||||||
entries = [self._extract_url_result(post) for post in series['posts']]
|
entries = [self._extract_url_result(post) for post in series['posts']]
|
||||||
series_title = None
|
series_title = None
|
||||||
series_description = None
|
series_description = None
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import base64
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..networking.exceptions import HTTPError
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
clean_html,
|
||||||
|
int_or_none,
|
||||||
|
parse_qs,
|
||||||
|
str_or_none,
|
||||||
|
strftime_or_none,
|
||||||
|
update_url,
|
||||||
|
update_url_query,
|
||||||
|
url_or_none,
|
||||||
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
|
class OnsenIE(InfoExtractor):
|
||||||
|
IE_NAME = 'onsen'
|
||||||
|
IE_DESC = 'インターネットラジオステーション<音泉>'
|
||||||
|
|
||||||
|
_BASE_URL = 'https://www.onsen.ag'
|
||||||
|
_HEADERS = {'Referer': f'{_BASE_URL}/'}
|
||||||
|
_NETRC_MACHINE = 'onsen'
|
||||||
|
_VALID_URL = r'https?://(?:(?:share|www)\.)onsen\.ag/program/(?P<id>[^/?#]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://share.onsen.ag/program/onsenking?p=90&c=MTA0NjI',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '10462',
|
||||||
|
'ext': 'm4a',
|
||||||
|
'title': '第SP回',
|
||||||
|
'cast': 'count:3',
|
||||||
|
'description': 'md5:de62c80a41c4c8d84da53a1ee681ad18',
|
||||||
|
'display_id': 'MTA0NjI=',
|
||||||
|
'media_type': 'sound',
|
||||||
|
'section_start': 0,
|
||||||
|
'series': '音泉キング「下野紘」のラジオ きみはもちろん、<音泉>ファミリーだよね?',
|
||||||
|
'series_id': 'onsenking',
|
||||||
|
'tags': 'count:2',
|
||||||
|
'thumbnail': r're:https?://d3bzklg4lms4gh\.cloudfront\.net/program_info/image/default/production/.+',
|
||||||
|
'upload_date': '20220627',
|
||||||
|
'webpage_url': 'https://www.onsen.ag/program/onsenking?c=MTA0NjI=',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://share.onsen.ag/program/girls-band-cry-radio?p=370&c=MTgwMDE',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '18001',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '第4回',
|
||||||
|
'cast': 'count:5',
|
||||||
|
'description': 'md5:bbca8a389d99c90cbbce8f383c85fedd',
|
||||||
|
'display_id': 'MTgwMDE=',
|
||||||
|
'media_type': 'movie',
|
||||||
|
'section_start': 0,
|
||||||
|
'series': 'TVアニメ『ガールズバンドクライ』WEBラジオ「ガールズバンドクライ~ラジオにも全部ぶち込め。~」',
|
||||||
|
'series_id': 'girls-band-cry-radio',
|
||||||
|
'tags': 'count:3',
|
||||||
|
'thumbnail': r're:https?://d3bzklg4lms4gh\.cloudfront\.net/program_info/image/default/production/.+',
|
||||||
|
'upload_date': '20240425',
|
||||||
|
'webpage_url': 'https://www.onsen.ag/program/girls-band-cry-radio?c=MTgwMDE=',
|
||||||
|
},
|
||||||
|
'skip': 'Only available for premium supporters',
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.onsen.ag/program/uma',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'uma',
|
||||||
|
'title': 'UMA YELL RADIO',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 35,
|
||||||
|
}]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_encoded_id(program):
|
||||||
|
return base64.urlsafe_b64encode(str(program['id']).encode()).decode()
|
||||||
|
|
||||||
|
def _perform_login(self, username, password):
|
||||||
|
sign_in = self._download_json(
|
||||||
|
f'{self._BASE_URL}/web_api/signin', None, 'Logging in', headers={
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}, data=json.dumps({
|
||||||
|
'session': {
|
||||||
|
'email': username,
|
||||||
|
'password': password,
|
||||||
|
},
|
||||||
|
}).encode(), expected_status=401)
|
||||||
|
|
||||||
|
if sign_in.get('error'):
|
||||||
|
raise ExtractorError('Invalid username or password', expected=True)
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
program_id = self._match_id(url)
|
||||||
|
try:
|
||||||
|
programs = self._download_json(
|
||||||
|
f'{self._BASE_URL}/web_api/programs/{program_id}', program_id)
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, HTTPError) and e.cause.status == 404:
|
||||||
|
raise ExtractorError('Invalid URL', expected=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
query = {k: v[-1] for k, v in parse_qs(url).items() if v}
|
||||||
|
if 'c' not in query:
|
||||||
|
entries = [
|
||||||
|
self.url_result(update_url_query(url, {'c': self._get_encoded_id(program)}), OnsenIE)
|
||||||
|
for program in traverse_obj(programs, ('contents', lambda _, v: v['id']))
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.playlist_result(
|
||||||
|
entries, program_id, traverse_obj(programs, ('program_info', 'title', {clean_html})))
|
||||||
|
|
||||||
|
raw_id = base64.urlsafe_b64decode(f'{query["c"]}===').decode()
|
||||||
|
p_keys = ('contents', lambda _, v: v['id'] == int(raw_id))
|
||||||
|
|
||||||
|
program = traverse_obj(programs, (*p_keys, any))
|
||||||
|
if not program:
|
||||||
|
raise ExtractorError(
|
||||||
|
'This program is no longer available', expected=True)
|
||||||
|
m3u8_url = traverse_obj(program, ('streaming_url', {url_or_none}))
|
||||||
|
if not m3u8_url:
|
||||||
|
self.raise_login_required(
|
||||||
|
'This program is only available for premium supporters')
|
||||||
|
|
||||||
|
display_id = self._get_encoded_id(program)
|
||||||
|
date_str = self._search_regex(
|
||||||
|
rf'{program_id}0?(\d{{6}})', m3u8_url, 'date string', default=None)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'display_id': display_id,
|
||||||
|
'formats': self._extract_m3u8_formats(m3u8_url, raw_id, headers=self._HEADERS),
|
||||||
|
'http_headers': self._HEADERS,
|
||||||
|
'section_start': int_or_none(query.get('t', 0)),
|
||||||
|
'upload_date': strftime_or_none(f'20{date_str}'),
|
||||||
|
'webpage_url': f'{self._BASE_URL}/program/{program_id}?c={display_id}',
|
||||||
|
**traverse_obj(program, {
|
||||||
|
'id': ('id', {int}, {str_or_none}),
|
||||||
|
'title': ('title', {clean_html}),
|
||||||
|
'media_type': ('media_type', {str}),
|
||||||
|
'thumbnail': ('poster_image_url', {url_or_none}, {update_url(query=None)}),
|
||||||
|
}),
|
||||||
|
**traverse_obj(programs, {
|
||||||
|
'cast': (('performers', (*p_keys, 'guests')), ..., 'name', {str}, filter),
|
||||||
|
'series_id': ('directory_name', {str}),
|
||||||
|
}),
|
||||||
|
**traverse_obj(programs, ('program_info', {
|
||||||
|
'description': ('description', {clean_html}, filter),
|
||||||
|
'series': ('title', {clean_html}),
|
||||||
|
'tags': ('hashtag_list', ..., {str}, filter),
|
||||||
|
})),
|
||||||
|
}
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
import itertools
|
|
||||||
|
|
||||||
from .cbs import CBSBaseIE
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import (
|
|
||||||
ExtractorError,
|
|
||||||
int_or_none,
|
|
||||||
url_or_none,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ParamountPlusIE(CBSBaseIE):
|
|
||||||
_VALID_URL = r'''(?x)
|
|
||||||
(?:
|
|
||||||
paramountplus:|
|
|
||||||
https?://(?:www\.)?(?:
|
|
||||||
paramountplus\.com/(?:shows|movies)/(?:video|[^/]+/video|[^/]+)/
|
|
||||||
)(?P<id>[\w-]+))'''
|
|
||||||
|
|
||||||
# All tests are blocked outside US
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.paramountplus.com/shows/video/Oe44g5_NrlgiZE3aQVONleD6vXc8kP0k/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'Oe44g5_NrlgiZE3aQVONleD6vXc8kP0k',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'CatDog - Climb Every CatDog/The Canine Mutiny',
|
|
||||||
'description': 'md5:7ac835000645a69933df226940e3c859',
|
|
||||||
'duration': 1426,
|
|
||||||
'timestamp': 920264400,
|
|
||||||
'upload_date': '19990301',
|
|
||||||
'uploader': 'CBSI-NEW',
|
|
||||||
'episode_number': 5,
|
|
||||||
'thumbnail': r're:https?://.+\.jpg$',
|
|
||||||
'season': 'Season 2',
|
|
||||||
'chapters': 'count:3',
|
|
||||||
'episode': 'Episode 5',
|
|
||||||
'season_number': 2,
|
|
||||||
'series': 'CatDog',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': 'm3u8',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.paramountplus.com/shows/video/6hSWYWRrR9EUTz7IEe5fJKBhYvSUfexd/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '6hSWYWRrR9EUTz7IEe5fJKBhYvSUfexd',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': '7/23/21 WEEK IN REVIEW (Rep. Jahana Hayes/Howard Fineman/Sen. Michael Bennet/Sheera Frenkel & Cecilia Kang)',
|
|
||||||
'description': 'md5:f4adcea3e8b106192022e121f1565bae',
|
|
||||||
'duration': 2506,
|
|
||||||
'timestamp': 1627063200,
|
|
||||||
'upload_date': '20210723',
|
|
||||||
'uploader': 'CBSI-NEW',
|
|
||||||
'episode_number': 81,
|
|
||||||
'thumbnail': r're:https?://.+\.jpg$',
|
|
||||||
'season': 'Season 2',
|
|
||||||
'chapters': 'count:4',
|
|
||||||
'episode': 'Episode 81',
|
|
||||||
'season_number': 2,
|
|
||||||
'series': 'Tooning Out The News',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': 'm3u8',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.paramountplus.com/movies/video/vM2vm0kE6vsS2U41VhMRKTOVHyQAr6pC/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'vM2vm0kE6vsS2U41VhMRKTOVHyQAr6pC',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Daddy\'s Home',
|
|
||||||
'upload_date': '20151225',
|
|
||||||
'description': 'md5:9a6300c504d5e12000e8707f20c54745',
|
|
||||||
'uploader': 'CBSI-NEW',
|
|
||||||
'timestamp': 1451030400,
|
|
||||||
'thumbnail': r're:https?://.+\.jpg$',
|
|
||||||
'chapters': 'count:0',
|
|
||||||
'duration': 5761,
|
|
||||||
'series': 'Paramount+ Movies',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': 'm3u8',
|
|
||||||
},
|
|
||||||
'skip': 'DRM',
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.paramountplus.com/movies/video/5EKDXPOzdVf9voUqW6oRuocyAEeJGbEc/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '5EKDXPOzdVf9voUqW6oRuocyAEeJGbEc',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'uploader': 'CBSI-NEW',
|
|
||||||
'description': 'md5:bc7b6fea84ba631ef77a9bda9f2ff911',
|
|
||||||
'timestamp': 1577865600,
|
|
||||||
'title': 'Sonic the Hedgehog',
|
|
||||||
'upload_date': '20200101',
|
|
||||||
'thumbnail': r're:https?://.+\.jpg$',
|
|
||||||
'chapters': 'count:0',
|
|
||||||
'duration': 5932,
|
|
||||||
'series': 'Paramount+ Movies',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': 'm3u8',
|
|
||||||
},
|
|
||||||
'skip': 'DRM',
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.paramountplus.com/shows/the-real-world/video/mOVeHeL9ub9yWdyzSZFYz8Uj4ZBkVzQg/the-real-world-reunion/',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.paramountplus.com/shows/video/mOVeHeL9ub9yWdyzSZFYz8Uj4ZBkVzQg/',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.paramountplus.com/movies/video/W0VyStQqUnqKzJkrpSAIARuCc9YuYGNy/',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.paramountplus.com/movies/paw-patrol-the-movie/W0VyStQqUnqKzJkrpSAIARuCc9YuYGNy/',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _extract_video_info(self, content_id, mpx_acc=2198311517):
|
|
||||||
items_data = self._download_json(
|
|
||||||
f'https://www.paramountplus.com/apps-api/v2.0/androidtv/video/cid/{content_id}.json',
|
|
||||||
content_id, query={
|
|
||||||
'locale': 'en-us',
|
|
||||||
'at': 'ABCXgPuoStiPipsK0OHVXIVh68zNys+G4f7nW9R6qH68GDOcneW6Kg89cJXGfiQCsj0=',
|
|
||||||
}, headers=self.geo_verification_headers())
|
|
||||||
|
|
||||||
asset_types = {
|
|
||||||
item.get('assetType'): {
|
|
||||||
'format': 'SMIL',
|
|
||||||
'formats': 'M3U+none,MPEG4', # '+none' specifies ProtectionScheme (no DRM)
|
|
||||||
} for item in items_data['itemList']
|
|
||||||
}
|
|
||||||
item = items_data['itemList'][-1]
|
|
||||||
|
|
||||||
info, error = {}, None
|
|
||||||
metadata = {
|
|
||||||
'title': item.get('title'),
|
|
||||||
'series': item.get('seriesTitle'),
|
|
||||||
'season_number': int_or_none(item.get('seasonNum')),
|
|
||||||
'episode_number': int_or_none(item.get('episodeNum')),
|
|
||||||
'duration': int_or_none(item.get('duration')),
|
|
||||||
'thumbnail': url_or_none(item.get('thumbnail')),
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
info = self._extract_common_video_info(content_id, asset_types, mpx_acc, extra_info=metadata)
|
|
||||||
except ExtractorError as e:
|
|
||||||
error = e
|
|
||||||
|
|
||||||
# Check for DRM formats to give appropriate error
|
|
||||||
if not info.get('formats'):
|
|
||||||
for query in asset_types.values():
|
|
||||||
query['formats'] = 'MPEG-DASH,M3U,MPEG4' # allows DRM formats
|
|
||||||
|
|
||||||
try:
|
|
||||||
drm_info = self._extract_common_video_info(content_id, asset_types, mpx_acc, extra_info=metadata)
|
|
||||||
except ExtractorError:
|
|
||||||
if error:
|
|
||||||
raise error from None
|
|
||||||
raise
|
|
||||||
if drm_info['formats']:
|
|
||||||
self.report_drm(content_id)
|
|
||||||
elif error:
|
|
||||||
raise error
|
|
||||||
|
|
||||||
return info
|
|
||||||
|
|
||||||
|
|
||||||
class ParamountPlusSeriesIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?paramountplus\.com/shows/(?P<id>[a-zA-Z0-9-_]+)/?(?:[#?]|$)'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.paramountplus.com/shows/drake-josh',
|
|
||||||
'playlist_mincount': 50,
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'drake-josh',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.paramountplus.com/shows/hawaii_five_0/',
|
|
||||||
'playlist_mincount': 240,
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'hawaii_five_0',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.paramountplus.com/shows/spongebob-squarepants/',
|
|
||||||
'playlist_mincount': 248,
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'spongebob-squarepants',
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _entries(self, show_name):
|
|
||||||
for page in itertools.count():
|
|
||||||
show_json = self._download_json(
|
|
||||||
f'https://www.paramountplus.com/shows/{show_name}/xhr/episodes/page/{page}/size/50/xs/0/season/0', show_name)
|
|
||||||
if not show_json.get('success'):
|
|
||||||
return
|
|
||||||
for episode in show_json['result']['data']:
|
|
||||||
yield self.url_result(
|
|
||||||
'https://www.paramountplus.com{}'.format(episode['url']),
|
|
||||||
ie=ParamountPlusIE.ie_key(), video_id=episode['content_id'])
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
show_name = self._match_id(url)
|
|
||||||
return self.playlist_result(self._entries(show_name), playlist_id=show_name)
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
from .common import InfoExtractor
|
|
||||||
from ..networking.exceptions import HTTPError
|
|
||||||
from ..utils import (
|
|
||||||
ExtractorError,
|
|
||||||
traverse_obj,
|
|
||||||
unified_timestamp,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PixivSketchBaseIE(InfoExtractor):
|
|
||||||
def _call_api(self, video_id, path, referer, note='Downloading JSON metadata'):
|
|
||||||
response = self._download_json(f'https://sketch.pixiv.net/api/{path}', video_id, note=note, headers={
|
|
||||||
'Referer': referer,
|
|
||||||
'X-Requested-With': referer,
|
|
||||||
})
|
|
||||||
errors = traverse_obj(response, ('errors', ..., 'message'))
|
|
||||||
if errors:
|
|
||||||
raise ExtractorError(' '.join(f'{e}.' for e in errors))
|
|
||||||
return response.get('data') or {}
|
|
||||||
|
|
||||||
|
|
||||||
class PixivSketchIE(PixivSketchBaseIE):
|
|
||||||
IE_NAME = 'pixiv:sketch'
|
|
||||||
_VALID_URL = r'https?://sketch\.pixiv\.net/@(?P<uploader_id>[a-zA-Z0-9_-]+)/lives/(?P<id>\d+)/?'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://sketch.pixiv.net/@nuhutya/lives/3654620468641830507',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '7370666691623196569',
|
|
||||||
'title': 'まにあえクリスマス!',
|
|
||||||
'uploader': 'ぬふちゃ',
|
|
||||||
'uploader_id': 'nuhutya',
|
|
||||||
'channel_id': '9844815',
|
|
||||||
'age_limit': 0,
|
|
||||||
'timestamp': 1640351536,
|
|
||||||
},
|
|
||||||
'skip': True,
|
|
||||||
}, {
|
|
||||||
# these two (age_limit > 0) requires you to login on website, but it's actually not required for download
|
|
||||||
'url': 'https://sketch.pixiv.net/@namahyou/lives/4393103321546851377',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '4907995960957946943',
|
|
||||||
'title': 'クリスマスなんて知らん🖕',
|
|
||||||
'uploader': 'すゃもり',
|
|
||||||
'uploader_id': 'suya2mori2',
|
|
||||||
'channel_id': '31169300',
|
|
||||||
'age_limit': 15,
|
|
||||||
'timestamp': 1640347640,
|
|
||||||
},
|
|
||||||
'skip': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://sketch.pixiv.net/@8aki/lives/3553803162487249670',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '1593420639479156945',
|
|
||||||
'title': 'おまけ本作業(リョナ有)',
|
|
||||||
'uploader': 'おぶい / Obui',
|
|
||||||
'uploader_id': 'oving',
|
|
||||||
'channel_id': '17606',
|
|
||||||
'age_limit': 18,
|
|
||||||
'timestamp': 1640330263,
|
|
||||||
},
|
|
||||||
'skip': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id, uploader_id = self._match_valid_url(url).group('id', 'uploader_id')
|
|
||||||
data = self._call_api(video_id, f'lives/{video_id}.json', url)
|
|
||||||
|
|
||||||
if not traverse_obj(data, 'is_broadcasting'):
|
|
||||||
raise ExtractorError(f'This live is offline. Use https://sketch.pixiv.net/@{uploader_id} for ongoing live.', expected=True)
|
|
||||||
|
|
||||||
m3u8_url = traverse_obj(data, ('owner', 'hls_movie', 'url'))
|
|
||||||
formats = self._extract_m3u8_formats(
|
|
||||||
m3u8_url, video_id, ext='mp4',
|
|
||||||
entry_protocol='m3u8_native', m3u8_id='hls')
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': data.get('name'),
|
|
||||||
'formats': formats,
|
|
||||||
'uploader': traverse_obj(data, ('user', 'name'), ('owner', 'user', 'name')),
|
|
||||||
'uploader_id': traverse_obj(data, ('user', 'unique_name'), ('owner', 'user', 'unique_name')),
|
|
||||||
'channel_id': str(traverse_obj(data, ('user', 'pixiv_user_id'), ('owner', 'user', 'pixiv_user_id'))),
|
|
||||||
'age_limit': 18 if data.get('is_r18') else 15 if data.get('is_r15') else 0,
|
|
||||||
'timestamp': unified_timestamp(data.get('created_at')),
|
|
||||||
'is_live': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PixivSketchUserIE(PixivSketchBaseIE):
|
|
||||||
IE_NAME = 'pixiv:sketch:user'
|
|
||||||
_VALID_URL = r'https?://sketch\.pixiv\.net/@(?P<id>[a-zA-Z0-9_-]+)/?'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://sketch.pixiv.net/@nuhutya',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://sketch.pixiv.net/@namahyou',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://sketch.pixiv.net/@8aki',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def suitable(cls, url):
|
|
||||||
return super().suitable(url) and not PixivSketchIE.suitable(url)
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
user_id = self._match_id(url)
|
|
||||||
data = self._call_api(user_id, f'lives/users/@{user_id}.json', url)
|
|
||||||
|
|
||||||
if not traverse_obj(data, 'is_broadcasting'):
|
|
||||||
try:
|
|
||||||
self._call_api(user_id, 'users/current.json', url, 'Investigating reason for request failure')
|
|
||||||
except ExtractorError as e:
|
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status == 401:
|
|
||||||
self.raise_login_required(f'Please log in, or use direct link like https://sketch.pixiv.net/@{user_id}/1234567890', method='cookies')
|
|
||||||
raise ExtractorError('This user is offline', expected=True)
|
|
||||||
|
|
||||||
return self.url_result(f'https://sketch.pixiv.net/@{user_id}/lives/{data["id"]}')
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import float_or_none, parse_iso8601, str_or_none, try_call
|
from ..utils import float_or_none, parse_iso8601, str_or_none, try_call, url_or_none
|
||||||
from ..utils.traversal import traverse_obj
|
from ..utils.traversal import traverse_obj, value
|
||||||
|
|
||||||
|
|
||||||
class PrankCastIE(InfoExtractor):
|
class PrankCastIE(InfoExtractor):
|
||||||
@@ -100,9 +100,38 @@ class PrankCastPostIE(InfoExtractor):
|
|||||||
'duration': 263.287,
|
'duration': 263.287,
|
||||||
'cast': ['despicabledogs'],
|
'cast': ['despicabledogs'],
|
||||||
'description': 'https://imgur.com/a/vtxLvKU',
|
'description': 'https://imgur.com/a/vtxLvKU',
|
||||||
'categories': [],
|
|
||||||
'upload_date': '20240104',
|
'upload_date': '20240104',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://prankcast.com/drtomservo/posts/11988-butteye-s-late-night-stank-episode-1-part-1-',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '11988',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': 'Butteye\'s Late Night Stank Episode 1 (Part 1)',
|
||||||
|
'display_id': 'butteye-s-late-night-stank-episode-1-part-1-',
|
||||||
|
'timestamp': 1754238686,
|
||||||
|
'uploader': 'DrTomServo',
|
||||||
|
'channel_id': '136',
|
||||||
|
'duration': 2176.464,
|
||||||
|
'cast': ['DrTomServo'],
|
||||||
|
'description': '',
|
||||||
|
'upload_date': '20250803',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://prankcast.com/drtomservo/posts/12105-butteye-s-late-night-stank-episode-08-16-2025-part-2',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '12105',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': 'Butteye\'s Late Night Stank Episode 08-16-2025 Part 2',
|
||||||
|
'display_id': 'butteye-s-late-night-stank-episode-08-16-2025-part-2',
|
||||||
|
'timestamp': 1755453505,
|
||||||
|
'uploader': 'DrTomServo',
|
||||||
|
'channel_id': '136',
|
||||||
|
'duration': 19018.392,
|
||||||
|
'cast': ['DrTomServo'],
|
||||||
|
'description': '',
|
||||||
|
'upload_date': '20250817',
|
||||||
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -112,26 +141,28 @@ class PrankCastPostIE(InfoExtractor):
|
|||||||
post = self._search_nextjs_data(webpage, video_id)['props']['pageProps']['ssr_data_posts']
|
post = self._search_nextjs_data(webpage, video_id)['props']['pageProps']['ssr_data_posts']
|
||||||
content = self._parse_json(post['post_contents_json'], video_id)[0]
|
content = self._parse_json(post['post_contents_json'], video_id)[0]
|
||||||
|
|
||||||
uploader = post.get('user_name')
|
|
||||||
guests_json = traverse_obj(content, ('guests_json', {json.loads}, {dict})) or {}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': post.get('post_title') or self._og_search_title(webpage),
|
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'url': content.get('url'),
|
'title': self._og_search_title(webpage),
|
||||||
'timestamp': parse_iso8601(content.get('start_date') or content.get('crdate'), ' '),
|
**traverse_obj(post, {
|
||||||
'uploader': uploader,
|
'title': ('post_title', {str}),
|
||||||
'channel_id': str_or_none(post.get('user_id')),
|
'description': ('post_body', {str}),
|
||||||
'duration': float_or_none(content.get('duration')),
|
'tags': ('post_tags', {lambda x: x.split(',')}, ..., {str.strip}, filter),
|
||||||
'cast': list(filter(None, [uploader, *traverse_obj(guests_json, (..., 'name'))])),
|
'channel_id': ('user_id', {int}, {str_or_none}),
|
||||||
'description': post.get('post_body'),
|
'uploader': ('user_name', {str}),
|
||||||
'categories': list(filter(None, [content.get('category')])),
|
}),
|
||||||
'tags': try_call(lambda: list(filter('', post['post_tags'].split(',')))),
|
**traverse_obj(content, {
|
||||||
'subtitles': {
|
'url': (('secure_url', 'url'), {url_or_none}, any),
|
||||||
'live_chat': [{
|
'timestamp': ((
|
||||||
'url': f'https://prankcast.com/api/private/chat/select-broadcast?id={post["content_id"]}&cache=',
|
(('start_date', 'crdate'), {parse_iso8601(delimiter=' ')}),
|
||||||
'ext': 'json',
|
('created_at', {parse_iso8601}),
|
||||||
}],
|
), any),
|
||||||
} if post.get('content_id') else None,
|
'duration': ('duration', {float_or_none}),
|
||||||
|
'categories': ('category', {str}, filter, all, filter),
|
||||||
|
'cast': ((
|
||||||
|
{value(post.get('user_name'))},
|
||||||
|
('guests_json', {json.loads}, ..., 'name'),
|
||||||
|
), {str}, filter),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ class RadioFranceProgramScheduleIE(RadioFranceBaseIE):
|
|||||||
_VALID_URL = rf'''(?x)
|
_VALID_URL = rf'''(?x)
|
||||||
{RadioFranceBaseIE._VALID_URL_BASE}
|
{RadioFranceBaseIE._VALID_URL_BASE}
|
||||||
/(?P<station>{RadioFranceBaseIE._STATIONS_RE})
|
/(?P<station>{RadioFranceBaseIE._STATIONS_RE})
|
||||||
/grille-programmes(?:\?date=(?P<date>[\d-]+))?
|
/grille-programmes
|
||||||
'''
|
'''
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
@@ -463,7 +463,7 @@ class RadioFranceProgramScheduleIE(RadioFranceBaseIE):
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
station, date = self._match_valid_url(url).group('station', 'date')
|
station = self._match_valid_url(url).group('station')
|
||||||
webpage = self._download_webpage(url, station)
|
webpage = self._download_webpage(url, station)
|
||||||
grid_data = self._extract_data_from_webpage(webpage, station, 'grid')
|
grid_data = self._extract_data_from_webpage(webpage, station, 'grid')
|
||||||
upload_date = strftime_or_none(grid_data.get('date'), '%Y%m%d')
|
upload_date = strftime_or_none(grid_data.get('date'), '%Y%m%d')
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ class RCTIPlusSeriesIE(RCTIPlusBaseIE):
|
|||||||
f'Only {video_type} will be downloaded. '
|
f'Only {video_type} will be downloaded. '
|
||||||
f'To download everything from the series, remove "/{video_type}" from the URL')
|
f'To download everything from the series, remove "/{video_type}" from the URL')
|
||||||
|
|
||||||
series_meta, meta_paths = self._call_api(
|
series_meta, _ = self._call_api(
|
||||||
f'https://api.rctiplus.com/api/v1/program/{series_id}/detail', display_id, 'Downloading series metadata')
|
f'https://api.rctiplus.com/api/v1/program/{series_id}/detail', display_id, 'Downloading series metadata')
|
||||||
metadata = {
|
metadata = {
|
||||||
'age_limit': try_get(series_meta, lambda x: self._AGE_RATINGS[x['age_restriction'][0]['code']]),
|
'age_limit': try_get(series_meta, lambda x: self._AGE_RATINGS[x['age_restriction'][0]['code']]),
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import ExtractorError, int_or_none, str_to_int
|
|
||||||
|
|
||||||
|
|
||||||
class RUTVIE(InfoExtractor):
|
|
||||||
IE_DESC = 'RUTV.RU'
|
|
||||||
_VALID_URL = r'''(?x)
|
|
||||||
https?://
|
|
||||||
(?:test)?player\.(?:rutv\.ru|vgtrk\.com)/
|
|
||||||
(?P<path>
|
|
||||||
flash\d+v/container\.swf\?id=|
|
|
||||||
iframe/(?P<type>swf|video|live)/id/|
|
|
||||||
index/iframe/cast_id/
|
|
||||||
)
|
|
||||||
(?P<id>\d+)
|
|
||||||
'''
|
|
||||||
_EMBED_REGEX = [
|
|
||||||
r'<iframe[^>]+?src=(["\'])(?P<url>https?://(?:test)?player\.(?:rutv\.ru|vgtrk\.com)/(?:iframe/(?:swf|video|live)/id|index/iframe/cast_id)/.+?)\1',
|
|
||||||
r'<meta[^>]+?property=(["\'])og:video\1[^>]+?content=(["\'])(?P<url>https?://(?:test)?player\.(?:rutv\.ru|vgtrk\.com)/flash\d+v/container\.swf\?id=.+?\2)',
|
|
||||||
]
|
|
||||||
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'http://player.rutv.ru/flash2v/container.swf?id=774471&sid=kultura&fbv=true&isPlay=true&ssl=false&i=560&acc_video_id=episode_id/972347/video_id/978186/brand_id/31724',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '774471',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Монологи на все времена. Концерт',
|
|
||||||
'description': 'md5:18d8b5e6a41fb1faa53819471852d5d5',
|
|
||||||
'duration': 2906,
|
|
||||||
'thumbnail': r're:https?://cdn-st2\.smotrim\.ru/.+\.jpg',
|
|
||||||
},
|
|
||||||
'params': {'skip_download': 'm3u8'},
|
|
||||||
}, {
|
|
||||||
'url': 'https://player.vgtrk.com/flash2v/container.swf?id=774016&sid=russiatv&fbv=true&isPlay=true&ssl=false&i=560&acc_video_id=episode_id/972098/video_id/977760/brand_id/57638',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '774016',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Чужой в семье Сталина',
|
|
||||||
'description': '',
|
|
||||||
'duration': 2539,
|
|
||||||
},
|
|
||||||
'skip': 'Invalid URL',
|
|
||||||
}, {
|
|
||||||
'url': 'http://player.rutv.ru/iframe/swf/id/766888/sid/hitech/?acc_video_id=4000',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '766888',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Вести.net: интернет-гиганты начали перетягивание программных "одеял"',
|
|
||||||
'description': 'md5:65ddd47f9830c4f42ed6475f8730c995',
|
|
||||||
'duration': 279,
|
|
||||||
'thumbnail': r're:https?://cdn-st2\.smotrim\.ru/.+\.jpg',
|
|
||||||
},
|
|
||||||
'params': {'skip_download': 'm3u8'},
|
|
||||||
}, {
|
|
||||||
'url': 'http://player.rutv.ru/iframe/video/id/771852/start_zoom/true/showZoomBtn/false/sid/russiatv/?acc_video_id=episode_id/970443/video_id/975648/brand_id/5169',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '771852',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Прямой эфир. Жертвы загадочной болезни: смерть от старости в 17 лет',
|
|
||||||
'description': 'md5:b81c8c55247a4bd996b43ce17395b2d8',
|
|
||||||
'duration': 3096,
|
|
||||||
'thumbnail': r're:https?://cdn-st2\.smotrim\.ru/.+\.jpg',
|
|
||||||
},
|
|
||||||
'params': {'skip_download': 'm3u8'},
|
|
||||||
}, {
|
|
||||||
'url': 'http://player.rutv.ru/iframe/live/id/51499/showZoomBtn/false/isPlay/true/sid/sochi2014',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '51499',
|
|
||||||
'ext': 'flv',
|
|
||||||
'title': 'Сочи-2014. Биатлон. Индивидуальная гонка. Мужчины ',
|
|
||||||
'description': 'md5:9e0ed5c9d2fa1efbfdfed90c9a6d179c',
|
|
||||||
},
|
|
||||||
'skip': 'Invalid URL',
|
|
||||||
}, {
|
|
||||||
'url': 'http://player.rutv.ru/iframe/live/id/21/showZoomBtn/false/isPlay/true/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '21',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': str,
|
|
||||||
'is_live': True,
|
|
||||||
},
|
|
||||||
'skip': 'Invalid URL',
|
|
||||||
}, {
|
|
||||||
'url': 'https://testplayer.vgtrk.com/iframe/live/id/19201/showZoomBtn/false/isPlay/true/',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
_WEBPAGE_TESTS = [{
|
|
||||||
'url': 'http://istoriya-teatra.ru/news/item/f00/s05/n0000545/index.shtml',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '1952012',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Новости культуры. Эфир от 10.10.2019 (23:30). Театр Сатиры отмечает день рождения премьерой',
|
|
||||||
'description': 'md5:fced27112ff01ff8fc4a452fc088bad6',
|
|
||||||
'duration': 191,
|
|
||||||
'thumbnail': r're:https?://cdn-st2\.smotrim\.ru/.+\.jpg',
|
|
||||||
},
|
|
||||||
'params': {'skip_download': 'm3u8'},
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
mobj = self._match_valid_url(url)
|
|
||||||
video_id = mobj.group('id')
|
|
||||||
video_path = mobj.group('path')
|
|
||||||
|
|
||||||
if re.match(r'flash\d+v', video_path):
|
|
||||||
video_type = 'video'
|
|
||||||
elif video_path.startswith('iframe'):
|
|
||||||
video_type = mobj.group('type')
|
|
||||||
if video_type == 'swf':
|
|
||||||
video_type = 'video'
|
|
||||||
elif video_path.startswith('index/iframe/cast_id'):
|
|
||||||
video_type = 'live'
|
|
||||||
|
|
||||||
is_live = video_type == 'live'
|
|
||||||
|
|
||||||
json_data = self._download_json(
|
|
||||||
'http://player.vgtrk.com/iframe/data{}/id/{}'.format('live' if is_live else 'video', video_id),
|
|
||||||
video_id, 'Downloading JSON')
|
|
||||||
|
|
||||||
if json_data['errors']:
|
|
||||||
raise ExtractorError('{} said: {}'.format(self.IE_NAME, json_data['errors']), expected=True)
|
|
||||||
|
|
||||||
playlist = json_data['data']['playlist']
|
|
||||||
medialist = playlist['medialist']
|
|
||||||
media = medialist[0]
|
|
||||||
|
|
||||||
if media['errors']:
|
|
||||||
raise ExtractorError('{} said: {}'.format(self.IE_NAME, media['errors']), expected=True)
|
|
||||||
|
|
||||||
view_count = int_or_none(playlist.get('count_views'))
|
|
||||||
priority_transport = playlist['priority_transport']
|
|
||||||
|
|
||||||
thumbnail = media['picture']
|
|
||||||
width = int_or_none(media['width'])
|
|
||||||
height = int_or_none(media['height'])
|
|
||||||
description = media['anons']
|
|
||||||
title = media['title']
|
|
||||||
duration = int_or_none(media.get('duration'))
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
subtitles = {}
|
|
||||||
|
|
||||||
for transport, links in media['sources'].items():
|
|
||||||
for quality, url in links.items():
|
|
||||||
preference = -1 if priority_transport == transport else -2
|
|
||||||
if transport == 'rtmp':
|
|
||||||
mobj = re.search(r'^(?P<url>rtmp://[^/]+/(?P<app>.+))/(?P<playpath>.+)$', url)
|
|
||||||
if not mobj:
|
|
||||||
continue
|
|
||||||
fmt = {
|
|
||||||
'url': mobj.group('url'),
|
|
||||||
'play_path': mobj.group('playpath'),
|
|
||||||
'app': mobj.group('app'),
|
|
||||||
'page_url': 'http://player.rutv.ru',
|
|
||||||
'player_url': 'http://player.rutv.ru/flash3v/osmf.swf?i=22',
|
|
||||||
'rtmp_live': True,
|
|
||||||
'ext': 'flv',
|
|
||||||
'vbr': str_to_int(quality),
|
|
||||||
}
|
|
||||||
elif transport == 'm3u8':
|
|
||||||
fmt, subs = self._extract_m3u8_formats_and_subtitles(
|
|
||||||
url, video_id, 'mp4', quality=preference, m3u8_id='hls')
|
|
||||||
formats.extend(fmt)
|
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
fmt = {
|
|
||||||
'url': url,
|
|
||||||
}
|
|
||||||
fmt.update({
|
|
||||||
'width': int_or_none(quality, default=height, invscale=width, scale=height),
|
|
||||||
'height': int_or_none(quality, default=height),
|
|
||||||
'format_id': f'{transport}-{quality}',
|
|
||||||
'source_preference': preference,
|
|
||||||
})
|
|
||||||
formats.append(fmt)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'thumbnail': thumbnail,
|
|
||||||
'view_count': view_count,
|
|
||||||
'duration': duration,
|
|
||||||
'formats': formats,
|
|
||||||
'subtitles': subtitles,
|
|
||||||
'is_live': is_live,
|
|
||||||
'_format_sort_fields': ('source', ),
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import (
|
|
||||||
determine_ext,
|
|
||||||
int_or_none,
|
|
||||||
parse_qs,
|
|
||||||
qualities,
|
|
||||||
try_get,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SixPlayIE(InfoExtractor):
|
|
||||||
IE_NAME = '6play'
|
|
||||||
_VALID_URL = r'(?:6play:|https?://(?:www\.)?(?P<domain>6play\.fr|rtlplay\.be|play\.rtl\.hr|rtlmost\.hu)/.+?-c_)(?P<id>[0-9]+)'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.6play.fr/minute-par-minute-p_9533/le-but-qui-a-marque-lhistoire-du-football-francais-c_12041051',
|
|
||||||
'md5': '31fcd112637baa0c2ab92c4fcd8baf27',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '12041051',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Le but qui a marqué l\'histoire du football français !',
|
|
||||||
'description': 'md5:b59e7e841d646ef1eb42a7868eb6a851',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.rtlplay.be/rtl-info-13h-p_8551/les-titres-du-rtlinfo-13h-c_12045869',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://play.rtl.hr/pj-masks-p_9455/epizoda-34-sezona-1-catboyevo-cudo-na-dva-kotaca-c_11984989',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.rtlmost.hu/megtorve-p_14167/megtorve-6-resz-c_12397787',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
domain, video_id = self._match_valid_url(url).groups()
|
|
||||||
service, consumer_name = {
|
|
||||||
'6play.fr': ('6play', 'm6web'),
|
|
||||||
'rtlplay.be': ('rtlbe_rtl_play', 'rtlbe'),
|
|
||||||
'play.rtl.hr': ('rtlhr_rtl_play', 'rtlhr'),
|
|
||||||
'rtlmost.hu': ('rtlhu_rtl_most', 'rtlhu'),
|
|
||||||
}.get(domain, ('6play', 'm6web'))
|
|
||||||
|
|
||||||
data = self._download_json(
|
|
||||||
f'https://pc.middleware.6play.fr/6play/v2/platforms/m6group_web/services/{service}/videos/clip_{video_id}',
|
|
||||||
video_id, headers={
|
|
||||||
'x-customer-name': consumer_name,
|
|
||||||
}, query={
|
|
||||||
'csa': 5,
|
|
||||||
'with': 'clips',
|
|
||||||
})
|
|
||||||
|
|
||||||
clip_data = data['clips'][0]
|
|
||||||
title = clip_data['title']
|
|
||||||
|
|
||||||
urls = []
|
|
||||||
quality_key = qualities(['lq', 'sd', 'hq', 'hd'])
|
|
||||||
formats = []
|
|
||||||
subtitles = {}
|
|
||||||
assets = clip_data.get('assets') or []
|
|
||||||
for asset in assets:
|
|
||||||
asset_url = asset.get('full_physical_path')
|
|
||||||
protocol = asset.get('protocol')
|
|
||||||
if not asset_url or ((protocol == 'primetime' or asset.get('type') == 'usp_hlsfp_h264') and not ('_drmnp.ism/' in asset_url or '_unpnp.ism/' in asset_url)) or asset_url in urls:
|
|
||||||
continue
|
|
||||||
urls.append(asset_url)
|
|
||||||
container = asset.get('video_container')
|
|
||||||
ext = determine_ext(asset_url)
|
|
||||||
if protocol == 'http_subtitle' or ext == 'vtt':
|
|
||||||
subtitles.setdefault('fr', []).append({'url': asset_url})
|
|
||||||
continue
|
|
||||||
if container == 'm3u8' or ext == 'm3u8':
|
|
||||||
if protocol == 'usp':
|
|
||||||
if parse_qs(asset_url).get('token', [None])[0]:
|
|
||||||
urlh = self._request_webpage(
|
|
||||||
asset_url, video_id, fatal=False,
|
|
||||||
headers=self.geo_verification_headers())
|
|
||||||
if not urlh:
|
|
||||||
continue
|
|
||||||
asset_url = urlh.url
|
|
||||||
asset_url = asset_url.replace('_drmnp.ism/', '_unpnp.ism/')
|
|
||||||
for i in range(3, 0, -1):
|
|
||||||
asset_url = asset_url.replace('_sd1/', f'_sd{i}/')
|
|
||||||
m3u8_formats = self._extract_m3u8_formats(
|
|
||||||
asset_url, video_id, 'mp4', 'm3u8_native',
|
|
||||||
m3u8_id='hls', fatal=False)
|
|
||||||
formats.extend(m3u8_formats)
|
|
||||||
formats.extend(self._extract_mpd_formats(
|
|
||||||
asset_url.replace('.m3u8', '.mpd'),
|
|
||||||
video_id, mpd_id='dash', fatal=False))
|
|
||||||
if m3u8_formats:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
formats.extend(self._extract_m3u8_formats(
|
|
||||||
asset_url, video_id, 'mp4', 'm3u8_native',
|
|
||||||
m3u8_id='hls', fatal=False))
|
|
||||||
elif container == 'mp4' or ext == 'mp4':
|
|
||||||
quality = asset.get('video_quality')
|
|
||||||
formats.append({
|
|
||||||
'url': asset_url,
|
|
||||||
'format_id': quality,
|
|
||||||
'quality': quality_key(quality),
|
|
||||||
'ext': ext,
|
|
||||||
})
|
|
||||||
|
|
||||||
def get(getter):
|
|
||||||
for src in (data, clip_data):
|
|
||||||
v = try_get(src, getter, str)
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'description': get(lambda x: x['description']),
|
|
||||||
'duration': int_or_none(clip_data.get('duration')),
|
|
||||||
'series': get(lambda x: x['program']['title']),
|
|
||||||
'formats': formats,
|
|
||||||
'subtitles': subtitles,
|
|
||||||
}
|
|
||||||
@@ -248,35 +248,17 @@ class SlidesLiveIE(InfoExtractor):
|
|||||||
'skip_download': 'm3u8',
|
'skip_download': 'm3u8',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# /v3/ slides, .jpg and .png, service_name = youtube
|
# /v3/ slides, .jpg and .png, formerly service_name = youtube, now native
|
||||||
'url': 'https://slideslive.com/embed/38932460/',
|
'url': 'https://slideslive.com/embed/38932460/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'RTPdrgkyTiE',
|
'id': '38932460',
|
||||||
'display_id': '38932460',
|
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Active Learning for Hierarchical Multi-Label Classification',
|
'title': 'Active Learning for Hierarchical Multi-Label Classification',
|
||||||
'description': 'Watch full version of this video at https://slideslive.com/38932460.',
|
'duration': 941,
|
||||||
'channel': 'SlidesLive Videos - A',
|
'thumbnail': r're:https?://.+/.+\.(?:jpg|png)',
|
||||||
'channel_id': 'UC62SdArr41t_-_fX40QCLRw',
|
|
||||||
'channel_url': 'https://www.youtube.com/channel/UC62SdArr41t_-_fX40QCLRw',
|
|
||||||
'uploader': 'SlidesLive Videos - A',
|
|
||||||
'uploader_id': '@slideslivevideos-a6075',
|
|
||||||
'uploader_url': 'https://www.youtube.com/@slideslivevideos-a6075',
|
|
||||||
'upload_date': '20200903',
|
|
||||||
'timestamp': 1697805922,
|
|
||||||
'duration': 942,
|
|
||||||
'age_limit': 0,
|
|
||||||
'live_status': 'not_live',
|
|
||||||
'playable_in_embed': True,
|
|
||||||
'availability': 'unlisted',
|
|
||||||
'categories': ['People & Blogs'],
|
|
||||||
'tags': [],
|
|
||||||
'channel_follower_count': int,
|
|
||||||
'like_count': int,
|
|
||||||
'view_count': int,
|
|
||||||
'thumbnail': r're:^https?://.*\.(?:jpg|png|webp)',
|
|
||||||
'thumbnails': 'count:21',
|
|
||||||
'chapters': 'count:20',
|
'chapters': 'count:20',
|
||||||
|
'timestamp': 1708338974,
|
||||||
|
'upload_date': '20240219',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': 'm3u8',
|
'skip_download': 'm3u8',
|
||||||
@@ -425,7 +407,7 @@ class SlidesLiveIE(InfoExtractor):
|
|||||||
|
|
||||||
player_token = self._search_regex(r'data-player-token="([^"]+)"', webpage, 'player token')
|
player_token = self._search_regex(r'data-player-token="([^"]+)"', webpage, 'player token')
|
||||||
player_data = self._download_webpage(
|
player_data = self._download_webpage(
|
||||||
f'https://ben.slideslive.com/player/{video_id}', video_id,
|
f'https://slideslive.com/player/{video_id}', video_id,
|
||||||
note='Downloading player info', query={'player_token': player_token})
|
note='Downloading player info', query={'player_token': player_token})
|
||||||
player_info = self._extract_custom_m3u8_info(player_data)
|
player_info = self._extract_custom_m3u8_info(player_data)
|
||||||
|
|
||||||
@@ -525,7 +507,7 @@ class SlidesLiveIE(InfoExtractor):
|
|||||||
yield info
|
yield info
|
||||||
|
|
||||||
service_data = self._download_json(
|
service_data = self._download_json(
|
||||||
f'https://ben.slideslive.com/player/{video_id}/slides_video_service_data',
|
f'https://slideslive.com/player/{video_id}/slides_video_service_data',
|
||||||
video_id, fatal=False, query={
|
video_id, fatal=False, query={
|
||||||
'player_token': player_token,
|
'player_token': player_token,
|
||||||
'videos': ','.join(video_slides),
|
'videos': ','.join(video_slides),
|
||||||
|
|||||||
+383
-45
@@ -1,65 +1,403 @@
|
|||||||
|
import functools
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import ExtractorError
|
from ..utils import (
|
||||||
|
OnDemandPagedList,
|
||||||
|
clean_html,
|
||||||
|
determine_ext,
|
||||||
|
extract_attributes,
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
str_or_none,
|
||||||
|
unescapeHTML,
|
||||||
|
url_or_none,
|
||||||
|
urljoin,
|
||||||
|
)
|
||||||
|
from ..utils.traversal import (
|
||||||
|
find_element,
|
||||||
|
find_elements,
|
||||||
|
require,
|
||||||
|
traverse_obj,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SmotrimIE(InfoExtractor):
|
class SmotrimBaseIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://smotrim\.ru/(?P<type>brand|video|article|live)/(?P<id>[0-9]+)'
|
_BASE_URL = 'https://smotrim.ru'
|
||||||
_TESTS = [{ # video
|
_GEO_BYPASS = False
|
||||||
|
_GEO_COUNTRIES = ['RU']
|
||||||
|
|
||||||
|
def _extract_from_smotrim_api(self, typ, item_id):
|
||||||
|
path = f'data{typ.replace("-", "")}/{"uid" if typ == "live" else "id"}'
|
||||||
|
data = self._download_json(
|
||||||
|
f'https://player.smotrim.ru/iframe/{path}/{item_id}/sid/smotrim', item_id)
|
||||||
|
media = traverse_obj(data, ('data', 'playlist', 'medialist', -1, {dict}))
|
||||||
|
if traverse_obj(media, ('locked', {bool})):
|
||||||
|
self.raise_login_required()
|
||||||
|
if error_msg := traverse_obj(media, ('errors', {clean_html})):
|
||||||
|
self.raise_geo_restricted(error_msg, countries=self._GEO_COUNTRIES)
|
||||||
|
|
||||||
|
webpage_url = traverse_obj(data, ('data', 'template', 'share_url', {url_or_none}))
|
||||||
|
webpage = self._download_webpage(webpage_url, item_id)
|
||||||
|
common = {
|
||||||
|
'thumbnail': self._html_search_meta(['og:image', 'twitter:image'], webpage, default=None),
|
||||||
|
**traverse_obj(media, {
|
||||||
|
'id': ('id', {str_or_none}),
|
||||||
|
'title': (('episodeTitle', 'title'), {clean_html}, filter, any),
|
||||||
|
'channel_id': ('channelId', {str_or_none}),
|
||||||
|
'description': ('anons', {clean_html}, filter),
|
||||||
|
'season': ('season', {clean_html}, filter),
|
||||||
|
'series': (('brand_title', 'brandTitle'), {clean_html}, filter, any),
|
||||||
|
'series_id': ('brand_id', {str_or_none}),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if typ == 'audio':
|
||||||
|
bookmark = self._search_json(
|
||||||
|
r'class="bookmark"[^>]+value\s*=\s*"', webpage,
|
||||||
|
'bookmark', item_id, default={}, transform_source=unescapeHTML)
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
'vcodec': 'none',
|
||||||
|
**common,
|
||||||
|
**traverse_obj(media, {
|
||||||
|
'ext': ('audio_url', {determine_ext(default_ext='mp3')}),
|
||||||
|
'duration': ('duration', {int_or_none}),
|
||||||
|
'url': ('audio_url', {url_or_none}),
|
||||||
|
}),
|
||||||
|
**traverse_obj(bookmark, {
|
||||||
|
'title': ('subtitle', {clean_html}),
|
||||||
|
'timestamp': ('published', {parse_iso8601}),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
elif typ == 'audio-live':
|
||||||
|
metadata = {
|
||||||
|
'ext': 'mp3',
|
||||||
|
'url': traverse_obj(media, ('source', 'auto', {url_or_none})),
|
||||||
|
'vcodec': 'none',
|
||||||
|
**common,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
formats, subtitles = [], {}
|
||||||
|
for m3u8_url in traverse_obj(media, (
|
||||||
|
'sources', 'm3u8', {dict.values}, ..., {url_or_none},
|
||||||
|
)):
|
||||||
|
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
||||||
|
m3u8_url, item_id, 'mp4', m3u8_id='hls', fatal=False)
|
||||||
|
formats.extend(fmts)
|
||||||
|
self._merge_subtitles(subs, target=subtitles)
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
**self._search_json_ld(webpage, item_id),
|
||||||
|
**common,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'age_limit': traverse_obj(data, ('data', 'age_restrictions', {int_or_none})),
|
||||||
|
'is_live': typ in ('audio-live', 'live'),
|
||||||
|
'tags': traverse_obj(webpage, (
|
||||||
|
{find_elements(cls='tags-list__link')}, ..., {clean_html}, filter, all, filter)),
|
||||||
|
'webpage_url': webpage_url,
|
||||||
|
**metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SmotrimIE(SmotrimBaseIE):
|
||||||
|
IE_NAME = 'smotrim'
|
||||||
|
_VALID_URL = r'(?:https?:)?//(?:(?:player|www)\.)?smotrim\.ru(?:/iframe)?/video(?:/id)?/(?P<id>\d+)'
|
||||||
|
_EMBED_REGEX = [fr'<iframe\b[^>]+\bsrc=["\'](?P<url>{_VALID_URL})']
|
||||||
|
_TESTS = [{
|
||||||
'url': 'https://smotrim.ru/video/1539617',
|
'url': 'https://smotrim.ru/video/1539617',
|
||||||
'md5': 'b1923a533c8cab09679789d720d0b1c5',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1539617',
|
'id': '1539617',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Полиглот. Китайский с нуля за 16 часов! Урок №16',
|
'title': 'Урок №16',
|
||||||
'description': '',
|
'duration': 2631,
|
||||||
|
'series': 'Полиглот. Китайский с нуля за 16 часов!',
|
||||||
|
'series_id': '60562',
|
||||||
|
'tags': 'mincount:6',
|
||||||
|
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
|
||||||
|
'timestamp': 1466771100,
|
||||||
|
'upload_date': '20160624',
|
||||||
|
'view_count': int,
|
||||||
},
|
},
|
||||||
'add_ie': ['RUTV'],
|
}, {
|
||||||
}, { # article (geo-restricted? plays fine from the US and JP)
|
'url': 'https://player.smotrim.ru/iframe/video/id/2988590',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2988590',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Трейлер',
|
||||||
|
'age_limit': 16,
|
||||||
|
'description': 'md5:6af7e68ecf4ed7b8ff6720d20c4da47b',
|
||||||
|
'duration': 30,
|
||||||
|
'series': 'Мы в разводе',
|
||||||
|
'series_id': '71624',
|
||||||
|
'tags': 'mincount:5',
|
||||||
|
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
|
||||||
|
'timestamp': 1750670040,
|
||||||
|
'upload_date': '20250623',
|
||||||
|
'view_count': int,
|
||||||
|
'webpage_url': 'https://smotrim.ru/video/2988590',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
_WEBPAGE_TESTS = [{
|
||||||
'url': 'https://smotrim.ru/article/2813445',
|
'url': 'https://smotrim.ru/article/2813445',
|
||||||
'md5': 'e0ac453952afbc6a2742e850b4dc8e77',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2431846',
|
'id': '2431846',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Новости культуры. Съёмки первой программы "Большие и маленькие"',
|
'title': 'Съёмки первой программы "Большие и маленькие"',
|
||||||
'description': 'md5:94a4a22472da4252bf5587a4ee441b99',
|
'description': 'md5:446c9a5d334b995152a813946353f447',
|
||||||
|
'duration': 240,
|
||||||
|
'series': 'Новости культуры',
|
||||||
|
'series_id': '19725',
|
||||||
|
'tags': 'mincount:6',
|
||||||
|
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
|
||||||
|
'timestamp': 1656054443,
|
||||||
|
'upload_date': '20220624',
|
||||||
|
'view_count': int,
|
||||||
|
'webpage_url': 'https://smotrim.ru/video/2431846',
|
||||||
},
|
},
|
||||||
'add_ie': ['RUTV'],
|
}, {
|
||||||
}, { # brand, redirect
|
'url': 'https://www.vesti.ru/article/4642878',
|
||||||
'url': 'https://smotrim.ru/brand/64356',
|
|
||||||
'md5': '740472999ccff81d7f6df79cecd91c18',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2354523',
|
'id': '3007209',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Большие и маленькие. Лучшее. 4-й выпуск',
|
'title': 'Иностранные мессенджеры используют не только мошенники, но и вербовщики',
|
||||||
'description': 'md5:84089e834429008371ea41ea3507b989',
|
'description': 'md5:74ab625a0a89b87b2e0ed98d6391b182',
|
||||||
|
'duration': 265,
|
||||||
|
'series': 'Вести. Дежурная часть',
|
||||||
|
'series_id': '5204',
|
||||||
|
'tags': 'mincount:6',
|
||||||
|
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
|
||||||
|
'timestamp': 1754756280,
|
||||||
|
'upload_date': '20250809',
|
||||||
|
'view_count': int,
|
||||||
|
'webpage_url': 'https://smotrim.ru/video/3007209',
|
||||||
},
|
},
|
||||||
'add_ie': ['RUTV'],
|
|
||||||
}, { # live
|
|
||||||
'url': 'https://smotrim.ru/live/19201',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '19201',
|
|
||||||
'ext': 'mp4',
|
|
||||||
# this looks like a TV channel name
|
|
||||||
'title': 'Россия Культура. Прямой эфир',
|
|
||||||
'description': '',
|
|
||||||
},
|
|
||||||
'add_ie': ['RUTV'],
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id, typ = self._match_valid_url(url).group('id', 'type')
|
video_id = self._match_id(url)
|
||||||
rutv_type = 'video'
|
|
||||||
if typ not in ('video', 'live'):
|
|
||||||
webpage = self._download_webpage(url, video_id, f'Resolving {typ} link')
|
|
||||||
# there are two cases matching regex:
|
|
||||||
# 1. "embedUrl" in JSON LD (/brand/)
|
|
||||||
# 2. "src" attribute from iframe (/article/)
|
|
||||||
video_id = self._search_regex(
|
|
||||||
r'"https://player.smotrim.ru/iframe/video/id/(?P<video_id>\d+)/',
|
|
||||||
webpage, 'video_id', default=None)
|
|
||||||
if not video_id:
|
|
||||||
raise ExtractorError('There are no video in this page.', expected=True)
|
|
||||||
elif typ == 'live':
|
|
||||||
rutv_type = 'live'
|
|
||||||
|
|
||||||
return self.url_result(f'https://player.vgtrk.com/iframe/{rutv_type}/id/{video_id}')
|
return self._extract_from_smotrim_api('video', video_id)
|
||||||
|
|
||||||
|
|
||||||
|
class SmotrimAudioIE(SmotrimBaseIE):
|
||||||
|
IE_NAME = 'smotrim:audio'
|
||||||
|
_VALID_URL = r'https?://(?:(?:player|www)\.)?smotrim\.ru(?:/iframe)?/audio(?:/id)?/(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://smotrim.ru/audio/2573986',
|
||||||
|
'md5': 'e28d94c20da524e242b2d00caef41a8e',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2573986',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': 'Радиоспектакль',
|
||||||
|
'description': 'md5:4bcaaf7d532bc78f76e478fad944e388',
|
||||||
|
'duration': 3072,
|
||||||
|
'series': 'Морис Леблан. Арсен Люпен, джентльмен-грабитель',
|
||||||
|
'series_id': '66461',
|
||||||
|
'tags': 'mincount:7',
|
||||||
|
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
|
||||||
|
'timestamp': 1624884358,
|
||||||
|
'upload_date': '20210628',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://player.smotrim.ru/iframe/audio/id/2860468',
|
||||||
|
'md5': '5a6bc1fa24c7142958be1ad9cfae58a8',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2860468',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': 'Колобок и музыкальная игра "Терем-теремок"',
|
||||||
|
'duration': 1501,
|
||||||
|
'series': 'Веселый колобок',
|
||||||
|
'series_id': '68880',
|
||||||
|
'tags': 'mincount:4',
|
||||||
|
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
|
||||||
|
'timestamp': 1755925800,
|
||||||
|
'upload_date': '20250823',
|
||||||
|
'webpage_url': 'https://smotrim.ru/audio/2860468',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
audio_id = self._match_id(url)
|
||||||
|
|
||||||
|
return self._extract_from_smotrim_api('audio', audio_id)
|
||||||
|
|
||||||
|
|
||||||
|
class SmotrimLiveIE(SmotrimBaseIE):
|
||||||
|
IE_NAME = 'smotrim:live'
|
||||||
|
_VALID_URL = r'''(?x:
|
||||||
|
(?:https?:)?//
|
||||||
|
(?:(?:(?:test)?player|www)\.)?
|
||||||
|
(?:
|
||||||
|
smotrim\.ru|
|
||||||
|
vgtrk\.com
|
||||||
|
)
|
||||||
|
(?:/iframe)?/
|
||||||
|
(?P<type>
|
||||||
|
channel|
|
||||||
|
(?:audio-)?live
|
||||||
|
)
|
||||||
|
(?:/u?id)?/(?P<id>[\da-f-]+)
|
||||||
|
)'''
|
||||||
|
_EMBED_REGEX = [fr'<iframe\b[^>]+\bsrc=["\'](?P<url>{_VALID_URL})']
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://smotrim.ru/channel/76',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1661',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': str,
|
||||||
|
'channel_id': '76',
|
||||||
|
'description': 'Смотрим прямой эфир «Москва 24»',
|
||||||
|
'display_id': '76',
|
||||||
|
'live_status': 'is_live',
|
||||||
|
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
|
||||||
|
'timestamp': int,
|
||||||
|
'upload_date': str,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'Livestream'},
|
||||||
|
}, {
|
||||||
|
# Radio
|
||||||
|
'url': 'https://smotrim.ru/channel/81',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '81',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': str,
|
||||||
|
'channel_id': '81',
|
||||||
|
'live_status': 'is_live',
|
||||||
|
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'Livestream'},
|
||||||
|
}, {
|
||||||
|
# Sometimes geo-restricted to Russia
|
||||||
|
'url': 'https://player.smotrim.ru/iframe/live/uid/381308c7-a066-4c4f-9656-83e2e792a7b4',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '19201',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': str,
|
||||||
|
'channel_id': '4',
|
||||||
|
'description': 'Смотрим прямой эфир «Россия К»',
|
||||||
|
'display_id': '381308c7-a066-4c4f-9656-83e2e792a7b4',
|
||||||
|
'live_status': 'is_live',
|
||||||
|
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
|
||||||
|
'timestamp': int,
|
||||||
|
'upload_date': str,
|
||||||
|
'webpage_url': 'https://smotrim.ru/channel/4',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'Livestream'},
|
||||||
|
}, {
|
||||||
|
'url': 'https://smotrim.ru/live/19201',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://player.smotrim.ru/iframe/audio-live/id/81',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://testplayer.vgtrk.com/iframe/live/id/19201',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
typ, display_id = self._match_valid_url(url).group('type', 'id')
|
||||||
|
|
||||||
|
if typ == 'live' and re.fullmatch(r'[0-9]+', display_id):
|
||||||
|
url = self._request_webpage(url, display_id).url
|
||||||
|
typ = self._match_valid_url(url).group('type')
|
||||||
|
|
||||||
|
if typ == 'channel':
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
src_url = traverse_obj(webpage, ((
|
||||||
|
({find_element(cls='main-player__frame', html=True)}, {extract_attributes}, 'src'),
|
||||||
|
({find_element(cls='audio-play-button', html=True)},
|
||||||
|
{extract_attributes}, 'value', {urllib.parse.unquote}, {json.loads}, 'source'),
|
||||||
|
), any, {self._proto_relative_url}, {url_or_none}, {require('src URL')}))
|
||||||
|
typ, video_id = self._match_valid_url(src_url).group('type', 'id')
|
||||||
|
else:
|
||||||
|
video_id = display_id
|
||||||
|
|
||||||
|
return {
|
||||||
|
'display_id': display_id,
|
||||||
|
**self._extract_from_smotrim_api(typ, video_id),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SmotrimPlaylistIE(SmotrimBaseIE):
|
||||||
|
IE_NAME = 'smotrim:playlist'
|
||||||
|
_PAGE_SIZE = 15
|
||||||
|
_VALID_URL = r'https?://smotrim\.ru/(?P<type>brand|podcast)/(?P<id>\d+)/?(?P<season>[\w-]+)?'
|
||||||
|
_TESTS = [{
|
||||||
|
# Video
|
||||||
|
'url': 'https://smotrim.ru/brand/64356',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '64356',
|
||||||
|
'title': 'Большие и маленькие',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 55,
|
||||||
|
}, {
|
||||||
|
# Video, season
|
||||||
|
'url': 'https://smotrim.ru/brand/65293/3-sezon',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '65293',
|
||||||
|
'title': 'Спасская',
|
||||||
|
'season': '3 сезон',
|
||||||
|
},
|
||||||
|
'playlist_count': 16,
|
||||||
|
}, {
|
||||||
|
# Audio
|
||||||
|
'url': 'https://smotrim.ru/brand/68880',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '68880',
|
||||||
|
'title': 'Веселый колобок',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 156,
|
||||||
|
}, {
|
||||||
|
# Podcast
|
||||||
|
'url': 'https://smotrim.ru/podcast/8021',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '8021',
|
||||||
|
'title': 'Сила звука',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 27,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _fetch_page(self, endpoint, key, playlist_id, page):
|
||||||
|
page += 1
|
||||||
|
items = self._download_json(
|
||||||
|
f'{self._BASE_URL}/api/{endpoint}', playlist_id,
|
||||||
|
f'Downloading page {page}', query={
|
||||||
|
key: playlist_id,
|
||||||
|
'limit': self._PAGE_SIZE,
|
||||||
|
'page': page,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
for link in traverse_obj(items, ('contents', -1, 'list', ..., 'link', {str})):
|
||||||
|
yield self.url_result(urljoin(self._BASE_URL, link))
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
playlist_type, playlist_id, season = self._match_valid_url(url).group('type', 'id', 'season')
|
||||||
|
key = 'rubricId' if playlist_type == 'podcast' else 'brandId'
|
||||||
|
webpage = self._download_webpage(url, playlist_id)
|
||||||
|
playlist_title = self._html_search_meta(['og:title', 'twitter:title'], webpage, default=None)
|
||||||
|
|
||||||
|
if season:
|
||||||
|
return self.playlist_from_matches(traverse_obj(webpage, (
|
||||||
|
{find_elements(tag='a', attr='href', value=r'/video/\d+', html=True, regex=True)},
|
||||||
|
..., {extract_attributes}, 'href', {str},
|
||||||
|
)), playlist_id, playlist_title, season=traverse_obj(webpage, (
|
||||||
|
{find_element(cls='seasons__item seasons__item--selected')}, {clean_html},
|
||||||
|
)), ie=SmotrimIE, getter=urljoin(self._BASE_URL))
|
||||||
|
|
||||||
|
if traverse_obj(webpage, (
|
||||||
|
{find_element(cls='brand-main-item__videos')}, {clean_html}, filter,
|
||||||
|
)):
|
||||||
|
endpoint = 'videos'
|
||||||
|
else:
|
||||||
|
endpoint = 'audios'
|
||||||
|
|
||||||
|
return self.playlist_result(OnDemandPagedList(
|
||||||
|
functools.partial(self._fetch_page, endpoint, key, playlist_id), self._PAGE_SIZE), playlist_id, playlist_title)
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ class SoundcloudIE(SoundcloudBaseIE):
|
|||||||
(?P<title>[\w\d-]+)
|
(?P<title>[\w\d-]+)
|
||||||
(?:/(?P<token>(?!(?:albums|sets|recommended))[^?]+?))?
|
(?:/(?P<token>(?!(?:albums|sets|recommended))[^?]+?))?
|
||||||
(?:[?].*)?$)
|
(?:[?].*)?$)
|
||||||
|(?:api(?:-v2)?\.soundcloud\.com/tracks/(?P<track_id>\d+)
|
|(?:api(?:-v2)?\.soundcloud\.com/tracks/(?:soundcloud%3Atracks%3A)?(?P<track_id>\d+)
|
||||||
(?:/?\?secret_token=(?P<secret_token>[^&]+))?)
|
(?:/?\?secret_token=(?P<secret_token>[^&]+))?)
|
||||||
)
|
)
|
||||||
'''
|
'''
|
||||||
@@ -692,6 +692,9 @@ class SoundcloudIE(SoundcloudBaseIE):
|
|||||||
# Go+ (account with active subscription needed)
|
# Go+ (account with active subscription needed)
|
||||||
'url': 'https://soundcloud.com/taylorswiftofficial/look-what-you-made-me-do',
|
'url': 'https://soundcloud.com/taylorswiftofficial/look-what-you-made-me-do',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://api.soundcloud.com/tracks/soundcloud%3Atracks%3A1083788353',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
import functools
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import (
|
|
||||||
OnDemandPagedList,
|
|
||||||
clean_podcast_url,
|
|
||||||
float_or_none,
|
|
||||||
int_or_none,
|
|
||||||
strip_or_none,
|
|
||||||
traverse_obj,
|
|
||||||
try_get,
|
|
||||||
unified_strdate,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SpotifyBaseIE(InfoExtractor):
|
|
||||||
_WORKING = False
|
|
||||||
_ACCESS_TOKEN = None
|
|
||||||
_OPERATION_HASHES = {
|
|
||||||
'Episode': '8276d4423d709ae9b68ec1b74cc047ba0f7479059a37820be730f125189ac2bf',
|
|
||||||
'MinimalShow': '13ee079672fad3f858ea45a55eb109553b4fb0969ed793185b2e34cbb6ee7cc0',
|
|
||||||
'ShowEpisodes': 'e0e5ce27bd7748d2c59b4d44ba245a8992a05be75d6fabc3b20753fc8857444d',
|
|
||||||
}
|
|
||||||
_VALID_URL_TEMPL = r'https?://open\.spotify\.com/(?:embed-podcast/|embed/|)%s/(?P<id>[^/?&#]+)'
|
|
||||||
_EMBED_REGEX = [r'<iframe[^>]+src="(?P<url>https?://open\.spotify.com/embed/[^"]+)"']
|
|
||||||
|
|
||||||
def _real_initialize(self):
|
|
||||||
self._ACCESS_TOKEN = self._download_json(
|
|
||||||
'https://open.spotify.com/get_access_token', None)['accessToken']
|
|
||||||
|
|
||||||
def _call_api(self, operation, video_id, variables, **kwargs):
|
|
||||||
return self._download_json(
|
|
||||||
'https://api-partner.spotify.com/pathfinder/v1/query', video_id, query={
|
|
||||||
'operationName': 'query' + operation,
|
|
||||||
'variables': json.dumps(variables),
|
|
||||||
'extensions': json.dumps({
|
|
||||||
'persistedQuery': {
|
|
||||||
'sha256Hash': self._OPERATION_HASHES[operation],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}, headers={'authorization': 'Bearer ' + self._ACCESS_TOKEN},
|
|
||||||
**kwargs)['data']
|
|
||||||
|
|
||||||
def _extract_episode(self, episode, series):
|
|
||||||
episode_id = episode['id']
|
|
||||||
title = episode['name'].strip()
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
audio_preview = episode.get('audioPreview') or {}
|
|
||||||
audio_preview_url = audio_preview.get('url')
|
|
||||||
if audio_preview_url:
|
|
||||||
f = {
|
|
||||||
'url': audio_preview_url.replace('://p.scdn.co/mp3-preview/', '://anon-podcast.scdn.co/'),
|
|
||||||
'vcodec': 'none',
|
|
||||||
}
|
|
||||||
audio_preview_format = audio_preview.get('format')
|
|
||||||
if audio_preview_format:
|
|
||||||
f['format_id'] = audio_preview_format
|
|
||||||
mobj = re.match(r'([0-9A-Z]{3})_(?:[A-Z]+_)?(\d+)', audio_preview_format)
|
|
||||||
if mobj:
|
|
||||||
f.update({
|
|
||||||
'abr': int(mobj.group(2)),
|
|
||||||
'ext': mobj.group(1).lower(),
|
|
||||||
})
|
|
||||||
formats.append(f)
|
|
||||||
|
|
||||||
for item in (try_get(episode, lambda x: x['audio']['items']) or []):
|
|
||||||
item_url = item.get('url')
|
|
||||||
if not (item_url and item.get('externallyHosted')):
|
|
||||||
continue
|
|
||||||
formats.append({
|
|
||||||
'url': clean_podcast_url(item_url),
|
|
||||||
'vcodec': 'none',
|
|
||||||
})
|
|
||||||
|
|
||||||
thumbnails = []
|
|
||||||
for source in (try_get(episode, lambda x: x['coverArt']['sources']) or []):
|
|
||||||
source_url = source.get('url')
|
|
||||||
if not source_url:
|
|
||||||
continue
|
|
||||||
thumbnails.append({
|
|
||||||
'url': source_url,
|
|
||||||
'width': int_or_none(source.get('width')),
|
|
||||||
'height': int_or_none(source.get('height')),
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': episode_id,
|
|
||||||
'title': title,
|
|
||||||
'formats': formats,
|
|
||||||
'thumbnails': thumbnails,
|
|
||||||
'description': strip_or_none(episode.get('description')),
|
|
||||||
'duration': float_or_none(try_get(
|
|
||||||
episode, lambda x: x['duration']['totalMilliseconds']), 1000),
|
|
||||||
'release_date': unified_strdate(try_get(
|
|
||||||
episode, lambda x: x['releaseDate']['isoString'])),
|
|
||||||
'series': series,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SpotifyIE(SpotifyBaseIE):
|
|
||||||
IE_NAME = 'spotify'
|
|
||||||
IE_DESC = 'Spotify episodes'
|
|
||||||
_VALID_URL = SpotifyBaseIE._VALID_URL_TEMPL % 'episode'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://open.spotify.com/episode/4Z7GAJ50bgctf6uclHlWKo',
|
|
||||||
'md5': '74010a1e3fa4d9e1ab3aa7ad14e42d3b',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '4Z7GAJ50bgctf6uclHlWKo',
|
|
||||||
'ext': 'mp3',
|
|
||||||
'title': 'From the archive: Why time management is ruining our lives',
|
|
||||||
'description': 'md5:b120d9c4ff4135b42aa9b6d9cde86935',
|
|
||||||
'duration': 2083.605,
|
|
||||||
'release_date': '20201217',
|
|
||||||
'series': "The Guardian's Audio Long Reads",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://open.spotify.com/embed/episode/4TvCsKKs2thXmarHigWvXE?si=7eatS8AbQb6RxqO2raIuWA',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
episode_id = self._match_id(url)
|
|
||||||
episode = self._call_api('Episode', episode_id, {
|
|
||||||
'uri': 'spotify:episode:' + episode_id,
|
|
||||||
})['episode']
|
|
||||||
return self._extract_episode(
|
|
||||||
episode, try_get(episode, lambda x: x['podcast']['name']))
|
|
||||||
|
|
||||||
|
|
||||||
class SpotifyShowIE(SpotifyBaseIE):
|
|
||||||
IE_NAME = 'spotify:show'
|
|
||||||
IE_DESC = 'Spotify shows'
|
|
||||||
_VALID_URL = SpotifyBaseIE._VALID_URL_TEMPL % 'show'
|
|
||||||
_TEST = {
|
|
||||||
'url': 'https://open.spotify.com/show/4PM9Ke6l66IRNpottHKV9M',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '4PM9Ke6l66IRNpottHKV9M',
|
|
||||||
'title': 'The Story from the Guardian',
|
|
||||||
'description': 'The Story podcast is dedicated to our finest audio documentaries, investigations and long form stories',
|
|
||||||
},
|
|
||||||
'playlist_mincount': 36,
|
|
||||||
}
|
|
||||||
_PER_PAGE = 100
|
|
||||||
|
|
||||||
def _fetch_page(self, show_id, page=0):
|
|
||||||
return self._call_api('ShowEpisodes', show_id, {
|
|
||||||
'limit': 100,
|
|
||||||
'offset': page * self._PER_PAGE,
|
|
||||||
'uri': f'spotify:show:{show_id}',
|
|
||||||
}, note=f'Downloading page {page + 1} JSON metadata')['podcast']
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
show_id = self._match_id(url)
|
|
||||||
first_page = self._fetch_page(show_id)
|
|
||||||
|
|
||||||
def _entries(page):
|
|
||||||
podcast = self._fetch_page(show_id, page) if page else first_page
|
|
||||||
yield from map(
|
|
||||||
functools.partial(self._extract_episode, series=podcast.get('name')),
|
|
||||||
traverse_obj(podcast, ('episodes', 'items', ..., 'episode')))
|
|
||||||
|
|
||||||
return self.playlist_result(
|
|
||||||
OnDemandPagedList(_entries, self._PER_PAGE),
|
|
||||||
show_id, first_page.get('name'), first_page.get('description'))
|
|
||||||
@@ -12,7 +12,7 @@ from ..utils.traversal import traverse_obj
|
|||||||
|
|
||||||
|
|
||||||
class SubstackIE(InfoExtractor):
|
class SubstackIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?P<username>[\w-]+)\.substack\.com/p/(?P<id>[\w-]+)'
|
_VALID_URL = r'https?://[\w-]+\.substack\.com/p/(?P<id>[\w-]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://haleynahman.substack.com/p/i-made-a-vlog?s=r',
|
'url': 'https://haleynahman.substack.com/p/i-made-a-vlog?s=r',
|
||||||
'md5': 'f27e4fc6252001d48d479f45e65cdfd5',
|
'md5': 'f27e4fc6252001d48d479f45e65cdfd5',
|
||||||
@@ -116,7 +116,7 @@ class SubstackIE(InfoExtractor):
|
|||||||
return formats, subtitles
|
return formats, subtitles
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id, username = self._match_valid_url(url).group('id', 'username')
|
display_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
|
||||||
webpage_info = self._parse_json(self._search_json(
|
webpage_info = self._parse_json(self._search_json(
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class TelecincoBaseIE(InfoExtractor):
|
|||||||
error_code = traverse_obj(
|
error_code = traverse_obj(
|
||||||
self._webpage_read_content(error.cause.response, caronte['cerbero'], video_id, fatal=False),
|
self._webpage_read_content(error.cause.response, caronte['cerbero'], video_id, fatal=False),
|
||||||
({json.loads}, 'code', {int}))
|
({json.loads}, 'code', {int}))
|
||||||
if error_code in (4038, 40313):
|
if error_code in (4036, 4038, 40313):
|
||||||
self.raise_geo_restricted(countries=['ES'])
|
self.raise_geo_restricted(countries=['ES'])
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ class TelecincoIE(TelecincoBaseIE):
|
|||||||
|
|
||||||
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)
|
||||||
article = self._search_json(
|
article = self._search_json(
|
||||||
r'window\.\$REACTBASE_STATE\.article(?:_multisite)?\s*=',
|
r'window\.\$REACTBASE_STATE\.article(?:_multisite)?\s*=',
|
||||||
webpage, 'article', display_id)['article']
|
webpage, 'article', display_id)['article']
|
||||||
|
|||||||
+163
-19
@@ -1,8 +1,23 @@
|
|||||||
|
import base64
|
||||||
|
import datetime as dt
|
||||||
import itertools
|
import itertools
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..networking import HEADRequest
|
from ..networking.exceptions import HTTPError
|
||||||
from ..utils import int_or_none, traverse_obj, url_or_none, urljoin
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
encode_data_uri,
|
||||||
|
filter_dict,
|
||||||
|
int_or_none,
|
||||||
|
jwt_decode_hs256,
|
||||||
|
url_or_none,
|
||||||
|
urlencode_postdata,
|
||||||
|
urljoin,
|
||||||
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class TenPlayIE(InfoExtractor):
|
class TenPlayIE(InfoExtractor):
|
||||||
@@ -83,7 +98,7 @@ class TenPlayIE(InfoExtractor):
|
|||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
_GEO_BYPASS = False
|
_GEO_BYPASS = False
|
||||||
|
_GEO_COUNTRIES = ['AU']
|
||||||
_AUS_AGES = {
|
_AUS_AGES = {
|
||||||
'G': 0,
|
'G': 0,
|
||||||
'PG': 15,
|
'PG': 15,
|
||||||
@@ -93,26 +108,155 @@ class TenPlayIE(InfoExtractor):
|
|||||||
'R': 18,
|
'R': 18,
|
||||||
'X': 18,
|
'X': 18,
|
||||||
}
|
}
|
||||||
|
_TOKEN_CACHE_KEY = 'token_data'
|
||||||
|
_SEGMENT_BITRATE_RE = r'(?m)-(?:300|150|75|55)0000-(\d+(?:-[\da-f]+)?)\.ts$'
|
||||||
|
|
||||||
|
_refresh_token = None
|
||||||
|
_access_token = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _filter_ads_from_m3u8(m3u8_doc):
|
||||||
|
out = []
|
||||||
|
for line in m3u8_doc.splitlines():
|
||||||
|
if line.startswith('https://redirector.googlevideo.com/'):
|
||||||
|
out.pop()
|
||||||
|
continue
|
||||||
|
out.append(line)
|
||||||
|
|
||||||
|
return '\n'.join(out)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _generate_xnetwork_ten_auth_token():
|
||||||
|
ts = dt.datetime.now(dt.timezone.utc).strftime('%Y%m%d%H%M%S')
|
||||||
|
return base64.b64encode(ts.encode()).decode()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_jwt_expired(token):
|
||||||
|
return jwt_decode_hs256(token)['exp'] - time.time() < 300
|
||||||
|
|
||||||
|
def _refresh_access_token(self):
|
||||||
|
try:
|
||||||
|
refresh_data = self._download_json(
|
||||||
|
'https://10.com.au/api/token/refresh', None, 'Refreshing access token',
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}, data=json.dumps({
|
||||||
|
'accessToken': self._access_token,
|
||||||
|
'refreshToken': self._refresh_token,
|
||||||
|
}).encode())
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, HTTPError) and e.cause.status == 400:
|
||||||
|
self._refresh_token = self._access_token = None
|
||||||
|
self.cache.store(self._NETRC_MACHINE, self._TOKEN_CACHE_KEY, [None, None])
|
||||||
|
self.report_warning('Refresh token has been invalidated; retrying with credentials')
|
||||||
|
self._perform_login(*self._get_login_info())
|
||||||
|
return
|
||||||
|
raise
|
||||||
|
self._access_token = refresh_data['accessToken']
|
||||||
|
self._refresh_token = refresh_data['refreshToken']
|
||||||
|
self.cache.store(self._NETRC_MACHINE, self._TOKEN_CACHE_KEY, [self._refresh_token, self._access_token])
|
||||||
|
|
||||||
|
def _perform_login(self, username, password):
|
||||||
|
if not self._refresh_token:
|
||||||
|
self._refresh_token, self._access_token = self.cache.load(
|
||||||
|
self._NETRC_MACHINE, self._TOKEN_CACHE_KEY, default=[None, None])
|
||||||
|
if self._refresh_token and self._access_token:
|
||||||
|
self.write_debug('Using cached refresh token')
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
auth_data = self._download_json(
|
||||||
|
'https://10.com.au/api/user/auth', None, 'Logging in',
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Network-Ten-Auth': self._generate_xnetwork_ten_auth_token(),
|
||||||
|
'Referer': 'https://10.com.au/',
|
||||||
|
}, data=json.dumps({
|
||||||
|
'email': username,
|
||||||
|
'password': password,
|
||||||
|
}).encode())
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, HTTPError) and e.cause.status == 400:
|
||||||
|
raise ExtractorError('Invalid username/password', expected=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
self._refresh_token = auth_data['jwt']['refreshToken']
|
||||||
|
self._access_token = auth_data['jwt']['accessToken']
|
||||||
|
self.cache.store(self._NETRC_MACHINE, self._TOKEN_CACHE_KEY, [self._refresh_token, self._access_token])
|
||||||
|
|
||||||
|
def _call_playback_api(self, content_id):
|
||||||
|
if self._access_token and self._is_jwt_expired(self._access_token):
|
||||||
|
self._refresh_access_token()
|
||||||
|
for is_retry in (False, True):
|
||||||
|
try:
|
||||||
|
return self._download_json_handle(
|
||||||
|
f'https://10.com.au/api/v1/videos/playback/{content_id}/', content_id,
|
||||||
|
note='Downloading video JSON', query={'platform': 'samsung'},
|
||||||
|
headers=filter_dict({
|
||||||
|
'TP-AcceptFeature': 'v1/fw;v1/drm',
|
||||||
|
'Authorization': f'Bearer {self._access_token}' if self._access_token else None,
|
||||||
|
}))
|
||||||
|
except ExtractorError as e:
|
||||||
|
if not is_retry and isinstance(e.cause, HTTPError) and e.cause.status == 403:
|
||||||
|
if self._access_token:
|
||||||
|
self.to_screen('Access token has expired; refreshing')
|
||||||
|
self._refresh_access_token()
|
||||||
|
continue
|
||||||
|
elif not self._get_login_info()[0]:
|
||||||
|
self.raise_login_required('Login required to access this video', method='password')
|
||||||
|
raise
|
||||||
|
|
||||||
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(
|
try:
|
||||||
'https://10.com.au/api/v1/videos/' + content_id, content_id)
|
data = self._download_json(f'https://10.com.au/api/v1/videos/{content_id}', content_id)
|
||||||
|
except ExtractorError as e:
|
||||||
|
if (
|
||||||
|
isinstance(e.cause, HTTPError) and e.cause.status == 403
|
||||||
|
and 'Error 54113' in e.cause.response.read().decode()
|
||||||
|
):
|
||||||
|
self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
|
||||||
|
raise
|
||||||
|
|
||||||
video_data = self._download_json(
|
video_data, urlh = self._call_playback_api(content_id)
|
||||||
f'https://vod.ten.com.au/api/videos/bcquery?command=find_videos_by_id&video_id={data["altId"]}',
|
content_source_id = video_data['dai']['contentSourceId']
|
||||||
content_id, 'Downloading video JSON')
|
video_id = video_data['dai']['videoId']
|
||||||
m3u8_url = self._request_webpage(
|
auth_token = urlh.get_header('x-dai-auth')
|
||||||
HEADRequest(video_data['items'][0]['HLSURL']),
|
if not auth_token:
|
||||||
content_id, 'Checking stream URL').url
|
raise ExtractorError('Failed to get DAI auth token')
|
||||||
if '10play-not-in-oz' in m3u8_url:
|
|
||||||
self.raise_geo_restricted(countries=['AU'])
|
dai_data = self._download_json(
|
||||||
# Attempt to get a higher quality stream
|
f'https://pubads.g.doubleclick.net/ondemand/hls/content/{content_source_id}/vid/{video_id}/streams',
|
||||||
formats = self._extract_m3u8_formats(
|
content_id, note='Downloading DAI JSON',
|
||||||
m3u8_url.replace(',150,75,55,0000', ',300,150,75,55,0000'),
|
data=urlencode_postdata({'auth-token': auth_token}))
|
||||||
content_id, 'mp4', fatal=False)
|
|
||||||
if not formats:
|
# Ignore subs to avoid ad break cleanup
|
||||||
formats = self._extract_m3u8_formats(m3u8_url, content_id, 'mp4')
|
formats, _ = self._extract_m3u8_formats_and_subtitles(
|
||||||
|
dai_data['stream_manifest'], content_id, 'mp4')
|
||||||
|
|
||||||
|
already_have_1080p = False
|
||||||
|
for fmt in formats:
|
||||||
|
m3u8_doc = self._download_webpage(
|
||||||
|
fmt['url'], content_id, note='Downloading m3u8 information')
|
||||||
|
m3u8_doc = self._filter_ads_from_m3u8(m3u8_doc)
|
||||||
|
fmt['hls_media_playlist_data'] = m3u8_doc
|
||||||
|
if fmt.get('height') == 1080:
|
||||||
|
already_have_1080p = True
|
||||||
|
|
||||||
|
# Attempt format upgrade
|
||||||
|
if not already_have_1080p and m3u8_doc and re.search(self._SEGMENT_BITRATE_RE, m3u8_doc):
|
||||||
|
m3u8_doc = re.sub(self._SEGMENT_BITRATE_RE, r'-5000000-\1.ts', m3u8_doc)
|
||||||
|
m3u8_doc = re.sub(r'-(?:300|150|75|55)0000\.key"', r'-5000000.key"', m3u8_doc)
|
||||||
|
formats.append({
|
||||||
|
'format_id': 'upgrade-attempt-1080p',
|
||||||
|
'url': encode_data_uri(m3u8_doc.encode(), 'application/x-mpegurl'),
|
||||||
|
'hls_media_playlist_data': m3u8_doc,
|
||||||
|
'width': 1920,
|
||||||
|
'height': 1080,
|
||||||
|
'ext': 'mp4',
|
||||||
|
'protocol': 'm3u8_native',
|
||||||
|
'__needs_testing': True,
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': content_id,
|
'id': content_id,
|
||||||
|
|||||||
+20
-14
@@ -220,7 +220,7 @@ class TikTokBaseIE(InfoExtractor):
|
|||||||
def _extract_web_data_and_status(self, url, video_id, fatal=True):
|
def _extract_web_data_and_status(self, url, video_id, fatal=True):
|
||||||
video_data, status = {}, -1
|
video_data, status = {}, -1
|
||||||
|
|
||||||
res = self._download_webpage_handle(url, video_id, fatal=fatal, headers={'User-Agent': 'Mozilla/5.0'})
|
res = self._download_webpage_handle(url, video_id, fatal=fatal, impersonate=True)
|
||||||
if res is False:
|
if res is False:
|
||||||
return video_data, status
|
return video_data, status
|
||||||
|
|
||||||
@@ -1071,12 +1071,15 @@ class TikTokUserIE(TikTokBaseIE):
|
|||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage(
|
||||||
self._UPLOADER_URL_FORMAT % user_name, user_name,
|
self._UPLOADER_URL_FORMAT % user_name, user_name,
|
||||||
'Downloading user webpage', 'Unable to download user webpage',
|
'Downloading user webpage', 'Unable to download user webpage',
|
||||||
fatal=False, headers={'User-Agent': 'Mozilla/5.0'}) or ''
|
fatal=False, impersonate=True) or ''
|
||||||
detail = traverse_obj(
|
detail = traverse_obj(
|
||||||
self._get_universal_data(webpage, user_name), ('webapp.user-detail', {dict})) or {}
|
self._get_universal_data(webpage, user_name), ('webapp.user-detail', {dict})) or {}
|
||||||
if detail.get('statusCode') == 10222:
|
video_count = traverse_obj(detail, ('userInfo', ('stats', 'statsV2'), 'videoCount', {int}, any))
|
||||||
|
if not video_count and detail.get('statusCode') == 10222:
|
||||||
self.raise_login_required(
|
self.raise_login_required(
|
||||||
'This user\'s account is private. Log into an account that has access')
|
'This user\'s account is private. Log into an account that has access')
|
||||||
|
elif video_count == 0:
|
||||||
|
raise ExtractorError('This account does not have any videos posted', expected=True)
|
||||||
sec_uid = traverse_obj(detail, ('userInfo', 'user', 'secUid', {str}))
|
sec_uid = traverse_obj(detail, ('userInfo', 'user', 'secUid', {str}))
|
||||||
if sec_uid:
|
if sec_uid:
|
||||||
fail_early = not traverse_obj(detail, ('userInfo', 'itemList', ...))
|
fail_early = not traverse_obj(detail, ('userInfo', 'itemList', ...))
|
||||||
@@ -1518,19 +1521,22 @@ class TikTokLiveIE(TikTokBaseIE):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
uploader, room_id = self._match_valid_url(url).group('uploader', 'id')
|
uploader, room_id = self._match_valid_url(url).group('uploader', 'id')
|
||||||
webpage = self._download_webpage(
|
if not room_id:
|
||||||
url, uploader or room_id, headers={'User-Agent': 'Mozilla/5.0'}, fatal=not room_id)
|
webpage = self._download_webpage(
|
||||||
|
format_field(uploader, None, self._UPLOADER_URL_FORMAT), uploader, impersonate=True)
|
||||||
|
room_id = traverse_obj(
|
||||||
|
self._get_universal_data(webpage, uploader),
|
||||||
|
('webapp.user-detail', 'userInfo', 'user', 'roomId', {str}))
|
||||||
|
|
||||||
if webpage:
|
if not uploader or not room_id:
|
||||||
|
webpage = self._download_webpage(url, uploader or room_id, fatal=not room_id)
|
||||||
data = self._get_sigi_state(webpage, uploader or room_id)
|
data = self._get_sigi_state(webpage, uploader or room_id)
|
||||||
room_id = (
|
room_id = room_id or traverse_obj(data, ((
|
||||||
traverse_obj(data, ((
|
('LiveRoom', 'liveRoomUserInfo', 'user'),
|
||||||
('LiveRoom', 'liveRoomUserInfo', 'user'),
|
('UserModule', 'users', ...)), 'roomId', {str}, any))
|
||||||
('UserModule', 'users', ...)), 'roomId', {str}, any))
|
uploader = uploader or traverse_obj(data, ((
|
||||||
or self._search_regex(r'snssdk\d*://live\?room_id=(\d+)', webpage, 'room ID', default=room_id))
|
('LiveRoom', 'liveRoomUserInfo', 'user'),
|
||||||
uploader = uploader or traverse_obj(
|
('UserModule', 'users', ...)), 'uniqueId', {str}, any))
|
||||||
data, ('LiveRoom', 'liveRoomUserInfo', 'user', 'uniqueId'),
|
|
||||||
('UserModule', 'users', ..., 'uniqueId'), get_all=False, expected_type=str)
|
|
||||||
|
|
||||||
if not room_id:
|
if not room_id:
|
||||||
raise UserNotLive(video_id=uploader)
|
raise UserNotLive(video_id=uploader)
|
||||||
|
|||||||
+265
-174
@@ -1,244 +1,335 @@
|
|||||||
|
import functools
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
OnDemandPagedList,
|
OnDemandPagedList,
|
||||||
determine_ext,
|
UnsupportedError,
|
||||||
|
clean_html,
|
||||||
|
int_or_none,
|
||||||
|
join_nonempty,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
traverse_obj,
|
update_url_query,
|
||||||
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class TuneInBaseIE(InfoExtractor):
|
class TuneInBaseIE(InfoExtractor):
|
||||||
_VALID_URL_BASE = r'https?://(?:www\.)?tunein\.com'
|
def _call_api(self, item_id, endpoint=None, note='Downloading JSON metadata', fatal=False, query=None):
|
||||||
|
return self._download_json(
|
||||||
def _extract_metadata(self, webpage, content_id):
|
join_nonempty('https://api.tunein.com/profiles', item_id, endpoint, delim='/'),
|
||||||
return self._search_json(r'window.INITIAL_STATE=', webpage, 'hydration', content_id, fatal=False)
|
item_id, note=note, fatal=fatal, query=query) or {}
|
||||||
|
|
||||||
def _extract_formats_and_subtitles(self, content_id):
|
def _extract_formats_and_subtitles(self, content_id):
|
||||||
streams = self._download_json(
|
streams = self._download_json(
|
||||||
f'https://opml.radiotime.com/Tune.ashx?render=json&formats=mp3,aac,ogg,flash,hls&id={content_id}',
|
'https://opml.radiotime.com/Tune.ashx', content_id, query={
|
||||||
content_id)['body']
|
'formats': 'mp3,aac,ogg,flash,hls',
|
||||||
|
'id': content_id,
|
||||||
|
'render': 'json',
|
||||||
|
})
|
||||||
|
|
||||||
formats, subtitles = [], {}
|
formats, subtitles = [], {}
|
||||||
for stream in streams:
|
for stream in traverse_obj(streams, ('body', lambda _, v: url_or_none(v['url']))):
|
||||||
if stream.get('media_type') == 'hls':
|
if stream.get('media_type') == 'hls':
|
||||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(stream['url'], content_id, fatal=False)
|
fmts, subs = self._extract_m3u8_formats_and_subtitles(stream['url'], content_id, fatal=False)
|
||||||
formats.extend(fmts)
|
formats.extend(fmts)
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
self._merge_subtitles(subs, target=subtitles)
|
||||||
elif determine_ext(stream['url']) == 'pls':
|
|
||||||
playlist_content = self._download_webpage(stream['url'], content_id)
|
|
||||||
formats.append({
|
|
||||||
'url': self._search_regex(r'File1=(.*)', playlist_content, 'url', fatal=False),
|
|
||||||
'abr': stream.get('bitrate'),
|
|
||||||
'ext': stream.get('media_type'),
|
|
||||||
})
|
|
||||||
else:
|
else:
|
||||||
formats.append({
|
formats.append(traverse_obj(stream, {
|
||||||
'url': stream['url'],
|
'abr': ('bitrate', {int_or_none}),
|
||||||
'abr': stream.get('bitrate'),
|
'ext': ('media_type', {str}),
|
||||||
'ext': stream.get('media_type'),
|
'url': ('url', {self._proto_relative_url}),
|
||||||
})
|
}))
|
||||||
|
|
||||||
return formats, subtitles
|
return formats, subtitles
|
||||||
|
|
||||||
|
|
||||||
class TuneInStationIE(TuneInBaseIE):
|
class TuneInStationIE(TuneInBaseIE):
|
||||||
_VALID_URL = TuneInBaseIE._VALID_URL_BASE + r'(?:/radio/[^?#]+-|/embed/player/)(?P<id>s\d+)'
|
IE_NAME = 'tunein:station'
|
||||||
_EMBED_REGEX = [r'<iframe[^>]+src=["\'](?P<url>(?:https?://)?tunein\.com/embed/player/s\d+)']
|
_VALID_URL = r'https?://tunein\.com/radio/[^/?#]+(?P<id>s\d+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://tunein.com/radio/Jazz24-885-s34682/',
|
'url': 'https://tunein.com/radio/Jazz24-885-s34682/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 's34682',
|
'id': 's34682',
|
||||||
'title': str,
|
|
||||||
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
|
|
||||||
'thumbnail': r're:https?://cdn-profiles\.tunein\.com/.+',
|
|
||||||
'location': 'Seattle-Tacoma, US',
|
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
|
'title': str,
|
||||||
|
'alt_title': 'World Class Jazz',
|
||||||
|
'channel_follower_count': int,
|
||||||
|
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
|
||||||
|
'location': r're:Seattle-Tacoma, (?:US|WA)',
|
||||||
'live_status': 'is_live',
|
'live_status': 'is_live',
|
||||||
|
'thumbnail': r're:https?://.+',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {'skip_download': 'Livestream'},
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://tunein.com/embed/player/s6404/',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://tunein.com/radio/BBC-Radio-1-988-s24939/',
|
'url': 'https://tunein.com/radio/BBC-Radio-1-988-s24939/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 's24939',
|
'id': 's24939',
|
||||||
'title': str,
|
|
||||||
'description': 'md5:ee2c56794844610d045f8caf5ff34d0c',
|
|
||||||
'thumbnail': r're:https?://cdn-profiles\.tunein\.com/.+',
|
|
||||||
'location': 'London, UK',
|
|
||||||
'ext': 'm4a',
|
'ext': 'm4a',
|
||||||
|
'title': str,
|
||||||
|
'alt_title': 'The biggest new pop and all-day vibes',
|
||||||
|
'channel_follower_count': int,
|
||||||
|
'description': 'md5:ee2c56794844610d045f8caf5ff34d0c',
|
||||||
|
'location': 'London, UK',
|
||||||
'live_status': 'is_live',
|
'live_status': 'is_live',
|
||||||
|
'thumbnail': r're:https?://.+',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {'skip_download': 'Livestream'},
|
||||||
'skip_download': True,
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
station_id = self._match_id(url)
|
||||||
|
formats, subtitles = self._extract_formats_and_subtitles(station_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': station_id,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
**traverse_obj(self._call_api(station_id), ('Item', {
|
||||||
|
'title': ('Title', {clean_html}),
|
||||||
|
'alt_title': ('Subtitle', {clean_html}, filter),
|
||||||
|
'channel_follower_count': ('Actions', 'Follow', 'FollowerCount', {int_or_none}),
|
||||||
|
'description': ('Description', {clean_html}, filter),
|
||||||
|
'is_live': ('Actions', 'Play', 'IsLive', {bool}),
|
||||||
|
'location': ('Properties', 'Location', 'DisplayName', {str}),
|
||||||
|
'thumbnail': ('Image', {url_or_none}),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TuneInPodcastIE(TuneInBaseIE):
|
||||||
|
IE_NAME = 'tunein:podcast:program'
|
||||||
|
_PAGE_SIZE = 20
|
||||||
|
_VALID_URL = r'https?://tunein\.com/podcasts(?:/[^/?#]+){1,2}(?P<id>p\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://tunein.com/podcasts/Technology-Podcasts/Artificial-Intelligence-p1153019/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p1153019',
|
||||||
|
'title': 'Lex Fridman Podcast',
|
||||||
},
|
},
|
||||||
|
'playlist_mincount': 200,
|
||||||
|
}, {
|
||||||
|
'url': 'https://tunein.com/podcasts/World-News/BBC-News-p14/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p14',
|
||||||
|
'title': 'BBC News',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 35,
|
||||||
|
}]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def suitable(cls, url):
|
||||||
|
return False if TuneInPodcastEpisodeIE.suitable(url) else super().suitable(url)
|
||||||
|
|
||||||
|
def _fetch_page(self, url, podcast_id, page=0):
|
||||||
|
items = self._call_api(
|
||||||
|
podcast_id, 'contents', f'Downloading page {page + 1}', query={
|
||||||
|
'filter': 't:free',
|
||||||
|
'limit': self._PAGE_SIZE,
|
||||||
|
'offset': page * self._PAGE_SIZE,
|
||||||
|
},
|
||||||
|
)['Items']
|
||||||
|
|
||||||
|
for item in traverse_obj(items, (..., 'GuideId', {str}, filter)):
|
||||||
|
yield self.url_result(update_url_query(url, {'topicId': item[1:]}))
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
podcast_id = self._match_id(url)
|
||||||
|
|
||||||
|
return self.playlist_result(OnDemandPagedList(
|
||||||
|
functools.partial(self._fetch_page, url, podcast_id), self._PAGE_SIZE),
|
||||||
|
podcast_id, traverse_obj(self._call_api(podcast_id), ('Item', 'Title', {str})))
|
||||||
|
|
||||||
|
|
||||||
|
class TuneInPodcastEpisodeIE(TuneInBaseIE):
|
||||||
|
IE_NAME = 'tunein:podcast'
|
||||||
|
_VALID_URL = r'https?://tunein\.com/podcasts(?:/[^/?#]+){1,2}(?P<series_id>p\d+)/?\?(?:[^#]+&)?(?i:topicid)=(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://tunein.com/podcasts/Technology-Podcasts/Artificial-Intelligence-p1153019/?topicId=236404354',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 't236404354',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': '#351 – MrBeast: Future of YouTube, Twitter, TikTok, and Instagram',
|
||||||
|
'alt_title': 'Technology Podcasts >',
|
||||||
|
'cast': 'count:1',
|
||||||
|
'description': 'md5:1029895354ef073ff00f20b82eb6eb71',
|
||||||
|
'display_id': '236404354',
|
||||||
|
'duration': 8330,
|
||||||
|
'thumbnail': r're:https?://.+',
|
||||||
|
'timestamp': 1673458571,
|
||||||
|
'upload_date': '20230111',
|
||||||
|
'series': 'Lex Fridman Podcast',
|
||||||
|
'series_id': 'p1153019',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://tunein.com/podcasts/The-BOB--TOM-Show-Free-Podcast-p20069/?topicId=174556405',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 't174556405',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': 'B&T Extra: Ohhh Yeah, It\'s Sexy Time',
|
||||||
|
'alt_title': 'Westwood One >',
|
||||||
|
'cast': 'count:2',
|
||||||
|
'description': 'md5:6828234f410ab88c85655495c5fcfa88',
|
||||||
|
'display_id': '174556405',
|
||||||
|
'duration': 1203,
|
||||||
|
'series': 'The BOB & TOM Show Free Podcast',
|
||||||
|
'series_id': 'p20069',
|
||||||
|
'thumbnail': r're:https?://.+',
|
||||||
|
'timestamp': 1661799600,
|
||||||
|
'upload_date': '20220829',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
series_id, display_id = self._match_valid_url(url).group('series_id', 'id')
|
||||||
|
episode_id = f't{display_id}'
|
||||||
|
formats, subtitles = self._extract_formats_and_subtitles(episode_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': episode_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
'formats': formats,
|
||||||
|
'series': traverse_obj(self._call_api(series_id), ('Item', 'Title', {clean_html})),
|
||||||
|
'series_id': series_id,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
**traverse_obj(self._call_api(episode_id), ('Item', {
|
||||||
|
'title': ('Title', {clean_html}),
|
||||||
|
'alt_title': ('Subtitle', {clean_html}, filter),
|
||||||
|
'cast': (
|
||||||
|
'Properties', 'ParentProgram', 'Hosts', {clean_html},
|
||||||
|
{lambda x: x.split(';')}, ..., {str.strip}, filter, all, filter),
|
||||||
|
'description': ('Description', {clean_html}, filter),
|
||||||
|
'duration': ('Actions', 'Play', 'Duration', {int_or_none}),
|
||||||
|
'thumbnail': ('Image', {url_or_none}),
|
||||||
|
'timestamp': ('Actions', 'Play', 'PublishTime', {parse_iso8601}),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TuneInEmbedIE(TuneInBaseIE):
|
||||||
|
IE_NAME = 'tunein:embed'
|
||||||
|
_VALID_URL = r'https?://tunein\.com/embed/player/(?P<id>[^/?#]+)'
|
||||||
|
_EMBED_REGEX = [r'<iframe\b[^>]+\bsrc=["\'](?P<url>(?:https?:)?//tunein\.com/embed/player/[^/?#"\']+)']
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://tunein.com/embed/player/s6404/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 's6404',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': str,
|
||||||
|
'alt_title': 'South Africa\'s News and Information Leader',
|
||||||
|
'channel_follower_count': int,
|
||||||
|
'live_status': 'is_live',
|
||||||
|
'location': 'Johannesburg, South Africa',
|
||||||
|
'thumbnail': r're:https?://.+',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'Livestream'},
|
||||||
|
}, {
|
||||||
|
'url': 'https://tunein.com/embed/player/t236404354/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 't236404354',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': '#351 – MrBeast: Future of YouTube, Twitter, TikTok, and Instagram',
|
||||||
|
'alt_title': 'Technology Podcasts >',
|
||||||
|
'cast': 'count:1',
|
||||||
|
'description': 'md5:1029895354ef073ff00f20b82eb6eb71',
|
||||||
|
'display_id': '236404354',
|
||||||
|
'duration': 8330,
|
||||||
|
'series': 'Lex Fridman Podcast',
|
||||||
|
'series_id': 'p1153019',
|
||||||
|
'thumbnail': r're:https?://.+',
|
||||||
|
'timestamp': 1673458571,
|
||||||
|
'upload_date': '20230111',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://tunein.com/embed/player/p191660/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p191660',
|
||||||
|
'title': 'SBS Tamil',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 195,
|
||||||
}]
|
}]
|
||||||
_WEBPAGE_TESTS = [{
|
_WEBPAGE_TESTS = [{
|
||||||
'url': 'https://www.martiniinthemorning.com/',
|
'url': 'https://www.martiniinthemorning.com/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 's55412',
|
'id': 's55412',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'TuneInStation video #s55412',
|
'title': str,
|
||||||
|
'alt_title': 'Now that\'s music!',
|
||||||
|
'channel_follower_count': int,
|
||||||
|
'description': 'md5:41588a3e2cf34b3eafc6c33522fa611a',
|
||||||
|
'live_status': 'is_live',
|
||||||
|
'location': 'US',
|
||||||
|
'thumbnail': r're:https?://.+',
|
||||||
},
|
},
|
||||||
'expected_warnings': ['unable to extract hydration', 'Extractor failed to obtain "title"'],
|
'params': {'skip_download': 'Livestream'},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
station_id = self._match_id(url)
|
embed_id = self._match_id(url)
|
||||||
|
kind = {
|
||||||
|
'p': 'program',
|
||||||
|
's': 'station',
|
||||||
|
't': 'topic',
|
||||||
|
}.get(embed_id[:1])
|
||||||
|
|
||||||
webpage = self._download_webpage(url, station_id)
|
return self.url_result(
|
||||||
metadata = self._extract_metadata(webpage, station_id)
|
f'https://tunein.com/{kind}/?{kind}id={embed_id[1:]}')
|
||||||
|
|
||||||
formats, subtitles = self._extract_formats_and_subtitles(station_id)
|
|
||||||
return {
|
|
||||||
'id': station_id,
|
|
||||||
'title': traverse_obj(metadata, ('profiles', station_id, 'title')),
|
|
||||||
'description': traverse_obj(metadata, ('profiles', station_id, 'description')),
|
|
||||||
'thumbnail': traverse_obj(metadata, ('profiles', station_id, 'image')),
|
|
||||||
'timestamp': parse_iso8601(
|
|
||||||
traverse_obj(metadata, ('profiles', station_id, 'actions', 'play', 'publishTime'))),
|
|
||||||
'location': traverse_obj(
|
|
||||||
metadata, ('profiles', station_id, 'metadata', 'properties', 'location', 'displayName'),
|
|
||||||
('profiles', station_id, 'properties', 'location', 'displayName')),
|
|
||||||
'formats': formats,
|
|
||||||
'subtitles': subtitles,
|
|
||||||
'is_live': traverse_obj(metadata, ('profiles', station_id, 'actions', 'play', 'isLive')),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TuneInPodcastIE(TuneInBaseIE):
|
|
||||||
_VALID_URL = TuneInBaseIE._VALID_URL_BASE + r'/(?:podcasts/[^?#]+-|embed/player/)(?P<id>p\d+)/?(?:#|$)'
|
|
||||||
_EMBED_REGEX = [r'<iframe[^>]+src=["\'](?P<url>(?:https?://)?tunein\.com/embed/player/p\d+)']
|
|
||||||
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://tunein.com/podcasts/Technology-Podcasts/Artificial-Intelligence-p1153019',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'p1153019',
|
|
||||||
'title': 'Lex Fridman Podcast',
|
|
||||||
'description': 'md5:bedc4e5f1c94f7dec6e4317b5654b00d',
|
|
||||||
},
|
|
||||||
'playlist_mincount': 200,
|
|
||||||
}, {
|
|
||||||
'url': 'https://tunein.com/embed/player/p191660/',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://tunein.com/podcasts/World-News/BBC-News-p14/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'p14',
|
|
||||||
'title': 'BBC News',
|
|
||||||
'description': 'md5:30b9622bcc4bd101d4acd6f38f284aed',
|
|
||||||
},
|
|
||||||
'playlist_mincount': 36,
|
|
||||||
}]
|
|
||||||
|
|
||||||
_PAGE_SIZE = 30
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
podcast_id = self._match_id(url)
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, podcast_id, fatal=False)
|
|
||||||
metadata = self._extract_metadata(webpage, podcast_id)
|
|
||||||
|
|
||||||
def page_func(page_num):
|
|
||||||
api_response = self._download_json(
|
|
||||||
f'https://api.tunein.com/profiles/{podcast_id}/contents', podcast_id,
|
|
||||||
note=f'Downloading page {page_num + 1}', query={
|
|
||||||
'filter': 't:free',
|
|
||||||
'offset': page_num * self._PAGE_SIZE,
|
|
||||||
'limit': self._PAGE_SIZE,
|
|
||||||
})
|
|
||||||
|
|
||||||
return [
|
|
||||||
self.url_result(
|
|
||||||
f'https://tunein.com/podcasts/{podcast_id}?topicId={episode["GuideId"][1:]}',
|
|
||||||
TuneInPodcastEpisodeIE, title=episode.get('Title'))
|
|
||||||
for episode in api_response['Items']]
|
|
||||||
|
|
||||||
entries = OnDemandPagedList(page_func, self._PAGE_SIZE)
|
|
||||||
return self.playlist_result(
|
|
||||||
entries, playlist_id=podcast_id, title=traverse_obj(metadata, ('profiles', podcast_id, 'title')),
|
|
||||||
description=traverse_obj(metadata, ('profiles', podcast_id, 'description')))
|
|
||||||
|
|
||||||
|
|
||||||
class TuneInPodcastEpisodeIE(TuneInBaseIE):
|
|
||||||
_VALID_URL = TuneInBaseIE._VALID_URL_BASE + r'/podcasts/(?:[^?&]+-)?(?P<podcast_id>p\d+)/?\?topicId=(?P<id>\w\d+)'
|
|
||||||
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://tunein.com/podcasts/Technology-Podcasts/Artificial-Intelligence-p1153019/?topicId=236404354',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 't236404354',
|
|
||||||
'title': '#351 – MrBeast: Future of YouTube, Twitter, TikTok, and Instagram',
|
|
||||||
'description': 'md5:2784533b98f8ac45c0820b1e4a8d8bb2',
|
|
||||||
'thumbnail': r're:https?://cdn-profiles\.tunein\.com/.+',
|
|
||||||
'timestamp': 1673458571,
|
|
||||||
'upload_date': '20230111',
|
|
||||||
'series_id': 'p1153019',
|
|
||||||
'series': 'Lex Fridman Podcast',
|
|
||||||
'ext': 'mp3',
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
podcast_id, episode_id = self._match_valid_url(url).group('podcast_id', 'id')
|
|
||||||
episode_id = f't{episode_id}'
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, episode_id)
|
|
||||||
metadata = self._extract_metadata(webpage, episode_id)
|
|
||||||
|
|
||||||
formats, subtitles = self._extract_formats_and_subtitles(episode_id)
|
|
||||||
return {
|
|
||||||
'id': episode_id,
|
|
||||||
'title': traverse_obj(metadata, ('profiles', episode_id, 'title')),
|
|
||||||
'description': traverse_obj(metadata, ('profiles', episode_id, 'description')),
|
|
||||||
'thumbnail': traverse_obj(metadata, ('profiles', episode_id, 'image')),
|
|
||||||
'timestamp': parse_iso8601(
|
|
||||||
traverse_obj(metadata, ('profiles', episode_id, 'actions', 'play', 'publishTime'))),
|
|
||||||
'series_id': podcast_id,
|
|
||||||
'series': traverse_obj(metadata, ('profiles', podcast_id, 'title')),
|
|
||||||
'formats': formats,
|
|
||||||
'subtitles': subtitles,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TuneInShortenerIE(InfoExtractor):
|
class TuneInShortenerIE(InfoExtractor):
|
||||||
_WORKING = False
|
|
||||||
IE_NAME = 'tunein:shortener'
|
IE_NAME = 'tunein:shortener'
|
||||||
IE_DESC = False # Do not list
|
IE_DESC = False # Do not list
|
||||||
_VALID_URL = r'https?://tun\.in/(?P<id>[A-Za-z0-9]+)'
|
_VALID_URL = r'https?://tun\.in/(?P<id>[^/?#]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# test redirection
|
|
||||||
'url': 'http://tun.in/ser7s',
|
'url': 'http://tun.in/ser7s',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 's34682',
|
'id': 's34682',
|
||||||
'title': str,
|
'title': str,
|
||||||
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
|
|
||||||
'thumbnail': r're:https?://cdn-profiles\.tunein\.com/.+',
|
|
||||||
'location': 'Seattle-Tacoma, US',
|
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
|
'alt_title': 'World Class Jazz',
|
||||||
|
'channel_follower_count': int,
|
||||||
|
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
|
||||||
|
'location': r're:Seattle-Tacoma, (?:US|WA)',
|
||||||
'live_status': 'is_live',
|
'live_status': 'is_live',
|
||||||
|
'thumbnail': r're:https?://.+',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {'skip_download': 'Livestream'},
|
||||||
'skip_download': True, # live stream
|
}, {
|
||||||
|
'url': 'http://tun.in/tqeeFw',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 't236404354',
|
||||||
|
'title': str,
|
||||||
|
'ext': 'mp3',
|
||||||
|
'alt_title': 'Technology Podcasts >',
|
||||||
|
'cast': 'count:1',
|
||||||
|
'description': 'md5:1029895354ef073ff00f20b82eb6eb71',
|
||||||
|
'display_id': '236404354',
|
||||||
|
'duration': 8330,
|
||||||
|
'series': 'Lex Fridman Podcast',
|
||||||
|
'series_id': 'p1153019',
|
||||||
|
'thumbnail': r're:https?://.+',
|
||||||
|
'timestamp': 1673458571,
|
||||||
|
'upload_date': '20230111',
|
||||||
},
|
},
|
||||||
|
'params': {'skip_download': 'Livestream'},
|
||||||
|
}, {
|
||||||
|
'url': 'http://tun.in/pei6i',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p14',
|
||||||
|
'title': 'BBC News',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 35,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
redirect_id = self._match_id(url)
|
redirect_id = self._match_id(url)
|
||||||
# The server doesn't support HEAD requests
|
# The server doesn't support HEAD requests
|
||||||
urlh = self._request_webpage(
|
urlh = self._request_webpage(url, redirect_id, 'Downloading redirect page')
|
||||||
url, redirect_id, note='Downloading redirect page')
|
# Need to strip port from URL
|
||||||
|
parsed = urllib.parse.urlparse(urlh.url)
|
||||||
url = urlh.url
|
new_url = parsed._replace(netloc=parsed.hostname).geturl()
|
||||||
url_parsed = urllib.parse.urlparse(url)
|
# Prevent infinite loop in case redirect fails
|
||||||
if url_parsed.port == 443:
|
if self.suitable(new_url):
|
||||||
url = url_parsed._replace(netloc=url_parsed.hostname).url
|
raise UnsupportedError(new_url)
|
||||||
|
return self.url_result(new_url)
|
||||||
self.to_screen(f'Following redirect: {url}')
|
|
||||||
return self.url_result(url)
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class TwitchBaseIE(InfoExtractor):
|
|||||||
'CollectionSideBar': '27111f1b382effad0b6def325caef1909c733fe6a4fbabf54f8d491ef2cf2f14',
|
'CollectionSideBar': '27111f1b382effad0b6def325caef1909c733fe6a4fbabf54f8d491ef2cf2f14',
|
||||||
'FilterableVideoTower_Videos': 'a937f1d22e269e39a03b509f65a7490f9fc247d7f83d6ac1421523e3b68042cb',
|
'FilterableVideoTower_Videos': 'a937f1d22e269e39a03b509f65a7490f9fc247d7f83d6ac1421523e3b68042cb',
|
||||||
'ClipsCards__User': 'b73ad2bfaecfd30a9e6c28fada15bd97032c83ec77a0440766a56fe0bd632777',
|
'ClipsCards__User': 'b73ad2bfaecfd30a9e6c28fada15bd97032c83ec77a0440766a56fe0bd632777',
|
||||||
'ShareClipRenderStatus': 'f130048a462a0ac86bb54d653c968c514e9ab9ca94db52368c1179e97b0f16eb',
|
'ShareClipRenderStatus': 'e0a46b287d760c6890a39d1ccd736af5ec9479a267d02c710e9ac33326b651d2',
|
||||||
'ChannelCollectionsContent': '447aec6a0cc1e8d0a8d7732d47eb0762c336a2294fdb009e9c9d854e49d484b9',
|
'ChannelCollectionsContent': '447aec6a0cc1e8d0a8d7732d47eb0762c336a2294fdb009e9c9d854e49d484b9',
|
||||||
'StreamMetadata': 'a647c2a13599e5991e175155f798ca7f1ecddde73f7f341f39009c14dbf59962',
|
'StreamMetadata': 'a647c2a13599e5991e175155f798ca7f1ecddde73f7f341f39009c14dbf59962',
|
||||||
'ComscoreStreamingQuery': 'e1edae8122517d013405f237ffcc124515dc6ded82480a88daef69c83b53ac01',
|
'ComscoreStreamingQuery': 'e1edae8122517d013405f237ffcc124515dc6ded82480a88daef69c83b53ac01',
|
||||||
@@ -514,7 +514,10 @@ class TwitchVodIE(TwitchBaseIE):
|
|||||||
is_live = None
|
is_live = None
|
||||||
if thumbnail:
|
if thumbnail:
|
||||||
if re.findall(r'/404_processing_[^.?#]+\.png', thumbnail):
|
if re.findall(r'/404_processing_[^.?#]+\.png', thumbnail):
|
||||||
is_live, thumbnail = True, None
|
# False positive for is_live if info.get('broadcastType') == 'HIGHLIGHT'
|
||||||
|
# See https://github.com/yt-dlp/yt-dlp/issues/14455
|
||||||
|
is_live = info.get('broadcastType') == 'ARCHIVE'
|
||||||
|
thumbnail = None
|
||||||
else:
|
else:
|
||||||
is_live = False
|
is_live = False
|
||||||
|
|
||||||
@@ -1153,8 +1156,8 @@ class TwitchClipsIE(TwitchBaseIE):
|
|||||||
'channel_id': '25163635',
|
'channel_id': '25163635',
|
||||||
'channel_is_verified': False,
|
'channel_is_verified': False,
|
||||||
'channel_follower_count': int,
|
'channel_follower_count': int,
|
||||||
'uploader': 'EA',
|
'uploader': 'stereotype_',
|
||||||
'uploader_id': '25163635',
|
'uploader_id': '43566419',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.twitch.tv/xqc/clip/CulturedAmazingKuduDatSheffy-TiZ_-ixAGYR3y2Uy',
|
'url': 'https://www.twitch.tv/xqc/clip/CulturedAmazingKuduDatSheffy-TiZ_-ixAGYR3y2Uy',
|
||||||
@@ -1174,8 +1177,8 @@ class TwitchClipsIE(TwitchBaseIE):
|
|||||||
'channel_id': '71092938',
|
'channel_id': '71092938',
|
||||||
'channel_is_verified': True,
|
'channel_is_verified': True,
|
||||||
'channel_follower_count': int,
|
'channel_follower_count': int,
|
||||||
'uploader': 'xQc',
|
'uploader': 'okSTFUdude',
|
||||||
'uploader_id': '71092938',
|
'uploader_id': '744085721',
|
||||||
'categories': ['Just Chatting'],
|
'categories': ['Just Chatting'],
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@@ -343,22 +343,6 @@ class TxxxIE(InfoExtractor):
|
|||||||
'thumbnail': 'https://tn.voyeurhit.com/contents/videos_sources/332000/332875/screenshots/1.jpg',
|
'thumbnail': 'https://tn.voyeurhit.com/contents/videos_sources/332000/332875/screenshots/1.jpg',
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
_WEBPAGE_TESTS = [{
|
|
||||||
'url': 'https://pornzog.com/video/9125519/michelle-malone-dreamgirls-wild-wet-3/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '5119660',
|
|
||||||
'display_id': '5119660',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Michelle Malone - Dreamgirls - Wild Wet 3',
|
|
||||||
'uploader': 'FallenAngel12',
|
|
||||||
'duration': 402,
|
|
||||||
'view_count': int,
|
|
||||||
'like_count': int,
|
|
||||||
'dislike_count': int,
|
|
||||||
'age_limit': 18,
|
|
||||||
'thumbnail': 'https://hctn.nv7s.com/contents/videos_sources/5119000/5119660/screenshots/1.jpg',
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _call_api(self, url, video_id, fatal=False, **kwargs):
|
def _call_api(self, url, video_id, fatal=False, **kwargs):
|
||||||
content = self._download_json(url, video_id, fatal=fatal, **kwargs)
|
content = self._download_json(url, video_id, fatal=fatal, **kwargs)
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ class KnownDRMIE(UnsupportedInfoExtractor):
|
|||||||
r'play\.hbomax\.com',
|
r'play\.hbomax\.com',
|
||||||
r'channel(?:4|5)\.com',
|
r'channel(?:4|5)\.com',
|
||||||
r'peacocktv\.com',
|
r'peacocktv\.com',
|
||||||
r'(?:[\w\.]+\.)?disneyplus\.com',
|
r'(?:[\w.]+\.)?disneyplus\.com',
|
||||||
r'open\.spotify\.com/(?:track|playlist|album|artist)',
|
r'open\.spotify\.com',
|
||||||
r'tvnz\.co\.nz',
|
r'tvnz\.co\.nz',
|
||||||
r'oneplus\.ch',
|
r'oneplus\.ch',
|
||||||
r'artstation\.com/learning/courses',
|
r'artstation\.com/learning/courses',
|
||||||
r'philo\.com',
|
r'philo\.com',
|
||||||
r'(?:[\w\.]+\.)?mech-plus\.com',
|
r'(?:[\w.]+\.)?mech-plus\.com',
|
||||||
r'aha\.video',
|
r'aha\.video',
|
||||||
r'mubi\.com',
|
r'mubi\.com',
|
||||||
r'vootkids\.com',
|
r'vootkids\.com',
|
||||||
@@ -57,6 +57,15 @@ class KnownDRMIE(UnsupportedInfoExtractor):
|
|||||||
r'ctv\.ca',
|
r'ctv\.ca',
|
||||||
r'noovo\.ca',
|
r'noovo\.ca',
|
||||||
r'tsn\.ca',
|
r'tsn\.ca',
|
||||||
|
r'paramountplus\.com',
|
||||||
|
r'(?:m\.)?(?:sony)?crackle\.com',
|
||||||
|
r'cw(?:tv(?:pr)?|seed)\.com',
|
||||||
|
r'6play\.fr',
|
||||||
|
r'rtlplay\.be',
|
||||||
|
r'play\.rtl\.hr',
|
||||||
|
r'rtlmost\.hu',
|
||||||
|
r'plus\.rtl\.de(?!/podcast/)',
|
||||||
|
r'mediasetinfinity\.es',
|
||||||
)
|
)
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
@@ -78,10 +87,7 @@ class KnownDRMIE(UnsupportedInfoExtractor):
|
|||||||
'url': r'https://www.disneyplus.com',
|
'url': r'https://www.disneyplus.com',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://open.spotify.com/artist/',
|
'url': 'https://open.spotify.com',
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://open.spotify.com/track/',
|
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
# https://github.com/yt-dlp/yt-dlp/issues/4122
|
# https://github.com/yt-dlp/yt-dlp/issues/4122
|
||||||
@@ -184,6 +190,42 @@ class KnownDRMIE(UnsupportedInfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://www.tsn.ca/video/relaxed-oilers-look-to-put-emotional-game-2-loss-in-the-rearview%7E3148747',
|
'url': 'https://www.tsn.ca/video/relaxed-oilers-look-to-put-emotional-game-2-loss-in-the-rearview%7E3148747',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.paramountplus.com',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.crackle.com',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://m.sonycrackle.com',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.cwtv.com',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.cwseed.com',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://cwtvpr.com',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.6play.fr',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.rtlplay.be',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://play.rtl.hr',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.rtlmost.hu',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://plus.rtl.de/video-tv/',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.mediasetinfinity.es/',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -222,6 +264,7 @@ class KnownPiracyIE(UnsupportedInfoExtractor):
|
|||||||
r'91porn\.com',
|
r'91porn\.com',
|
||||||
r'einthusan\.(?:tv|com|ca)',
|
r'einthusan\.(?:tv|com|ca)',
|
||||||
r'yourupload\.com',
|
r'yourupload\.com',
|
||||||
|
r'xanimu\.com',
|
||||||
)
|
)
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from .rutv import RUTVIE
|
|
||||||
from ..utils import ExtractorError
|
|
||||||
|
|
||||||
|
|
||||||
class VestiIE(InfoExtractor):
|
|
||||||
_WORKING = False
|
|
||||||
IE_DESC = 'Вести.Ru'
|
|
||||||
_VALID_URL = r'https?://(?:.+?\.)?vesti\.ru/(?P<id>.+)'
|
|
||||||
|
|
||||||
_TESTS = [
|
|
||||||
{
|
|
||||||
'url': 'http://www.vesti.ru/videos?vid=575582&cid=1',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '765035',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Вести.net: биткоины в России не являются законными',
|
|
||||||
'description': 'md5:d4bb3859dc1177b28a94c5014c35a36b',
|
|
||||||
'duration': 302,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'url': 'http://www.vesti.ru/doc.html?id=1349233',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '773865',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Участники митинга штурмуют Донецкую областную администрацию',
|
|
||||||
'description': 'md5:1a160e98b3195379b4c849f2f4958009',
|
|
||||||
'duration': 210,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'url': 'http://www.vesti.ru/only_video.html?vid=576180',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '766048',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'США заморозило, Британию затопило',
|
|
||||||
'description': 'md5:f0ed0695ec05aed27c56a70a58dc4cc1',
|
|
||||||
'duration': 87,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'url': 'http://hitech.vesti.ru/news/view/id/4000',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '766888',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Вести.net: интернет-гиганты начали перетягивание программных "одеял"',
|
|
||||||
'description': 'md5:65ddd47f9830c4f42ed6475f8730c995',
|
|
||||||
'duration': 279,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'url': 'http://sochi2014.vesti.ru/video/index/video_id/766403',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '766403',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'XXII зимние Олимпийские игры. Российские хоккеисты стартовали на Олимпиаде с победы',
|
|
||||||
'description': 'md5:55805dfd35763a890ff50fa9e35e31b3',
|
|
||||||
'duration': 271,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'skip': 'Blocked outside Russia',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'url': 'http://sochi2014.vesti.ru/live/play/live_id/301',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '51499',
|
|
||||||
'ext': 'flv',
|
|
||||||
'title': 'Сочи-2014. Биатлон. Индивидуальная гонка. Мужчины ',
|
|
||||||
'description': 'md5:9e0ed5c9d2fa1efbfdfed90c9a6d179c',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# rtmp download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'skip': 'Translation has finished',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
mobj = self._match_valid_url(url)
|
|
||||||
video_id = mobj.group('id')
|
|
||||||
|
|
||||||
page = self._download_webpage(url, video_id, 'Downloading page')
|
|
||||||
|
|
||||||
mobj = re.search(
|
|
||||||
r'<meta[^>]+?property="og:video"[^>]+?content="http://www\.vesti\.ru/i/flvplayer_videoHost\.swf\?vid=(?P<id>\d+)',
|
|
||||||
page)
|
|
||||||
if mobj:
|
|
||||||
video_id = mobj.group('id')
|
|
||||||
page = self._download_webpage(f'http://www.vesti.ru/only_video.html?vid={video_id}', video_id,
|
|
||||||
'Downloading video page')
|
|
||||||
|
|
||||||
rutv_url = RUTVIE._extract_url(page)
|
|
||||||
if rutv_url:
|
|
||||||
return self.url_result(rutv_url, 'RUTV')
|
|
||||||
|
|
||||||
raise ExtractorError('No video found', expected=True)
|
|
||||||
+60
-47
@@ -58,6 +58,20 @@ class VidyardBaseIE(InfoExtractor):
|
|||||||
|
|
||||||
return subs
|
return subs
|
||||||
|
|
||||||
|
def _get_additional_metadata(self, video_id):
|
||||||
|
additional_metadata = self._download_json(
|
||||||
|
f'https://play.vidyard.com/video/{video_id}', video_id,
|
||||||
|
note='Downloading additional metadata', fatal=False)
|
||||||
|
return traverse_obj(additional_metadata, {
|
||||||
|
'title': ('name', {str}),
|
||||||
|
'duration': ('seconds', {int_or_none}),
|
||||||
|
'thumbnails': ('thumbnailUrl', {'url': {url_or_none}}, all),
|
||||||
|
'chapters': ('videoSections', lambda _, v: float_or_none(v['milliseconds']) is not None, {
|
||||||
|
'title': ('title', {str}),
|
||||||
|
'start_time': ('milliseconds', {float_or_none(scale=1000)}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
def _fetch_video_json(self, video_id):
|
def _fetch_video_json(self, video_id):
|
||||||
return self._download_json(
|
return self._download_json(
|
||||||
f'https://play.vidyard.com/player/{video_id}.json', video_id)['payload']
|
f'https://play.vidyard.com/player/{video_id}.json', video_id)['payload']
|
||||||
@@ -67,6 +81,7 @@ class VidyardBaseIE(InfoExtractor):
|
|||||||
self._merge_subtitles(self._get_direct_subtitles(json_data.get('captions')), target=subtitles)
|
self._merge_subtitles(self._get_direct_subtitles(json_data.get('captions')), target=subtitles)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
**self._get_additional_metadata(json_data['facadeUuid']),
|
||||||
**traverse_obj(json_data, {
|
**traverse_obj(json_data, {
|
||||||
'id': ('facadeUuid', {str}),
|
'id': ('facadeUuid', {str}),
|
||||||
'display_id': ('videoId', {int}, {str_or_none}),
|
'display_id': ('videoId', {int}, {str_or_none}),
|
||||||
@@ -113,6 +128,29 @@ class VidyardIE(VidyardBaseIE):
|
|||||||
'thumbnail': 'https://cdn.vidyard.com/thumbnails/spacer.gif',
|
'thumbnail': 'https://cdn.vidyard.com/thumbnails/spacer.gif',
|
||||||
'duration': 41.186,
|
'duration': 41.186,
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://share.vidyard.com/watch/wL237MtNgZUHo6e8WPiJbF',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'wL237MtNgZUHo6e8WPiJbF',
|
||||||
|
'display_id': '25926870',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Adding & Editing Video Chapters',
|
||||||
|
'thumbnail': 'https://cdn.vidyard.com/thumbnails/25926870/bvSEZS3dGY7DByQ_bzB57avIZ_hsvhr4_small.jpg',
|
||||||
|
'duration': 135.46,
|
||||||
|
'chapters': [{
|
||||||
|
'title': 'Adding new chapters',
|
||||||
|
'start_time': 0,
|
||||||
|
}, {
|
||||||
|
'title': 'Previewing your video',
|
||||||
|
'start_time': 74,
|
||||||
|
}, {
|
||||||
|
'title': 'Editing your chapters',
|
||||||
|
'start_time': 91,
|
||||||
|
}, {
|
||||||
|
'title': 'Share a link to a specific chapter',
|
||||||
|
'start_time': 105,
|
||||||
|
}],
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://embed.vidyard.com/share/oTDMPlUv--51Th455G5u7Q',
|
'url': 'https://embed.vidyard.com/share/oTDMPlUv--51Th455G5u7Q',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -132,8 +170,8 @@ class VidyardIE(VidyardBaseIE):
|
|||||||
'id': 'SyStyHtYujcBHe5PkZc5DL',
|
'id': 'SyStyHtYujcBHe5PkZc5DL',
|
||||||
'display_id': '41974005',
|
'display_id': '41974005',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Prepare the Frame and Track for Palm Beach Polysatin Shutters With BiFold Track',
|
'title': 'Install Palm Beach Shutters with a Bi-Fold Track System (Video 1 of 6)',
|
||||||
'description': r're:In this video, you will learn how to prepare the frame.+',
|
'description': r're:In this video, you will learn the first step.+',
|
||||||
'thumbnail': 'https://cdn.vidyard.com/thumbnails/41974005/IJw7oCaJcF1h7WWu3OVZ8A_small.png',
|
'thumbnail': 'https://cdn.vidyard.com/thumbnails/41974005/IJw7oCaJcF1h7WWu3OVZ8A_small.png',
|
||||||
'duration': 258.666,
|
'duration': 258.666,
|
||||||
},
|
},
|
||||||
@@ -147,42 +185,42 @@ class VidyardIE(VidyardBaseIE):
|
|||||||
'id': 'SyStyHtYujcBHe5PkZc5DL',
|
'id': 'SyStyHtYujcBHe5PkZc5DL',
|
||||||
'display_id': '41974005',
|
'display_id': '41974005',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Prepare the Frame and Track for Palm Beach Polysatin Shutters With BiFold Track',
|
'title': 'Install Palm Beach Shutters with a Bi-Fold Track System (Video 1 of 6)',
|
||||||
'thumbnail': 'https://cdn.vidyard.com/thumbnails/41974005/IJw7oCaJcF1h7WWu3OVZ8A_small.png',
|
'thumbnail': 'https://cdn.vidyard.com/thumbnails/41974005/IJw7oCaJcF1h7WWu3OVZ8A_small.png',
|
||||||
'duration': 258.666,
|
'duration': 258.666,
|
||||||
}, {
|
}, {
|
||||||
'id': '1Fw4B84jZTXLXWqkE71RiM',
|
'id': '1Fw4B84jZTXLXWqkE71RiM',
|
||||||
'display_id': '5861113',
|
'display_id': '5861113',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Palm Beach - Bi-Fold Track System "Frame Installation"',
|
'title': 'Install Palm Beach Shutters with a Bi-Fold Track System (Video 2 of 6)',
|
||||||
'thumbnail': 'https://cdn.vidyard.com/thumbnails/5861113/29CJ54s5g1_aP38zkKLHew_small.jpg',
|
'thumbnail': 'https://cdn.vidyard.com/thumbnails/5861113/29CJ54s5g1_aP38zkKLHew_small.jpg',
|
||||||
'duration': 167.858,
|
'duration': 167.858,
|
||||||
}, {
|
}, {
|
||||||
'id': 'DqP3wBvLXSpxrcqpT5kEeo',
|
'id': 'DqP3wBvLXSpxrcqpT5kEeo',
|
||||||
'display_id': '41976334',
|
'display_id': '41976334',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Install the Track for Palm Beach Polysatin Shutters With BiFold Track',
|
'title': 'Install Palm Beach Shutters with a Bi-Fold Track System (Video 3 of 6)',
|
||||||
'thumbnail': 'https://cdn.vidyard.com/thumbnails/5861090/RwG2VaTylUa6KhSTED1r1Q_small.png',
|
'thumbnail': 'https://cdn.vidyard.com/thumbnails/5861090/RwG2VaTylUa6KhSTED1r1Q_small.png',
|
||||||
'duration': 94.229,
|
'duration': 94.229,
|
||||||
}, {
|
}, {
|
||||||
'id': 'opfybfxpzQArxqtQYB6oBU',
|
'id': 'opfybfxpzQArxqtQYB6oBU',
|
||||||
'display_id': '41976364',
|
'display_id': '41976364',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Install the Panel for Palm Beach Polysatin Shutters With BiFold Track',
|
'title': 'Install Palm Beach Shutters with a Bi-Fold Track System (Video 4 of 6)',
|
||||||
'thumbnail': 'https://cdn.vidyard.com/thumbnails/5860926/JIOaJR08dM4QgXi_iQ2zGA_small.png',
|
'thumbnail': 'https://cdn.vidyard.com/thumbnails/5860926/JIOaJR08dM4QgXi_iQ2zGA_small.png',
|
||||||
'duration': 191.467,
|
'duration': 191.467,
|
||||||
}, {
|
}, {
|
||||||
'id': 'rWrXvkbTNNaNqD6189HJya',
|
'id': 'rWrXvkbTNNaNqD6189HJya',
|
||||||
'display_id': '41976382',
|
'display_id': '41976382',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Adjust the Panels for Palm Beach Polysatin Shutters With BiFold Track',
|
'title': 'Install Palm Beach Shutters with a Bi-Fold Track System (Video 5 of 6)',
|
||||||
'thumbnail': 'https://cdn.vidyard.com/thumbnails/5860687/CwHxBv4UudAhOh43FVB4tw_small.png',
|
'thumbnail': 'https://cdn.vidyard.com/thumbnails/5860687/CwHxBv4UudAhOh43FVB4tw_small.png',
|
||||||
'duration': 138.155,
|
'duration': 138.155,
|
||||||
}, {
|
}, {
|
||||||
'id': 'eYPTB521MZ9TPEArSethQ5',
|
'id': 'eYPTB521MZ9TPEArSethQ5',
|
||||||
'display_id': '41976409',
|
'display_id': '41976409',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Assemble and Install the Valance for Palm Beach Polysatin Shutters With BiFold Track',
|
'title': 'Install Palm Beach Shutters with a Bi-Fold Track System (Video 6 of 6)',
|
||||||
'thumbnail': 'https://cdn.vidyard.com/thumbnails/5861425/0y68qlMU4O5VKU7bJ8i_AA_small.png',
|
'thumbnail': 'https://cdn.vidyard.com/thumbnails/5861425/0y68qlMU4O5VKU7bJ8i_AA_small.png',
|
||||||
'duration': 148.224,
|
'duration': 148.224,
|
||||||
}],
|
}],
|
||||||
@@ -191,6 +229,7 @@ class VidyardIE(VidyardBaseIE):
|
|||||||
}, {
|
}, {
|
||||||
# Non hubs.vidyard.com playlist
|
# Non hubs.vidyard.com playlist
|
||||||
'url': 'https://salesforce.vidyard.com/watch/d4vqPjs7Q5EzVEis5QT3jd',
|
'url': 'https://salesforce.vidyard.com/watch/d4vqPjs7Q5EzVEis5QT3jd',
|
||||||
|
'skip': 'URL now 404s. Alternative non hubs.vidyard.com playlist not yet available',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'd4vqPjs7Q5EzVEis5QT3jd',
|
'id': 'd4vqPjs7Q5EzVEis5QT3jd',
|
||||||
'title': 'How To: Service Cloud: Import External Content in Lightning Knowledge',
|
'title': 'How To: Service Cloud: Import External Content in Lightning Knowledge',
|
||||||
@@ -300,6 +339,7 @@ class VidyardIE(VidyardBaseIE):
|
|||||||
}, {
|
}, {
|
||||||
# <script ... id="vidyard_embed_code_DXx2sW4WaLA6hTdGFz7ja8" src="//play.vidyard.com/DXx2sW4WaLA6hTdGFz7ja8.js?
|
# <script ... id="vidyard_embed_code_DXx2sW4WaLA6hTdGFz7ja8" src="//play.vidyard.com/DXx2sW4WaLA6hTdGFz7ja8.js?
|
||||||
'url': 'http://videos.vivint.com/watch/DXx2sW4WaLA6hTdGFz7ja8',
|
'url': 'http://videos.vivint.com/watch/DXx2sW4WaLA6hTdGFz7ja8',
|
||||||
|
'skip': 'URL certificate expired 2025-09-10. Alternative script embed test case not yet available',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'DXx2sW4WaLA6hTdGFz7ja8',
|
'id': 'DXx2sW4WaLA6hTdGFz7ja8',
|
||||||
'display_id': '2746529',
|
'display_id': '2746529',
|
||||||
@@ -317,11 +357,12 @@ class VidyardIE(VidyardBaseIE):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Lesson 1 - Opening an MT4 Account',
|
'title': 'Lesson 1 - Opening an MT4 Account',
|
||||||
'description': 'Never heard of MetaTrader4? Here\'s the 411 on the popular trading platform!',
|
'description': 'Never heard of MetaTrader4? Here\'s the 411 on the popular trading platform!',
|
||||||
'duration': 168,
|
'duration': 168.16,
|
||||||
'thumbnail': 'https://cdn.vidyard.com/thumbnails/20291/IM-G2WXQR9VBLl2Cmzvftg_small.jpg',
|
'thumbnail': 'https://cdn.vidyard.com/thumbnails/20291/IM-G2WXQR9VBLl2Cmzvftg_small.jpg',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# <iframe ... src="//play.vidyard.com/d61w8EQoZv1LDuPxDkQP2Q/type/background?preview=1"
|
# <iframe ... src="//play.vidyard.com/d61w8EQoZv1LDuPxDkQP2Q/type/background?preview=1"
|
||||||
|
'skip': 'URL changed embed method to \'class="vidyard-player-embed"\'. An alternative iframe embed test case is not yet available',
|
||||||
'url': 'https://www.avaya.com/en/',
|
'url': 'https://www.avaya.com/en/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
# These values come from the generic extractor and don't matter
|
# These values come from the generic extractor and don't matter
|
||||||
@@ -354,46 +395,18 @@ class VidyardIE(VidyardBaseIE):
|
|||||||
}],
|
}],
|
||||||
'playlist_count': 2,
|
'playlist_count': 2,
|
||||||
}, {
|
}, {
|
||||||
# <div class="vidyard-player-embed" data-uuid="vpCWTVHw3qrciLtVY94YkS"
|
# <div class="vidyard-player-embed" data-uuid="pMk8eNCYzukzJaEPoo1Hgn"
|
||||||
'url': 'https://www.gogoair.com/',
|
# URL previously used iframe embeds and was used for that test case
|
||||||
|
'url': 'https://www.avaya.com/en/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
# These values come from the generic extractor and don't matter
|
'id': 'pMk8eNCYzukzJaEPoo1Hgn',
|
||||||
'id': str,
|
'display_id': '47074153',
|
||||||
'title': str,
|
'ext': 'mp4',
|
||||||
'description': str,
|
'title': 'Avaya Infinity Helps Redefine the Contact Center as Your Connection Center',
|
||||||
'age_limit': 0,
|
'description': r're:Our mission is to help you turn single engagements.+',
|
||||||
|
'duration': 81.55,
|
||||||
|
'thumbnail': 'https://cdn.vidyard.com/thumbnails/47074153/MZOLKhXdbiUWwp2ROnT5HaXL0oau6JtR_small.jpg',
|
||||||
},
|
},
|
||||||
'playlist': [{
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'vpCWTVHw3qrciLtVY94YkS',
|
|
||||||
'display_id': '40780699',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Upgrade to AVANCE 100% worth it - Jason Talley, Owner and Pilot, Testimonial',
|
|
||||||
'description': 'md5:f609824839439a51990cef55ffc472aa',
|
|
||||||
'duration': 70.737,
|
|
||||||
'thumbnail': 'https://cdn.vidyard.com/thumbnails/40780699/KzjfYZz5MZl2gHF_e-4i2c6ib1cLDweQ_small.jpg',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'xAmV9AsLbnitCw35paLBD8',
|
|
||||||
'display_id': '31130867',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Brad Keselowski goes faster with Gogo AVANCE inflight Wi-Fi',
|
|
||||||
'duration': 132.565,
|
|
||||||
'thumbnail': 'https://cdn.vidyard.com/thumbnails/31130867/HknyDtLdm2Eih9JZ4A5XLjhfBX_6HRw5_small.jpg',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'RkkrFRNxfP79nwCQavecpF',
|
|
||||||
'display_id': '39009815',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Live Demo of Gogo Galileo',
|
|
||||||
'description': 'md5:e2df497236f4e12c3fef8b392b5f23e0',
|
|
||||||
'duration': 112.128,
|
|
||||||
'thumbnail': 'https://cdn.vidyard.com/thumbnails/38144873/CWLlxfUbJ4Gh0ThbUum89IsEM4yupzMb_small.jpg',
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
'playlist_count': 3,
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class VimeoBaseInfoExtractor(InfoExtractor):
|
|||||||
'Referer': self._LOGIN_URL,
|
'Referer': self._LOGIN_URL,
|
||||||
})
|
})
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status in (405, 418):
|
if isinstance(e.cause, HTTPError) and e.cause.status in (404, 405, 418):
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
'Unable to log in: bad username or password',
|
'Unable to log in: bad username or password',
|
||||||
expected=True)
|
expected=True)
|
||||||
|
|||||||
+11
-4
@@ -96,12 +96,12 @@ class VKIE(VKBaseIE):
|
|||||||
https?://
|
https?://
|
||||||
(?:
|
(?:
|
||||||
(?:
|
(?:
|
||||||
(?:(?:m|new)\.)?vk(?:(?:video)?\.ru|\.com)/video_|
|
(?:(?:m|new|vksport)\.)?vk(?:(?:video)?\.ru|\.com)/video_|
|
||||||
(?:www\.)?daxab\.com/
|
(?:www\.)?daxab\.com/
|
||||||
)
|
)
|
||||||
ext\.php\?(?P<embed_query>.*?\boid=(?P<oid>-?\d+).*?\bid=(?P<id>\d+).*)|
|
ext\.php\?(?P<embed_query>.*?\boid=(?P<oid>-?\d+).*?\bid=(?P<id>\d+).*)|
|
||||||
(?:
|
(?:
|
||||||
(?:(?:m|new)\.)?vk(?:(?:video)?\.ru|\.com)/(?:.+?\?.*?z=)?(?:video|clip)|
|
(?:(?:m|new|vksport)\.)?vk(?:(?:video)?\.ru|\.com)/(?:.+?\?.*?z=)?(?:video|clip)|
|
||||||
(?:www\.)?daxab\.com/embed/
|
(?:www\.)?daxab\.com/embed/
|
||||||
)
|
)
|
||||||
(?P<videoid>-?\d+_\d+)(?:.*\blist=(?P<list_id>([\da-f]+)|(ln-[\da-zA-Z]+)))?
|
(?P<videoid>-?\d+_\d+)(?:.*\blist=(?P<list_id>([\da-f]+)|(ln-[\da-zA-Z]+)))?
|
||||||
@@ -359,6 +359,10 @@ class VKIE(VKBaseIE):
|
|||||||
'url': 'https://vk.ru/video-220754053_456242564',
|
'url': 'https://vk.ru/video-220754053_456242564',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://vksport.vkvideo.ru/video-124096712_456240773',
|
||||||
|
'only_matching': True,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -568,7 +572,7 @@ class VKUserVideosIE(VKBaseIE):
|
|||||||
IE_DESC = "VK - User's Videos"
|
IE_DESC = "VK - User's Videos"
|
||||||
_BASE_URL_RE = r'https?://(?:(?:m|new)\.)?vk(?:video\.ru|\.com/video)'
|
_BASE_URL_RE = r'https?://(?:(?:m|new)\.)?vk(?:video\.ru|\.com/video)'
|
||||||
_VALID_URL = [
|
_VALID_URL = [
|
||||||
rf'{_BASE_URL_RE}/playlist/(?P<id>-?\d+_\d+)',
|
rf'{_BASE_URL_RE}/playlist/(?P<id>-?\d+_-?\d+)',
|
||||||
rf'{_BASE_URL_RE}/(?P<id>@[^/?#]+)(?:/all)?/?(?!\?.*\bz=video)(?:[?#]|$)',
|
rf'{_BASE_URL_RE}/(?P<id>@[^/?#]+)(?:/all)?/?(?!\?.*\bz=video)(?:[?#]|$)',
|
||||||
]
|
]
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
@@ -598,6 +602,9 @@ class VKUserVideosIE(VKBaseIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://vk.com/video/playlist/-174476437_2',
|
'url': 'https://vk.com/video/playlist/-174476437_2',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://vkvideo.ru/playlist/-51890028_-2',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
_VIDEO = collections.namedtuple('Video', ['owner_id', 'id'])
|
_VIDEO = collections.namedtuple('Video', ['owner_id', 'id'])
|
||||||
|
|
||||||
@@ -723,7 +730,7 @@ class VKWallPostIE(VKBaseIE):
|
|||||||
def _unmask_url(self, mask_url, vk_id):
|
def _unmask_url(self, mask_url, vk_id):
|
||||||
if 'audio_api_unavailable' in mask_url:
|
if 'audio_api_unavailable' in mask_url:
|
||||||
extra = mask_url.split('?extra=')[1].split('#')
|
extra = mask_url.split('?extra=')[1].split('#')
|
||||||
func, base = self._decode(extra[1]).split(chr(11))
|
_, base = self._decode(extra[1]).split(chr(11))
|
||||||
mask_url = list(self._decode(extra[0]))
|
mask_url = list(self._decode(extra[0]))
|
||||||
url_len = len(mask_url)
|
url_len = len(mask_url)
|
||||||
indexes = [None] * url_len
|
indexes = [None] * url_len
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user