mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2025-03-09 12:50:23 -05:00
Merge branch 'master' into misc/globals-and-plugins-revive
This commit is contained in:
commit
4a544353af
47 changed files with 948 additions and 480 deletions
24
.github/ISSUE_TEMPLATE/1_broken_site.yml
vendored
24
.github/ISSUE_TEMPLATE/1_broken_site.yml
vendored
|
@ -2,13 +2,11 @@ name: Broken site support
|
||||||
description: Report issue with yt-dlp on a supported site
|
description: Report issue with yt-dlp on a supported site
|
||||||
labels: [triage, site-bug]
|
labels: [triage, site-bug]
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
label: DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE
|
value: |
|
||||||
description: Fill all fields even if you think it is irrelevant for the issue
|
> [!IMPORTANT]
|
||||||
options:
|
> Not providing the required (*) information or removing the template will result in your issue being closed and ignored.
|
||||||
- label: I understand that I will be **blocked** if I *intentionally* remove or skip any mandatory\* field
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -24,9 +22,7 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
|
- label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
|
||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) 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
|
|
||||||
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
|
|
||||||
required: true
|
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
|
||||||
|
@ -47,6 +43,8 @@ body:
|
||||||
id: verbose
|
id: verbose
|
||||||
attributes:
|
attributes:
|
||||||
label: Provide verbose output that clearly demonstrates the problem
|
label: Provide verbose output that clearly demonstrates the problem
|
||||||
|
description: |
|
||||||
|
This is mandatory unless absolutely impossible to provide. If you are unable to provide the output, please explain why.
|
||||||
options:
|
options:
|
||||||
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
||||||
required: true
|
required: true
|
||||||
|
@ -78,11 +76,3 @@ body:
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
> [!CAUTION]
|
|
||||||
> ### GitHub is experiencing a high volume of malicious spam comments.
|
|
||||||
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
|
||||||
>
|
|
||||||
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
|
||||||
|
|
|
@ -2,13 +2,11 @@ name: Site support request
|
||||||
description: Request support for a new site
|
description: Request support for a new site
|
||||||
labels: [triage, site-request]
|
labels: [triage, site-request]
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
label: DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE
|
value: |
|
||||||
description: Fill all fields even if you think it is irrelevant for the issue
|
> [!IMPORTANT]
|
||||||
options:
|
> Not providing the required (*) information or removing the template will result in your issue being closed and ignored.
|
||||||
- label: I understand that I will be **blocked** if I *intentionally* remove or skip any mandatory\* field
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -24,9 +22,7 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that none of provided URLs [violate any copyrights](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy) or contain any [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) to the best of my knowledge
|
- label: I've checked that none of provided URLs [violate any copyrights](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy) or contain any [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) to the best of my knowledge
|
||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **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
|
|
||||||
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
|
|
||||||
required: true
|
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
|
||||||
|
@ -59,6 +55,8 @@ body:
|
||||||
id: verbose
|
id: verbose
|
||||||
attributes:
|
attributes:
|
||||||
label: Provide verbose output that clearly demonstrates the problem
|
label: Provide verbose output that clearly demonstrates the problem
|
||||||
|
description: |
|
||||||
|
This is mandatory unless absolutely impossible to provide. If you are unable to provide the output, please explain why.
|
||||||
options:
|
options:
|
||||||
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
||||||
required: true
|
required: true
|
||||||
|
@ -90,11 +88,3 @@ body:
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
> [!CAUTION]
|
|
||||||
> ### GitHub is experiencing a high volume of malicious spam comments.
|
|
||||||
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
|
||||||
>
|
|
||||||
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
name: Site feature request
|
name: Site feature request
|
||||||
description: Request a new functionality for a supported site
|
description: Request new functionality for a site supported by yt-dlp
|
||||||
labels: [triage, site-enhancement]
|
labels: [triage, site-enhancement]
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
label: DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE
|
value: |
|
||||||
description: Fill all fields even if you think it is irrelevant for the issue
|
> [!IMPORTANT]
|
||||||
options:
|
> Not providing the required (*) information or removing the template will result in your issue being closed and ignored.
|
||||||
- label: I understand that I will be **blocked** if I *intentionally* remove or skip any mandatory\* field
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -22,9 +20,7 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
|
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
|
||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **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
|
|
||||||
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
|
|
||||||
required: true
|
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
|
||||||
|
@ -55,6 +51,8 @@ body:
|
||||||
id: verbose
|
id: verbose
|
||||||
attributes:
|
attributes:
|
||||||
label: Provide verbose output that clearly demonstrates the problem
|
label: Provide verbose output that clearly demonstrates the problem
|
||||||
|
description: |
|
||||||
|
This is mandatory unless absolutely impossible to provide. If you are unable to provide the output, please explain why.
|
||||||
options:
|
options:
|
||||||
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
||||||
required: true
|
required: true
|
||||||
|
@ -86,11 +84,3 @@ body:
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
> [!CAUTION]
|
|
||||||
> ### GitHub is experiencing a high volume of malicious spam comments.
|
|
||||||
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
|
||||||
>
|
|
||||||
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
|
||||||
|
|
28
.github/ISSUE_TEMPLATE/4_bug_report.yml
vendored
28
.github/ISSUE_TEMPLATE/4_bug_report.yml
vendored
|
@ -2,13 +2,11 @@ name: Core bug report
|
||||||
description: Report a bug unrelated to any particular site or extractor
|
description: Report a bug unrelated to any particular site or extractor
|
||||||
labels: [triage, bug]
|
labels: [triage, bug]
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
label: DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE
|
value: |
|
||||||
description: Fill all fields even if you think it is irrelevant for the issue
|
> [!IMPORTANT]
|
||||||
options:
|
> Not providing the required (*) information or removing the template will result in your issue being closed and ignored.
|
||||||
- label: I understand that I will be **blocked** if I *intentionally* remove or skip any mandatory\* field
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -20,13 +18,7 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
|
- label: I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
|
- 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
|
|
||||||
- label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
|
|
||||||
required: true
|
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
|
|
||||||
required: true
|
|
||||||
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
|
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
|
@ -40,6 +32,8 @@ body:
|
||||||
id: verbose
|
id: verbose
|
||||||
attributes:
|
attributes:
|
||||||
label: Provide verbose output that clearly demonstrates the problem
|
label: Provide verbose output that clearly demonstrates the problem
|
||||||
|
description: |
|
||||||
|
This is mandatory unless absolutely impossible to provide. If you are unable to provide the output, please explain why.
|
||||||
options:
|
options:
|
||||||
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
||||||
required: true
|
required: true
|
||||||
|
@ -71,11 +65,3 @@ body:
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
> [!CAUTION]
|
|
||||||
> ### GitHub is experiencing a high volume of malicious spam comments.
|
|
||||||
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
|
||||||
>
|
|
||||||
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
|
||||||
|
|
26
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
26
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
|
@ -1,14 +1,12 @@
|
||||||
name: Feature request
|
name: Feature request
|
||||||
description: Request a new functionality unrelated to any particular site or extractor
|
description: Request a new feature unrelated to any particular site or extractor
|
||||||
labels: [triage, enhancement]
|
labels: [triage, enhancement]
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
label: DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE
|
value: |
|
||||||
description: Fill all fields even if you think it is irrelevant for the issue
|
> [!IMPORTANT]
|
||||||
options:
|
> Not providing the required (*) information or removing the template will result in your issue being closed and ignored.
|
||||||
- label: I understand that I will be **blocked** if I *intentionally* remove or skip any mandatory\* field
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -22,9 +20,7 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
|
- label: I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
|
||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **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
|
|
||||||
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
|
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
|
@ -38,6 +34,8 @@ body:
|
||||||
id: verbose
|
id: verbose
|
||||||
attributes:
|
attributes:
|
||||||
label: Provide verbose output that clearly demonstrates the problem
|
label: Provide verbose output that clearly demonstrates the problem
|
||||||
|
description: |
|
||||||
|
This is mandatory unless absolutely impossible to provide. If you are unable to provide the output, please explain why.
|
||||||
options:
|
options:
|
||||||
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
||||||
- label: "If using API, add `'verbose': True` to `YoutubeDL` params instead"
|
- label: "If using API, add `'verbose': True` to `YoutubeDL` params instead"
|
||||||
|
@ -65,11 +63,3 @@ body:
|
||||||
[youtube] Extracting URL: https://www.youtube.com/watch?v=BaW_jenozKc
|
[youtube] Extracting URL: https://www.youtube.com/watch?v=BaW_jenozKc
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
> [!CAUTION]
|
|
||||||
> ### GitHub is experiencing a high volume of malicious spam comments.
|
|
||||||
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
|
||||||
>
|
|
||||||
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
|
||||||
|
|
26
.github/ISSUE_TEMPLATE/6_question.yml
vendored
26
.github/ISSUE_TEMPLATE/6_question.yml
vendored
|
@ -1,14 +1,12 @@
|
||||||
name: Ask question
|
name: Ask question
|
||||||
description: Ask yt-dlp related question
|
description: Ask a question about using yt-dlp
|
||||||
labels: [question]
|
labels: [question]
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
label: DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE
|
value: |
|
||||||
description: Fill all fields even if you think it is irrelevant for the issue
|
> [!IMPORTANT]
|
||||||
options:
|
> Not providing the required (*) information or removing the template will result in your issue being closed and ignored.
|
||||||
- label: I understand that I will be **blocked** if I *intentionally* remove or skip any mandatory\* field
|
|
||||||
required: true
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
|
@ -28,9 +26,7 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
|
- label: I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
|
||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) 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
|
|
||||||
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
|
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: question
|
id: question
|
||||||
|
@ -44,6 +40,8 @@ body:
|
||||||
id: verbose
|
id: verbose
|
||||||
attributes:
|
attributes:
|
||||||
label: Provide verbose output that clearly demonstrates the problem
|
label: Provide verbose output that clearly demonstrates the problem
|
||||||
|
description: |
|
||||||
|
This is mandatory unless absolutely impossible to provide. If you are unable to provide the output, please explain why.
|
||||||
options:
|
options:
|
||||||
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
||||||
- label: "If using API, add `'verbose': True` to `YoutubeDL` params instead"
|
- label: "If using API, add `'verbose': True` to `YoutubeDL` params instead"
|
||||||
|
@ -71,11 +69,3 @@ body:
|
||||||
[youtube] Extracting URL: https://www.youtube.com/watch?v=BaW_jenozKc
|
[youtube] Extracting URL: https://www.youtube.com/watch?v=BaW_jenozKc
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
> [!CAUTION]
|
|
||||||
> ### GitHub is experiencing a high volume of malicious spam comments.
|
|
||||||
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
|
||||||
>
|
|
||||||
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
|
||||||
|
|
7
.github/ISSUE_TEMPLATE/config.yml
vendored
7
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1,8 +1,5 @@
|
||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Get help from the community on Discord
|
- name: Get help on Discord
|
||||||
url: https://discord.gg/H5MNcFW63r
|
url: https://discord.gg/H5MNcFW63r
|
||||||
about: Join the yt-dlp Discord for community-powered support!
|
about: Join the yt-dlp Discord server for support and discussion
|
||||||
- name: Matrix Bridge to the Discord server
|
|
||||||
url: https://matrix.to/#/#yt-dlp:matrix.org
|
|
||||||
about: For those who do not want to use Discord
|
|
||||||
|
|
|
@ -18,9 +18,7 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
|
- label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
|
||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) 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
|
|
||||||
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
|
|
||||||
required: true
|
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
|
||||||
|
|
|
@ -18,9 +18,7 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that none of provided URLs [violate any copyrights](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy) or contain any [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) to the best of my knowledge
|
- label: I've checked that none of provided URLs [violate any copyrights](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy) or contain any [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) to the best of my knowledge
|
||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **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
|
|
||||||
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
|
|
||||||
required: true
|
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
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: Site feature request
|
name: Site feature request
|
||||||
description: Request a new functionality for a supported site
|
description: Request new functionality for a site supported by yt-dlp
|
||||||
labels: [triage, site-enhancement]
|
labels: [triage, site-enhancement]
|
||||||
body:
|
body:
|
||||||
%(no_skip)s
|
%(no_skip)s
|
||||||
|
@ -16,9 +16,7 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
|
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
|
||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **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
|
|
||||||
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
|
|
||||||
required: true
|
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
|
||||||
|
|
8
.github/ISSUE_TEMPLATE_tmpl/4_bug_report.yml
vendored
8
.github/ISSUE_TEMPLATE_tmpl/4_bug_report.yml
vendored
|
@ -14,13 +14,7 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
|
- label: I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
|
- 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
|
|
||||||
- label: I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
|
|
||||||
required: true
|
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
|
|
||||||
required: true
|
|
||||||
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
|
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: Feature request
|
name: Feature request
|
||||||
description: Request a new functionality unrelated to any particular site or extractor
|
description: Request a new feature unrelated to any particular site or extractor
|
||||||
labels: [triage, enhancement]
|
labels: [triage, enhancement]
|
||||||
body:
|
body:
|
||||||
%(no_skip)s
|
%(no_skip)s
|
||||||
|
@ -16,9 +16,7 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
|
- label: I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
|
||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **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
|
|
||||||
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
|
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
|
|
6
.github/ISSUE_TEMPLATE_tmpl/6_question.yml
vendored
6
.github/ISSUE_TEMPLATE_tmpl/6_question.yml
vendored
|
@ -1,5 +1,5 @@
|
||||||
name: Ask question
|
name: Ask question
|
||||||
description: Ask yt-dlp related question
|
description: Ask a question about using yt-dlp
|
||||||
labels: [question]
|
labels: [question]
|
||||||
body:
|
body:
|
||||||
%(no_skip)s
|
%(no_skip)s
|
||||||
|
@ -22,9 +22,7 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
|
- label: I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
|
||||||
required: true
|
required: true
|
||||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) 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
|
|
||||||
- label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
|
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: question
|
id: question
|
||||||
|
|
37
.github/PULL_REQUEST_TEMPLATE.md
vendored
37
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,14 +1,17 @@
|
||||||
**IMPORTANT**: PRs without the template will be CLOSED
|
<!--
|
||||||
|
**IMPORTANT**: PRs without the template will be CLOSED
|
||||||
|
|
||||||
|
Due to the high volume of pull requests, it may be a while before your PR is reviewed.
|
||||||
|
Please try to keep your pull request focused on a single bugfix or new feature.
|
||||||
|
Pull requests with a vast scope and/or very large diff will take much longer to review.
|
||||||
|
It is recommended for new contributors to stick to smaller pull requests, so you can receive much more immediate feedback as you familiarize yourself with the codebase.
|
||||||
|
|
||||||
|
PLEASE AVOID FORCE-PUSHING after opening a PR, as it makes reviewing more difficult.
|
||||||
|
-->
|
||||||
|
|
||||||
### Description of your *pull request* and other information
|
### Description of your *pull request* and other information
|
||||||
|
|
||||||
<!--
|
ADD DETAILED DESCRIPTION HERE
|
||||||
|
|
||||||
Explanation of your *pull request* in arbitrary form goes here. Please **make sure the description explains the purpose and effect** of your *pull request* and is worded well enough to be understood. Provide as much **context and examples** as possible
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
ADD DESCRIPTION HERE
|
|
||||||
|
|
||||||
Fixes #
|
Fixes #
|
||||||
|
|
||||||
|
@ -16,24 +19,22 @@ ### Description of your *pull request* and other information
|
||||||
<details open><summary>Template</summary> <!-- OPEN is intentional -->
|
<details open><summary>Template</summary> <!-- OPEN is intentional -->
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
# PLEASE FOLLOW THE GUIDE BELOW
|
||||||
|
|
||||||
# PLEASE FOLLOW THE GUIDE BELOW
|
- You will be asked some questions, please read them **carefully** and answer honestly
|
||||||
|
- Put an `x` into all the boxes `[ ]` relevant to your *pull request* (like [x])
|
||||||
- You will be asked some questions, please read them **carefully** and answer honestly
|
- Use *Preview* tab to see what your *pull request* will actually look like
|
||||||
- Put an `x` into all the boxes `[ ]` relevant to your *pull request* (like [x])
|
|
||||||
- Use *Preview* tab to see how your *pull request* will actually look like
|
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Before submitting a *pull request* make sure you have:
|
### Before submitting a *pull request* make sure you have:
|
||||||
- [ ] At least skimmed through [contributing guidelines](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions) including [yt-dlp coding conventions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#yt-dlp-coding-conventions)
|
- [ ] At least skimmed through [contributing guidelines](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions) including [yt-dlp coding conventions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#yt-dlp-coding-conventions)
|
||||||
- [ ] [Searched](https://github.com/yt-dlp/yt-dlp/search?q=is%3Apr&type=Issues) the bugtracker for similar pull requests
|
- [ ] [Searched](https://github.com/yt-dlp/yt-dlp/search?q=is%3Apr&type=Issues) the bugtracker for similar pull requests
|
||||||
|
|
||||||
### 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 all of the following options that apply:
|
### 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 this code 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 this code but it is in 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)
|
||||||
|
|
||||||
### What is the purpose of your *pull request*?
|
### 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)
|
||||||
- [ ] New extractor ([Piracy websites will not be accepted](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy))
|
- [ ] New extractor ([Piracy websites will not be accepted](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy))
|
||||||
- [ ] Core bug fix/improvement
|
- [ ] Core bug fix/improvement
|
||||||
|
|
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
|
@ -33,7 +33,7 @@ jobs:
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
@ -47,7 +47,7 @@ jobs:
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
|
@ -60,6 +60,6 @@ jobs:
|
||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v3
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|
|
@ -736,3 +736,9 @@ NecroRomnt
|
||||||
pjrobertson
|
pjrobertson
|
||||||
subsense
|
subsense
|
||||||
test20140
|
test20140
|
||||||
|
arantius
|
||||||
|
entourage8
|
||||||
|
lfavole
|
||||||
|
mp3butcher
|
||||||
|
slipinthedove
|
||||||
|
YoshiTabletopGamer
|
||||||
|
|
43
Changelog.md
43
Changelog.md
|
@ -4,6 +4,49 @@ # Changelog
|
||||||
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
### 2025.02.19
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- **jsinterp**
|
||||||
|
- [Add `js_number_to_string`](https://github.com/yt-dlp/yt-dlp/commit/0d9f061d38c3a4da61972e2adad317079f2f1c84) ([#12110](https://github.com/yt-dlp/yt-dlp/issues/12110)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
- [Improve zeroise](https://github.com/yt-dlp/yt-dlp/commit/4ca8c44a073d5aa3a3e3112c35b2b23d6ce25ac6) ([#12313](https://github.com/yt-dlp/yt-dlp/issues/12313)) by [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **acast**: [Support shows.acast.com URLs](https://github.com/yt-dlp/yt-dlp/commit/57c717fee4bfbc9309845bbb48901b72e4b69304) ([#12223](https://github.com/yt-dlp/yt-dlp/issues/12223)) by [barsnick](https://github.com/barsnick)
|
||||||
|
- **cwtv**
|
||||||
|
- [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/18a28514e306e822eab4f3a79c76d515bf076406) ([#12207](https://github.com/yt-dlp/yt-dlp/issues/12207)) by [arantius](https://github.com/arantius)
|
||||||
|
- movie: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/03c3d705778c07739e0034b51490877cffdc0983) ([#12227](https://github.com/yt-dlp/yt-dlp/issues/12227)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **digiview**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/f53553087d3fde9dcd61d6e9f98caf09db1d8ef2) ([#9902](https://github.com/yt-dlp/yt-dlp/issues/9902)) by [lfavole](https://github.com/lfavole)
|
||||||
|
- **dropbox**: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/861aeec449c8f3c062d962945b234ff0341f61f3) ([#12228](https://github.com/yt-dlp/yt-dlp/issues/12228)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **francetv**
|
||||||
|
- site
|
||||||
|
- [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/817483ccc68aed6049ed9c4a2ffae44ca82d2b1c) ([#12236](https://github.com/yt-dlp/yt-dlp/issues/12236)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Fix livestream extraction](https://github.com/yt-dlp/yt-dlp/commit/1295bbedd45fa8d9bc3f7a194864ae280297848e) ([#12316](https://github.com/yt-dlp/yt-dlp/issues/12316)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **francetvinfo.fr**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/5c4c2ddfaa47988b4d50c1ad4988badc0b4f30c2) ([#12402](https://github.com/yt-dlp/yt-dlp/issues/12402)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **gem.cbc.ca**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/5271ef48c6f61c145e03e18e960995d2e651d205) ([#12404](https://github.com/yt-dlp/yt-dlp/issues/12404)) by [bashonly](https://github.com/bashonly), [dirkf](https://github.com/dirkf)
|
||||||
|
- **generic**: [Extract `live_status` for DASH manifest URLs](https://github.com/yt-dlp/yt-dlp/commit/19edaa44fcd375f54e63d6227b092f5252d3e889) ([#12256](https://github.com/yt-dlp/yt-dlp/issues/12256)) by [mp3butcher](https://github.com/mp3butcher)
|
||||||
|
- **globo**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/f8d0161455f00add65585ca1a476a7b5d56f5f96) ([#11795](https://github.com/yt-dlp/yt-dlp/issues/11795)) by [slipinthedove](https://github.com/slipinthedove), [YoshiTabletopGamer](https://github.com/YoshiTabletopGamer)
|
||||||
|
- **goplay**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/d59f14a0a7a8b55e6bf468237def62b73ab4a517) ([#12237](https://github.com/yt-dlp/yt-dlp/issues/12237)) by [alard](https://github.com/alard)
|
||||||
|
- **pbs**: [Support www.thirteen.org URLs](https://github.com/yt-dlp/yt-dlp/commit/9fb8ab2ff67fb699f60cce09163a580976e90c0e) ([#11191](https://github.com/yt-dlp/yt-dlp/issues/11191)) by [rohieb](https://github.com/rohieb)
|
||||||
|
- **reddit**: [Bypass gated subreddit warning](https://github.com/yt-dlp/yt-dlp/commit/6ca23ffaa4663cb552f937f0b1e9769b66db11bd) ([#12335](https://github.com/yt-dlp/yt-dlp/issues/12335)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **twitter**: [Fix syndication token generation](https://github.com/yt-dlp/yt-dlp/commit/14cd7f3443c6da4d49edaefcc12da9dee86e243e) ([#12107](https://github.com/yt-dlp/yt-dlp/issues/12107)) by [Grub4K](https://github.com/Grub4K), [pjrobertson](https://github.com/pjrobertson)
|
||||||
|
- **youtube**
|
||||||
|
- [Retry on more critical requests](https://github.com/yt-dlp/yt-dlp/commit/d48e612609d012abbea3785be4d26d78a014abb2) ([#12339](https://github.com/yt-dlp/yt-dlp/issues/12339)) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
- [nsig workaround for `tce` player JS](https://github.com/yt-dlp/yt-dlp/commit/ec17fb16e8d69d4e3e10fb73bf3221be8570dfee) ([#12401](https://github.com/yt-dlp/yt-dlp/issues/12401)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **zdf**: [Extract more metadata](https://github.com/yt-dlp/yt-dlp/commit/241ace4f104d50fdf7638f9203927aefcf57a1f7) ([#9565](https://github.com/yt-dlp/yt-dlp/issues/9565)) by [StefanLobbenmeier](https://github.com/StefanLobbenmeier) (With fixes in [e7882b6](https://github.com/yt-dlp/yt-dlp/commit/e7882b682b959e476d8454911655b3e9b14c79b2) by [bashonly](https://github.com/bashonly))
|
||||||
|
|
||||||
|
#### Downloader changes
|
||||||
|
- **hls**
|
||||||
|
- [Fix `BYTERANGE` logic](https://github.com/yt-dlp/yt-dlp/commit/10b7ff68e98f17655e31952f6e17120b2d7dda96) ([#11972](https://github.com/yt-dlp/yt-dlp/issues/11972)) by [entourage8](https://github.com/entourage8)
|
||||||
|
- [Support `--write-pages` for m3u8 media playlists](https://github.com/yt-dlp/yt-dlp/commit/be69468752ff598cacee57bb80533deab2367a5d) ([#12333](https://github.com/yt-dlp/yt-dlp/issues/12333)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Support `hls_media_playlist_data` format field](https://github.com/yt-dlp/yt-dlp/commit/c987be0acb6872c6561f28aa28171e803393d851) ([#12322](https://github.com/yt-dlp/yt-dlp/issues/12322)) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- [Improve Issue/PR templates](https://github.com/yt-dlp/yt-dlp/commit/517ddf3c3f12560ab93e3d36244dc82db9f97818) ([#11499](https://github.com/yt-dlp/yt-dlp/issues/11499)) by [seproDev](https://github.com/seproDev) (With fixes in [4ecb833](https://github.com/yt-dlp/yt-dlp/commit/4ecb833472c90e078567b561fb7c089f1aa9587b) by [bashonly](https://github.com/bashonly))
|
||||||
|
- **cleanup**: Miscellaneous: [4985a40](https://github.com/yt-dlp/yt-dlp/commit/4985a4041770eaa0016271809a1fd950dc809a55) by [dirkf](https://github.com/dirkf), [Grub4K](https://github.com/Grub4K), [StefanLobbenmeier](https://github.com/StefanLobbenmeier)
|
||||||
|
- **docs**: [Add note to `supportedsites.md`](https://github.com/yt-dlp/yt-dlp/commit/01a63629a21781458dcbd38779898e117678f5ff) ([#12382](https://github.com/yt-dlp/yt-dlp/issues/12382)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **test**: download: [Validate and sort info dict fields](https://github.com/yt-dlp/yt-dlp/commit/208163447408c78673b08c172beafe5c310fb167) ([#12299](https://github.com/yt-dlp/yt-dlp/issues/12299)) by [bashonly](https://github.com/bashonly), [pzhlkj6612](https://github.com/pzhlkj6612)
|
||||||
|
|
||||||
### 2025.01.26
|
### 2025.01.26
|
||||||
|
|
||||||
#### Core changes
|
#### Core changes
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
[](#installation "Installation")
|
[](#installation "Installation")
|
||||||
[](https://pypi.org/project/yt-dlp "PyPI")
|
[](https://pypi.org/project/yt-dlp "PyPI")
|
||||||
[](Collaborators.md#collaborators "Donate")
|
[](Collaborators.md#collaborators "Donate")
|
||||||
[](https://matrix.to/#/#yt-dlp:matrix.org "Matrix")
|
|
||||||
[](https://discord.gg/H5MNcFW63r "Discord")
|
[](https://discord.gg/H5MNcFW63r "Discord")
|
||||||
[](supportedsites.md "Supported Sites")
|
[](supportedsites.md "Supported Sites")
|
||||||
[](LICENSE "License")
|
[](LICENSE "License")
|
||||||
|
@ -1527,7 +1526,7 @@ ## Sorting Formats
|
||||||
- `hasvid`: Gives priority to formats that have a video stream
|
- `hasvid`: Gives priority to formats that have a video stream
|
||||||
- `hasaud`: Gives priority to formats that have an audio stream
|
- `hasaud`: Gives priority to formats that have an audio stream
|
||||||
- `ie_pref`: The format preference
|
- `ie_pref`: The format preference
|
||||||
- `lang`: The language preference
|
- `lang`: The language preference as determined by the extractor (e.g. original language preferred over audio description)
|
||||||
- `quality`: The quality of the format
|
- `quality`: The quality of the format
|
||||||
- `source`: The preference of the source
|
- `source`: The preference of the source
|
||||||
- `proto`: Protocol used for download (`https`/`ftps` > `http`/`ftp` > `m3u8_native`/`m3u8` > `http_dash_segments`> `websocket_frag` > `mms`/`rtsp` > `f4f`/`f4m`)
|
- `proto`: Protocol used for download (`https`/`ftps` > `http`/`ftp` > `m3u8_native`/`m3u8` > `http_dash_segments`> `websocket_frag` > `mms`/`rtsp` > `f4f`/`f4m`)
|
||||||
|
|
|
@ -11,11 +11,13 @@
|
||||||
|
|
||||||
from devscripts.utils import get_filename_args, read_file, write_file
|
from devscripts.utils import get_filename_args, read_file, write_file
|
||||||
|
|
||||||
VERBOSE_TMPL = '''
|
VERBOSE = '''
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: verbose
|
id: verbose
|
||||||
attributes:
|
attributes:
|
||||||
label: Provide verbose output that clearly demonstrates the problem
|
label: Provide verbose output that clearly demonstrates the problem
|
||||||
|
description: |
|
||||||
|
This is mandatory unless absolutely impossible to provide. If you are unable to provide the output, please explain why.
|
||||||
options:
|
options:
|
||||||
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
||||||
required: true
|
required: true
|
||||||
|
@ -47,31 +49,23 @@
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
> [!CAUTION]
|
|
||||||
> ### GitHub is experiencing a high volume of malicious spam comments.
|
|
||||||
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
|
||||||
>
|
|
||||||
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
|
||||||
'''.strip()
|
'''.strip()
|
||||||
|
|
||||||
NO_SKIP = '''
|
NO_SKIP = '''
|
||||||
- type: checkboxes
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
label: DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE
|
value: |
|
||||||
description: Fill all fields even if you think it is irrelevant for the issue
|
> [!IMPORTANT]
|
||||||
options:
|
> Not providing the required (*) information or removing the template will result in your issue being closed and ignored.
|
||||||
- label: I understand that I will be **blocked** if I *intentionally* remove or skip any mandatory\\* field
|
|
||||||
required: true
|
|
||||||
'''.strip()
|
'''.strip()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
fields = {'no_skip': NO_SKIP}
|
fields = {
|
||||||
fields['verbose'] = VERBOSE_TMPL % fields
|
'no_skip': NO_SKIP,
|
||||||
fields['verbose_optional'] = re.sub(r'(\n\s+validations:)?\n\s+required: true', '', fields['verbose'])
|
'verbose': VERBOSE,
|
||||||
|
'verbose_optional': re.sub(r'(\n\s+validations:)?\n\s+required: true', '', VERBOSE),
|
||||||
|
}
|
||||||
|
|
||||||
infile, outfile = get_filename_args(has_infile=True)
|
infile, outfile = get_filename_args(has_infile=True)
|
||||||
write_file(outfile, read_file(infile) % fields)
|
write_file(outfile, read_file(infile) % fields)
|
||||||
|
|
|
@ -10,10 +10,21 @@
|
||||||
from devscripts.utils import get_filename_args, write_file
|
from devscripts.utils import get_filename_args, write_file
|
||||||
from yt_dlp.extractor import list_extractor_classes
|
from yt_dlp.extractor import list_extractor_classes
|
||||||
|
|
||||||
|
TEMPLATE = '''\
|
||||||
|
# Supported sites
|
||||||
|
|
||||||
|
Below is a list of all extractors that are currently included with yt-dlp.
|
||||||
|
If a site is not listed here, it might still be supported by yt-dlp's embed extraction or generic extractor.
|
||||||
|
Not all sites listed here are guaranteed to work; websites are constantly changing and sometimes this breaks yt-dlp's support for them.
|
||||||
|
The only reliable way to check if a site is supported is to try it.
|
||||||
|
|
||||||
|
{ie_list}
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
out = '\n'.join(ie.description() for ie in list_extractor_classes() if ie.IE_DESC is not False)
|
out = '\n'.join(ie.description() for ie in list_extractor_classes() if ie.IE_DESC is not False)
|
||||||
write_file(get_filename_args(), f'# Supported sites\n{out}\n')
|
write_file(get_filename_args(), TEMPLATE.format(ie_list=out))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -25,7 +25,8 @@ def parse_args():
|
||||||
|
|
||||||
|
|
||||||
def run_tests(*tests, pattern=None, ci=False):
|
def run_tests(*tests, pattern=None, ci=False):
|
||||||
run_core = 'core' in tests or (not pattern and not tests)
|
# XXX: hatch uses `tests` if no arguments are passed
|
||||||
|
run_core = 'core' in tests or 'tests' in tests or (not pattern and not tests)
|
||||||
run_download = 'download' in tests
|
run_download = 'download' in tests
|
||||||
|
|
||||||
pytest_args = args.pytest_args or os.getenv('HATCH_TEST_ARGS', '')
|
pytest_args = args.pytest_args or os.getenv('HATCH_TEST_ARGS', '')
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
# Supported sites
|
# Supported sites
|
||||||
|
|
||||||
|
Below is a list of all extractors that are currently included with yt-dlp.
|
||||||
|
If a site is not listed here, it might still be supported by yt-dlp's embed extraction or generic extractor.
|
||||||
|
Not all sites listed here are guaranteed to work; websites are constantly changing and sometimes this breaks yt-dlp's support for them.
|
||||||
|
The only reliable way to check if a site is supported is to try it.
|
||||||
|
|
||||||
- **17live**
|
- **17live**
|
||||||
- **17live:clip**
|
- **17live:clip**
|
||||||
- **1News**: 1news.co.nz article videos
|
- **1News**: 1news.co.nz article videos
|
||||||
|
@ -314,7 +320,8 @@ # Supported sites
|
||||||
- **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**
|
||||||
|
- **cwtv:movie**
|
||||||
- **Cybrary**: [*cybrary*](## "netrc machine")
|
- **Cybrary**: [*cybrary*](## "netrc machine")
|
||||||
- **CybraryCourse**: [*cybrary*](## "netrc machine")
|
- **CybraryCourse**: [*cybrary*](## "netrc machine")
|
||||||
- **DacastPlaylist**
|
- **DacastPlaylist**
|
||||||
|
@ -349,6 +356,7 @@ # Supported sites
|
||||||
- **DigitalConcertHall**: [*digitalconcerthall*](## "netrc machine") DigitalConcertHall extractor
|
- **DigitalConcertHall**: [*digitalconcerthall*](## "netrc machine") DigitalConcertHall extractor
|
||||||
- **DigitallySpeaking**
|
- **DigitallySpeaking**
|
||||||
- **Digiteka**
|
- **Digiteka**
|
||||||
|
- **Digiview**
|
||||||
- **DiscogsReleasePlaylist**
|
- **DiscogsReleasePlaylist**
|
||||||
- **DiscoveryLife**
|
- **DiscoveryLife**
|
||||||
- **DiscoveryNetworksDe**
|
- **DiscoveryNetworksDe**
|
||||||
|
@ -465,9 +473,9 @@ # Supported sites
|
||||||
- **fptplay**: fptplay.vn
|
- **fptplay**: fptplay.vn
|
||||||
- **FranceCulture**
|
- **FranceCulture**
|
||||||
- **FranceInter**
|
- **FranceInter**
|
||||||
- **FranceTV**
|
- **francetv**
|
||||||
|
- **francetv:site**
|
||||||
- **francetvinfo.fr**
|
- **francetvinfo.fr**
|
||||||
- **FranceTVSite**
|
|
||||||
- **Freesound**
|
- **Freesound**
|
||||||
- **freespeech.org**
|
- **freespeech.org**
|
||||||
- **freetv:series**
|
- **freetv:series**
|
||||||
|
@ -499,7 +507,7 @@ # Supported sites
|
||||||
- **GediDigital**
|
- **GediDigital**
|
||||||
- **gem.cbc.ca**: [*cbcgem*](## "netrc machine")
|
- **gem.cbc.ca**: [*cbcgem*](## "netrc machine")
|
||||||
- **gem.cbc.ca:live**
|
- **gem.cbc.ca:live**
|
||||||
- **gem.cbc.ca:playlist**
|
- **gem.cbc.ca:playlist**: [*cbcgem*](## "netrc machine")
|
||||||
- **Genius**
|
- **Genius**
|
||||||
- **GeniusLyrics**
|
- **GeniusLyrics**
|
||||||
- **Germanupa**: germanupa.de
|
- **Germanupa**: germanupa.de
|
||||||
|
|
|
@ -237,6 +237,20 @@ def sanitize(key, value):
|
||||||
|
|
||||||
|
|
||||||
def expect_info_dict(self, got_dict, expected_dict):
|
def expect_info_dict(self, got_dict, expected_dict):
|
||||||
|
ALLOWED_KEYS_SORT_ORDER = (
|
||||||
|
# NB: Keep in sync with the docstring of extractor/common.py
|
||||||
|
'id', 'ext', 'direct', 'display_id', 'title', 'alt_title', 'description', 'media_type',
|
||||||
|
'uploader', 'uploader_id', 'uploader_url', 'channel', 'channel_id', 'channel_url', 'channel_is_verified',
|
||||||
|
'channel_follower_count', 'comment_count', 'view_count', 'concurrent_view_count',
|
||||||
|
'like_count', 'dislike_count', 'repost_count', 'average_rating', 'age_limit', 'duration', 'thumbnail', 'heatmap',
|
||||||
|
'chapters', 'chapter', 'chapter_number', 'chapter_id', 'start_time', 'end_time', 'section_start', 'section_end',
|
||||||
|
'categories', 'tags', 'cast', 'composers', 'artists', 'album_artists', 'creators', 'genres',
|
||||||
|
'track', 'track_number', 'track_id', 'album', 'album_type', 'disc_number',
|
||||||
|
'series', 'series_id', 'season', 'season_number', 'season_id', 'episode', 'episode_number', 'episode_id',
|
||||||
|
'timestamp', 'upload_date', 'release_timestamp', 'release_date', 'release_year', 'modified_timestamp', 'modified_date',
|
||||||
|
'playable_in_embed', 'availability', 'live_status', 'location', 'license', '_old_archive_ids',
|
||||||
|
)
|
||||||
|
|
||||||
expect_dict(self, got_dict, expected_dict)
|
expect_dict(self, got_dict, expected_dict)
|
||||||
# Check for the presence of mandatory fields
|
# Check for the presence of mandatory fields
|
||||||
if got_dict.get('_type') not in ('playlist', 'multi_video'):
|
if got_dict.get('_type') not in ('playlist', 'multi_video'):
|
||||||
|
@ -252,7 +266,13 @@ def expect_info_dict(self, got_dict, expected_dict):
|
||||||
|
|
||||||
test_info_dict = sanitize_got_info_dict(got_dict)
|
test_info_dict = sanitize_got_info_dict(got_dict)
|
||||||
|
|
||||||
missing_keys = set(test_info_dict.keys()) - set(expected_dict.keys())
|
# Check for invalid/misspelled field names being returned by the extractor
|
||||||
|
invalid_keys = sorted(test_info_dict.keys() - ALLOWED_KEYS_SORT_ORDER)
|
||||||
|
self.assertFalse(invalid_keys, f'Invalid fields returned by the extractor: {", ".join(invalid_keys)}')
|
||||||
|
|
||||||
|
missing_keys = sorted(
|
||||||
|
test_info_dict.keys() - expected_dict.keys(),
|
||||||
|
key=lambda x: ALLOWED_KEYS_SORT_ORDER.index(x))
|
||||||
if missing_keys:
|
if missing_keys:
|
||||||
def _repr(v):
|
def _repr(v):
|
||||||
if isinstance(v, str):
|
if isinstance(v, str):
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
from yt_dlp.jsinterp import JS_Undefined, JSInterpreter
|
from yt_dlp.jsinterp import JS_Undefined, JSInterpreter, js_number_to_string
|
||||||
|
|
||||||
|
|
||||||
class NaN:
|
class NaN:
|
||||||
|
@ -93,6 +93,16 @@ def test_operators(self):
|
||||||
self._test('function f(){return 0 ?? 42;}', 0)
|
self._test('function f(){return 0 ?? 42;}', 0)
|
||||||
self._test('function f(){return "life, the universe and everything" < 42;}', False)
|
self._test('function f(){return "life, the universe and everything" < 42;}', False)
|
||||||
self._test('function f(){return 0 - 7 * - 6;}', 42)
|
self._test('function f(){return 0 - 7 * - 6;}', 42)
|
||||||
|
self._test('function f(){return true << "5";}', 32)
|
||||||
|
self._test('function f(){return true << true;}', 2)
|
||||||
|
self._test('function f(){return "19" & "21.9";}', 17)
|
||||||
|
self._test('function f(){return "19" & false;}', 0)
|
||||||
|
self._test('function f(){return "11.0" >> "2.1";}', 2)
|
||||||
|
self._test('function f(){return 5 ^ 9;}', 12)
|
||||||
|
self._test('function f(){return 0.0 << NaN}', 0)
|
||||||
|
self._test('function f(){return null << undefined}', 0)
|
||||||
|
# TODO: Does not work due to number too large
|
||||||
|
# self._test('function f(){return 21 << 4294967297}', 42)
|
||||||
|
|
||||||
def test_array_access(self):
|
def test_array_access(self):
|
||||||
self._test('function f(){var x = [1,2,3]; x[0] = 4; x[0] = 5; x[2.0] = 7; return x;}', [5, 2, 7])
|
self._test('function f(){var x = [1,2,3]; x[0] = 4; x[0] = 5; x[2.0] = 7; return x;}', [5, 2, 7])
|
||||||
|
@ -431,6 +441,27 @@ def test_slice(self):
|
||||||
self._test('function f(){return "012345678".slice(-1, 1)}', '')
|
self._test('function f(){return "012345678".slice(-1, 1)}', '')
|
||||||
self._test('function f(){return "012345678".slice(-3, -1)}', '67')
|
self._test('function f(){return "012345678".slice(-3, -1)}', '67')
|
||||||
|
|
||||||
|
def test_js_number_to_string(self):
|
||||||
|
for test, radix, expected in [
|
||||||
|
(0, None, '0'),
|
||||||
|
(-0, None, '0'),
|
||||||
|
(0.0, None, '0'),
|
||||||
|
(-0.0, None, '0'),
|
||||||
|
(math.nan, None, 'NaN'),
|
||||||
|
(-math.nan, None, 'NaN'),
|
||||||
|
(math.inf, None, 'Infinity'),
|
||||||
|
(-math.inf, None, '-Infinity'),
|
||||||
|
(10 ** 21.5, 8, '526665530627250154000000'),
|
||||||
|
(6, 2, '110'),
|
||||||
|
(254, 16, 'fe'),
|
||||||
|
(-10, 2, '-1010'),
|
||||||
|
(-0xff, 2, '-11111111'),
|
||||||
|
(0.1 + 0.2, 16, '0.4cccccccccccd'),
|
||||||
|
(1234.1234, 10, '1234.1234'),
|
||||||
|
# (1000000000000000128, 10, '1000000000000000100')
|
||||||
|
]:
|
||||||
|
assert js_number_to_string(test, radix) == expected
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -201,6 +201,10 @@
|
||||||
'https://www.youtube.com/s/player/2f1832d2/player_ias.vflset/en_US/base.js',
|
'https://www.youtube.com/s/player/2f1832d2/player_ias.vflset/en_US/base.js',
|
||||||
'YWt1qdbe8SAfkoPHW5d', 'RrRjWQOJmBiP',
|
'YWt1qdbe8SAfkoPHW5d', 'RrRjWQOJmBiP',
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/9c6dfc4a/player_ias.vflset/en_US/base.js',
|
||||||
|
'jbu7ylIosQHyJyJV', 'uwI0ESiynAmhNg',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -165,7 +165,7 @@
|
||||||
write_json_file,
|
write_json_file,
|
||||||
write_string,
|
write_string,
|
||||||
)
|
)
|
||||||
from .utils._utils import _UnsafeExtensionError, _YDLLogger
|
from .utils._utils import _UnsafeExtensionError, _YDLLogger, _ProgressState
|
||||||
from .utils.networking import (
|
from .utils.networking import (
|
||||||
HTTPHeaderDict,
|
HTTPHeaderDict,
|
||||||
clean_headers,
|
clean_headers,
|
||||||
|
@ -606,7 +606,7 @@ class YoutubeDL:
|
||||||
# NB: Keep in sync with the docstring of extractor/common.py
|
# NB: Keep in sync with the docstring of extractor/common.py
|
||||||
'url', 'manifest_url', 'manifest_stream_number', 'ext', 'format', 'format_id', 'format_note',
|
'url', 'manifest_url', 'manifest_stream_number', 'ext', 'format', 'format_id', 'format_note',
|
||||||
'width', 'height', 'aspect_ratio', 'resolution', 'dynamic_range', 'tbr', 'abr', 'acodec', 'asr', 'audio_channels',
|
'width', 'height', 'aspect_ratio', 'resolution', 'dynamic_range', 'tbr', 'abr', 'acodec', 'asr', 'audio_channels',
|
||||||
'vbr', 'fps', 'vcodec', 'container', 'filesize', 'filesize_approx', 'rows', 'columns',
|
'vbr', 'fps', 'vcodec', 'container', 'filesize', 'filesize_approx', 'rows', 'columns', 'hls_media_playlist_data',
|
||||||
'player_url', 'protocol', 'fragment_base_url', 'fragments', 'is_from_start', 'is_dash_periods', 'request_data',
|
'player_url', 'protocol', 'fragment_base_url', 'fragments', 'is_from_start', 'is_dash_periods', 'request_data',
|
||||||
'preference', 'language', 'language_preference', 'quality', 'source_preference', 'cookies',
|
'preference', 'language', 'language_preference', 'quality', 'source_preference', 'cookies',
|
||||||
'http_headers', 'stretched_ratio', 'no_resume', 'has_drm', 'extra_param_to_segment_url', 'extra_param_to_key_url',
|
'http_headers', 'stretched_ratio', 'no_resume', 'has_drm', 'extra_param_to_segment_url', 'extra_param_to_key_url',
|
||||||
|
@ -654,20 +654,19 @@ def __init__(self, params=None, auto_init=True):
|
||||||
if not all_plugins_loaded.value:
|
if not all_plugins_loaded.value:
|
||||||
load_all_plugins()
|
load_all_plugins()
|
||||||
|
|
||||||
|
try:
|
||||||
|
windows_enable_vt_mode()
|
||||||
|
except Exception as e:
|
||||||
|
self.write_debug(f'Failed to enable VT mode: {e}')
|
||||||
|
|
||||||
stdout = sys.stderr if self.params.get('logtostderr') else sys.stdout
|
stdout = sys.stderr if self.params.get('logtostderr') else sys.stdout
|
||||||
self._out_files = Namespace(
|
self._out_files = Namespace(
|
||||||
out=stdout,
|
out=stdout,
|
||||||
error=sys.stderr,
|
error=sys.stderr,
|
||||||
screen=sys.stderr if self.params.get('quiet') else stdout,
|
screen=sys.stderr if self.params.get('quiet') else stdout,
|
||||||
console=None if os.name == 'nt' else next(
|
console=next(filter(supports_terminal_sequences, (sys.stderr, sys.stdout)), None),
|
||||||
filter(supports_terminal_sequences, (sys.stderr, sys.stdout)), None),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
|
||||||
windows_enable_vt_mode()
|
|
||||||
except Exception as e:
|
|
||||||
self.write_debug(f'Failed to enable VT mode: {e}')
|
|
||||||
|
|
||||||
if self.params.get('no_color'):
|
if self.params.get('no_color'):
|
||||||
if self.params.get('color') is not None:
|
if self.params.get('color') is not None:
|
||||||
self.params.setdefault('_warnings', []).append(
|
self.params.setdefault('_warnings', []).append(
|
||||||
|
@ -968,21 +967,22 @@ def to_stderr(self, message, only_once=False):
|
||||||
self._write_string(f'{self._bidi_workaround(message)}\n', self._out_files.error, only_once=only_once)
|
self._write_string(f'{self._bidi_workaround(message)}\n', self._out_files.error, only_once=only_once)
|
||||||
|
|
||||||
def _send_console_code(self, code):
|
def _send_console_code(self, code):
|
||||||
if os.name == 'nt' or not self._out_files.console:
|
if not supports_terminal_sequences(self._out_files.console):
|
||||||
return
|
return False
|
||||||
self._write_string(code, self._out_files.console)
|
self._write_string(code, self._out_files.console)
|
||||||
|
return True
|
||||||
|
|
||||||
def to_console_title(self, message):
|
def to_console_title(self, message=None, progress_state=None, percent=None):
|
||||||
if not self.params.get('consoletitle', False):
|
if not self.params.get('consoletitle'):
|
||||||
return
|
return
|
||||||
message = remove_terminal_sequences(message)
|
|
||||||
if os.name == 'nt':
|
if message:
|
||||||
if ctypes.windll.kernel32.GetConsoleWindow():
|
success = self._send_console_code(f'\033]0;{remove_terminal_sequences(message)}\007')
|
||||||
# c_wchar_p() might not be necessary if `message` is
|
if not success and os.name == 'nt' and ctypes.windll.kernel32.GetConsoleWindow():
|
||||||
# already of type unicode()
|
ctypes.windll.kernel32.SetConsoleTitleW(message)
|
||||||
ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
|
|
||||||
else:
|
if isinstance(progress_state, _ProgressState):
|
||||||
self._send_console_code(f'\033]0;{message}\007')
|
self._send_console_code(progress_state.get_ansi_escape(percent))
|
||||||
|
|
||||||
def save_console_title(self):
|
def save_console_title(self):
|
||||||
if not self.params.get('consoletitle') or self.params.get('simulate'):
|
if not self.params.get('consoletitle') or self.params.get('simulate'):
|
||||||
|
@ -996,6 +996,7 @@ def restore_console_title(self):
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.save_console_title()
|
self.save_console_title()
|
||||||
|
self.to_console_title(progress_state=_ProgressState.INDETERMINATE)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def save_cookies(self):
|
def save_cookies(self):
|
||||||
|
@ -1004,6 +1005,7 @@ def save_cookies(self):
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
self.restore_console_title()
|
self.restore_console_title()
|
||||||
|
self.to_console_title(progress_state=_ProgressState.HIDDEN)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
|
|
@ -294,18 +294,20 @@ def parse_sleep_func(expr):
|
||||||
raise ValueError(f'invalid {key} retry sleep expression {expr!r}')
|
raise ValueError(f'invalid {key} retry sleep expression {expr!r}')
|
||||||
|
|
||||||
# Bytes
|
# Bytes
|
||||||
def validate_bytes(name, value):
|
def validate_bytes(name, value, strict_positive=False):
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
numeric_limit = parse_bytes(value)
|
numeric_limit = parse_bytes(value)
|
||||||
validate(numeric_limit is not None, 'rate limit', value)
|
validate(numeric_limit is not None, name, value)
|
||||||
|
if strict_positive:
|
||||||
|
validate_positive(name, numeric_limit, True)
|
||||||
return numeric_limit
|
return numeric_limit
|
||||||
|
|
||||||
opts.ratelimit = validate_bytes('rate limit', opts.ratelimit)
|
opts.ratelimit = validate_bytes('rate limit', opts.ratelimit, True)
|
||||||
opts.throttledratelimit = validate_bytes('throttled rate limit', opts.throttledratelimit)
|
opts.throttledratelimit = validate_bytes('throttled rate limit', opts.throttledratelimit)
|
||||||
opts.min_filesize = validate_bytes('min filesize', opts.min_filesize)
|
opts.min_filesize = validate_bytes('min filesize', opts.min_filesize)
|
||||||
opts.max_filesize = validate_bytes('max filesize', opts.max_filesize)
|
opts.max_filesize = validate_bytes('max filesize', opts.max_filesize)
|
||||||
opts.buffersize = validate_bytes('buffer size', opts.buffersize)
|
opts.buffersize = validate_bytes('buffer size', opts.buffersize, True)
|
||||||
opts.http_chunk_size = validate_bytes('http chunk size', opts.http_chunk_size)
|
opts.http_chunk_size = validate_bytes('http chunk size', opts.http_chunk_size)
|
||||||
|
|
||||||
# Output templates
|
# Output templates
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
timetuple_from_msec,
|
timetuple_from_msec,
|
||||||
try_call,
|
try_call,
|
||||||
)
|
)
|
||||||
|
from ..utils._utils import _ProgressState
|
||||||
|
|
||||||
|
|
||||||
class FileDownloader:
|
class FileDownloader:
|
||||||
|
@ -333,7 +334,7 @@ def _report_progress_status(self, s, default_template):
|
||||||
progress_dict), s.get('progress_idx') or 0)
|
progress_dict), s.get('progress_idx') or 0)
|
||||||
self.to_console_title(self.ydl.evaluate_outtmpl(
|
self.to_console_title(self.ydl.evaluate_outtmpl(
|
||||||
progress_template.get('download-title') or 'yt-dlp %(progress._default_template)s',
|
progress_template.get('download-title') or 'yt-dlp %(progress._default_template)s',
|
||||||
progress_dict))
|
progress_dict), _ProgressState.from_dict(s), s.get('_percent'))
|
||||||
|
|
||||||
def _format_progress(self, *args, **kwargs):
|
def _format_progress(self, *args, **kwargs):
|
||||||
return self.ydl._format_text(
|
return self.ydl._format_text(
|
||||||
|
@ -357,6 +358,7 @@ def with_fields(*tups, default=''):
|
||||||
'_speed_str': self.format_speed(speed).strip(),
|
'_speed_str': self.format_speed(speed).strip(),
|
||||||
'_total_bytes_str': _format_bytes('total_bytes'),
|
'_total_bytes_str': _format_bytes('total_bytes'),
|
||||||
'_elapsed_str': self.format_seconds(s.get('elapsed')),
|
'_elapsed_str': self.format_seconds(s.get('elapsed')),
|
||||||
|
'_percent': 100.0,
|
||||||
'_percent_str': self.format_percent(100),
|
'_percent_str': self.format_percent(100),
|
||||||
})
|
})
|
||||||
self._report_progress_status(s, join_nonempty(
|
self._report_progress_status(s, join_nonempty(
|
||||||
|
@ -375,13 +377,15 @@ def with_fields(*tups, default=''):
|
||||||
return
|
return
|
||||||
self._progress_delta_time += update_delta
|
self._progress_delta_time += update_delta
|
||||||
|
|
||||||
|
progress = try_call(
|
||||||
|
lambda: 100 * s['downloaded_bytes'] / s['total_bytes'],
|
||||||
|
lambda: 100 * s['downloaded_bytes'] / s['total_bytes_estimate'],
|
||||||
|
lambda: s['downloaded_bytes'] == 0 and 0)
|
||||||
s.update({
|
s.update({
|
||||||
'_eta_str': self.format_eta(s.get('eta')).strip(),
|
'_eta_str': self.format_eta(s.get('eta')).strip(),
|
||||||
'_speed_str': self.format_speed(s.get('speed')),
|
'_speed_str': self.format_speed(s.get('speed')),
|
||||||
'_percent_str': self.format_percent(try_call(
|
'_percent': progress,
|
||||||
lambda: 100 * s['downloaded_bytes'] / s['total_bytes'],
|
'_percent_str': self.format_percent(progress),
|
||||||
lambda: 100 * s['downloaded_bytes'] / s['total_bytes_estimate'],
|
|
||||||
lambda: s['downloaded_bytes'] == 0 and 0)),
|
|
||||||
'_total_bytes_str': _format_bytes('total_bytes'),
|
'_total_bytes_str': _format_bytes('total_bytes'),
|
||||||
'_total_bytes_estimate_str': _format_bytes('total_bytes_estimate'),
|
'_total_bytes_estimate_str': _format_bytes('total_bytes_estimate'),
|
||||||
'_downloaded_bytes_str': _format_bytes('downloaded_bytes'),
|
'_downloaded_bytes_str': _format_bytes('downloaded_bytes'),
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
update_url_query,
|
update_url_query,
|
||||||
urljoin,
|
urljoin,
|
||||||
)
|
)
|
||||||
|
from ..utils._utils import _request_dump_filename
|
||||||
|
|
||||||
|
|
||||||
class HlsFD(FragmentFD):
|
class HlsFD(FragmentFD):
|
||||||
|
@ -72,11 +73,23 @@ def check_results():
|
||||||
|
|
||||||
def real_download(self, filename, info_dict):
|
def real_download(self, filename, info_dict):
|
||||||
man_url = info_dict['url']
|
man_url = info_dict['url']
|
||||||
self.to_screen(f'[{self.FD_NAME}] Downloading m3u8 manifest')
|
|
||||||
|
|
||||||
urlh = self.ydl.urlopen(self._prepare_url(info_dict, man_url))
|
s = info_dict.get('hls_media_playlist_data')
|
||||||
man_url = urlh.url
|
if s:
|
||||||
s = urlh.read().decode('utf-8', 'ignore')
|
self.to_screen(f'[{self.FD_NAME}] Using m3u8 manifest from extracted info')
|
||||||
|
else:
|
||||||
|
self.to_screen(f'[{self.FD_NAME}] Downloading m3u8 manifest')
|
||||||
|
urlh = self.ydl.urlopen(self._prepare_url(info_dict, man_url))
|
||||||
|
man_url = urlh.url
|
||||||
|
s_bytes = urlh.read()
|
||||||
|
if self.params.get('write_pages'):
|
||||||
|
dump_filename = _request_dump_filename(
|
||||||
|
man_url, info_dict['id'], None,
|
||||||
|
trim_length=self.params.get('trim_file_name'))
|
||||||
|
self.to_screen(f'[{self.FD_NAME}] Saving request to {dump_filename}')
|
||||||
|
with open(dump_filename, 'wb') as outf:
|
||||||
|
outf.write(s_bytes)
|
||||||
|
s = s_bytes.decode('utf-8', 'ignore')
|
||||||
|
|
||||||
can_download, message = self.can_download(s, info_dict, self.params.get('allow_unplayable_formats')), None
|
can_download, message = self.can_download(s, info_dict, self.params.get('allow_unplayable_formats')), None
|
||||||
if can_download:
|
if can_download:
|
||||||
|
@ -177,6 +190,7 @@ def is_ad_fragment_end(s):
|
||||||
if external_aes_iv:
|
if external_aes_iv:
|
||||||
external_aes_iv = binascii.unhexlify(remove_start(external_aes_iv, '0x').zfill(32))
|
external_aes_iv = binascii.unhexlify(remove_start(external_aes_iv, '0x').zfill(32))
|
||||||
byte_range = {}
|
byte_range = {}
|
||||||
|
byte_range_offset = 0
|
||||||
discontinuity_count = 0
|
discontinuity_count = 0
|
||||||
frag_index = 0
|
frag_index = 0
|
||||||
ad_frag_next = False
|
ad_frag_next = False
|
||||||
|
@ -204,6 +218,11 @@ def is_ad_fragment_end(s):
|
||||||
})
|
})
|
||||||
media_sequence += 1
|
media_sequence += 1
|
||||||
|
|
||||||
|
# If the byte_range is truthy, reset it after appending a fragment that uses it
|
||||||
|
if byte_range:
|
||||||
|
byte_range_offset = byte_range['end']
|
||||||
|
byte_range = {}
|
||||||
|
|
||||||
elif line.startswith('#EXT-X-MAP'):
|
elif line.startswith('#EXT-X-MAP'):
|
||||||
if format_index and discontinuity_count != format_index:
|
if format_index and discontinuity_count != format_index:
|
||||||
continue
|
continue
|
||||||
|
@ -217,10 +236,12 @@ def is_ad_fragment_end(s):
|
||||||
if extra_segment_query:
|
if extra_segment_query:
|
||||||
frag_url = update_url_query(frag_url, extra_segment_query)
|
frag_url = update_url_query(frag_url, extra_segment_query)
|
||||||
|
|
||||||
|
map_byte_range = {}
|
||||||
|
|
||||||
if map_info.get('BYTERANGE'):
|
if map_info.get('BYTERANGE'):
|
||||||
splitted_byte_range = map_info.get('BYTERANGE').split('@')
|
splitted_byte_range = map_info.get('BYTERANGE').split('@')
|
||||||
sub_range_start = int(splitted_byte_range[1]) if len(splitted_byte_range) == 2 else byte_range['end']
|
sub_range_start = int(splitted_byte_range[1]) if len(splitted_byte_range) == 2 else 0
|
||||||
byte_range = {
|
map_byte_range = {
|
||||||
'start': sub_range_start,
|
'start': sub_range_start,
|
||||||
'end': sub_range_start + int(splitted_byte_range[0]),
|
'end': sub_range_start + int(splitted_byte_range[0]),
|
||||||
}
|
}
|
||||||
|
@ -229,7 +250,7 @@ def is_ad_fragment_end(s):
|
||||||
'frag_index': frag_index,
|
'frag_index': frag_index,
|
||||||
'url': frag_url,
|
'url': frag_url,
|
||||||
'decrypt_info': decrypt_info,
|
'decrypt_info': decrypt_info,
|
||||||
'byte_range': byte_range,
|
'byte_range': map_byte_range,
|
||||||
'media_sequence': media_sequence,
|
'media_sequence': media_sequence,
|
||||||
})
|
})
|
||||||
media_sequence += 1
|
media_sequence += 1
|
||||||
|
@ -257,7 +278,7 @@ def is_ad_fragment_end(s):
|
||||||
media_sequence = int(line[22:])
|
media_sequence = int(line[22:])
|
||||||
elif line.startswith('#EXT-X-BYTERANGE'):
|
elif line.startswith('#EXT-X-BYTERANGE'):
|
||||||
splitted_byte_range = line[17:].split('@')
|
splitted_byte_range = line[17:].split('@')
|
||||||
sub_range_start = int(splitted_byte_range[1]) if len(splitted_byte_range) == 2 else byte_range['end']
|
sub_range_start = int(splitted_byte_range[1]) if len(splitted_byte_range) == 2 else byte_range_offset
|
||||||
byte_range = {
|
byte_range = {
|
||||||
'start': sub_range_start,
|
'start': sub_range_start,
|
||||||
'end': sub_range_start + int(splitted_byte_range[0]),
|
'end': sub_range_start + int(splitted_byte_range[0]),
|
||||||
|
|
|
@ -508,6 +508,7 @@
|
||||||
from .dhm import DHMIE
|
from .dhm import DHMIE
|
||||||
from .digitalconcerthall import DigitalConcertHallIE
|
from .digitalconcerthall import DigitalConcertHallIE
|
||||||
from .digiteka import DigitekaIE
|
from .digiteka import DigitekaIE
|
||||||
|
from .digiview import DigiviewIE
|
||||||
from .discogs import DiscogsReleasePlaylistIE
|
from .discogs import DiscogsReleasePlaylistIE
|
||||||
from .disney import DisneyIE
|
from .disney import DisneyIE
|
||||||
from .dispeak import DigitallySpeakingIE
|
from .dispeak import DigitallySpeakingIE
|
||||||
|
|
|
@ -14,16 +14,18 @@
|
||||||
js_to_json,
|
js_to_json,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
orderedSet,
|
orderedSet,
|
||||||
|
parse_age_limit,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
replace_extension,
|
replace_extension,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
strip_or_none,
|
strip_or_none,
|
||||||
traverse_obj,
|
|
||||||
try_get,
|
try_get,
|
||||||
|
unified_timestamp,
|
||||||
update_url,
|
update_url,
|
||||||
url_basename,
|
url_basename,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
from ..utils.traversal import require, traverse_obj, trim_str
|
||||||
|
|
||||||
|
|
||||||
class CBCIE(InfoExtractor):
|
class CBCIE(InfoExtractor):
|
||||||
|
@ -516,9 +518,43 @@ def entries():
|
||||||
return self.playlist_result(entries(), playlist_id)
|
return self.playlist_result(entries(), playlist_id)
|
||||||
|
|
||||||
|
|
||||||
class CBCGemIE(InfoExtractor):
|
class CBCGemBaseIE(InfoExtractor):
|
||||||
|
_NETRC_MACHINE = 'cbcgem'
|
||||||
|
_GEO_COUNTRIES = ['CA']
|
||||||
|
|
||||||
|
def _call_show_api(self, item_id, display_id=None):
|
||||||
|
return self._download_json(
|
||||||
|
f'https://services.radio-canada.ca/ott/catalog/v2/gem/show/{item_id}',
|
||||||
|
display_id or item_id, query={'device': 'web'})
|
||||||
|
|
||||||
|
def _extract_item_info(self, item_info):
|
||||||
|
episode_number = None
|
||||||
|
title = traverse_obj(item_info, ('title', {str}))
|
||||||
|
if title and (mobj := re.match(r'(?P<episode>\d+)\. (?P<title>.+)', title)):
|
||||||
|
episode_number = int_or_none(mobj.group('episode'))
|
||||||
|
title = mobj.group('title')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'episode_number': episode_number,
|
||||||
|
**traverse_obj(item_info, {
|
||||||
|
'id': ('url', {str}),
|
||||||
|
'episode_id': ('url', {str}),
|
||||||
|
'description': ('description', {str}),
|
||||||
|
'thumbnail': ('images', 'card', 'url', {url_or_none}, {update_url(query=None)}),
|
||||||
|
'episode_number': ('episodeNumber', {int_or_none}),
|
||||||
|
'duration': ('metadata', 'duration', {int_or_none}),
|
||||||
|
'release_timestamp': ('metadata', 'airDate', {unified_timestamp}),
|
||||||
|
'timestamp': ('metadata', 'availabilityDate', {unified_timestamp}),
|
||||||
|
'age_limit': ('metadata', 'rating', {trim_str(start='C')}, {parse_age_limit}),
|
||||||
|
}),
|
||||||
|
'episode': title,
|
||||||
|
'title': title,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CBCGemIE(CBCGemBaseIE):
|
||||||
IE_NAME = 'gem.cbc.ca'
|
IE_NAME = 'gem.cbc.ca'
|
||||||
_VALID_URL = r'https?://gem\.cbc\.ca/(?:media/)?(?P<id>[0-9a-z-]+/s[0-9]+[a-z][0-9]+)'
|
_VALID_URL = r'https?://gem\.cbc\.ca/(?:media/)?(?P<id>[0-9a-z-]+/s(?P<season>[0-9]+)[a-z][0-9]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# This is a normal, public, TV show video
|
# This is a normal, public, TV show video
|
||||||
'url': 'https://gem.cbc.ca/media/schitts-creek/s06e01',
|
'url': 'https://gem.cbc.ca/media/schitts-creek/s06e01',
|
||||||
|
@ -529,7 +565,7 @@ class CBCGemIE(InfoExtractor):
|
||||||
'description': 'md5:929868d20021c924020641769eb3e7f1',
|
'description': 'md5:929868d20021c924020641769eb3e7f1',
|
||||||
'thumbnail': r're:https://images\.radio-canada\.ca/[^#?]+/cbc_schitts_creek_season_06e01_thumbnail_v01\.jpg',
|
'thumbnail': r're:https://images\.radio-canada\.ca/[^#?]+/cbc_schitts_creek_season_06e01_thumbnail_v01\.jpg',
|
||||||
'duration': 1324,
|
'duration': 1324,
|
||||||
'categories': ['comedy'],
|
'genres': ['Comédie et humour'],
|
||||||
'series': 'Schitt\'s Creek',
|
'series': 'Schitt\'s Creek',
|
||||||
'season': 'Season 6',
|
'season': 'Season 6',
|
||||||
'season_number': 6,
|
'season_number': 6,
|
||||||
|
@ -537,9 +573,10 @@ class CBCGemIE(InfoExtractor):
|
||||||
'episode_number': 1,
|
'episode_number': 1,
|
||||||
'episode_id': 'schitts-creek/s06e01',
|
'episode_id': 'schitts-creek/s06e01',
|
||||||
'upload_date': '20210618',
|
'upload_date': '20210618',
|
||||||
'timestamp': 1623988800,
|
'timestamp': 1623974400,
|
||||||
'release_date': '20200107',
|
'release_date': '20200107',
|
||||||
'release_timestamp': 1578427200,
|
'release_timestamp': 1578355200,
|
||||||
|
'age_limit': 14,
|
||||||
},
|
},
|
||||||
'params': {'format': 'bv'},
|
'params': {'format': 'bv'},
|
||||||
}, {
|
}, {
|
||||||
|
@ -557,12 +594,13 @@ class CBCGemIE(InfoExtractor):
|
||||||
'episode_number': 1,
|
'episode_number': 1,
|
||||||
'episode': 'The Cup Runneth Over',
|
'episode': 'The Cup Runneth Over',
|
||||||
'episode_id': 'schitts-creek/s01e01',
|
'episode_id': 'schitts-creek/s01e01',
|
||||||
'duration': 1309,
|
'duration': 1308,
|
||||||
'categories': ['comedy'],
|
'genres': ['Comédie et humour'],
|
||||||
'upload_date': '20210617',
|
'upload_date': '20210617',
|
||||||
'timestamp': 1623902400,
|
'timestamp': 1623888000,
|
||||||
'release_date': '20151124',
|
'release_date': '20151123',
|
||||||
'release_timestamp': 1448323200,
|
'release_timestamp': 1448236800,
|
||||||
|
'age_limit': 14,
|
||||||
},
|
},
|
||||||
'params': {'format': 'bv'},
|
'params': {'format': 'bv'},
|
||||||
}, {
|
}, {
|
||||||
|
@ -570,9 +608,7 @@ class CBCGemIE(InfoExtractor):
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_GEO_COUNTRIES = ['CA']
|
|
||||||
_TOKEN_API_KEY = '3f4beddd-2061-49b0-ae80-6f1f2ed65b37'
|
_TOKEN_API_KEY = '3f4beddd-2061-49b0-ae80-6f1f2ed65b37'
|
||||||
_NETRC_MACHINE = 'cbcgem'
|
|
||||||
_claims_token = None
|
_claims_token = None
|
||||||
|
|
||||||
def _new_claims_token(self, email, password):
|
def _new_claims_token(self, email, password):
|
||||||
|
@ -634,10 +670,12 @@ def _real_initialize(self):
|
||||||
self._claims_token = self.cache.load(self._NETRC_MACHINE, 'claims_token')
|
self._claims_token = self.cache.load(self._NETRC_MACHINE, 'claims_token')
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id, season_number = self._match_valid_url(url).group('id', 'season')
|
||||||
video_info = self._download_json(
|
video_info = self._call_show_api(video_id)
|
||||||
f'https://services.radio-canada.ca/ott/cbc-api/v2/assets/{video_id}',
|
item_info = traverse_obj(video_info, (
|
||||||
video_id, expected_status=426)
|
'content', ..., 'lineups', ..., 'items',
|
||||||
|
lambda _, v: v['url'] == video_id, any, {require('item info')}))
|
||||||
|
media_id = item_info['idMedia']
|
||||||
|
|
||||||
email, password = self._get_login_info()
|
email, password = self._get_login_info()
|
||||||
if email and password:
|
if email and password:
|
||||||
|
@ -645,7 +683,20 @@ def _real_extract(self, url):
|
||||||
headers = {'x-claims-token': claims_token}
|
headers = {'x-claims-token': claims_token}
|
||||||
else:
|
else:
|
||||||
headers = {}
|
headers = {}
|
||||||
m3u8_info = self._download_json(video_info['playSession']['url'], video_id, headers=headers)
|
|
||||||
|
m3u8_info = self._download_json(
|
||||||
|
'https://services.radio-canada.ca/media/validation/v2/',
|
||||||
|
video_id, headers=headers, query={
|
||||||
|
'appCode': 'gem',
|
||||||
|
'connectionType': 'hd',
|
||||||
|
'deviceType': 'ipad',
|
||||||
|
'multibitrate': 'true',
|
||||||
|
'output': 'json',
|
||||||
|
'tech': 'hls',
|
||||||
|
'manifestVersion': '2',
|
||||||
|
'manifestType': 'desktop',
|
||||||
|
'idMedia': media_id,
|
||||||
|
})
|
||||||
|
|
||||||
if m3u8_info.get('errorCode') == 1:
|
if m3u8_info.get('errorCode') == 1:
|
||||||
self.raise_geo_restricted(countries=['CA'])
|
self.raise_geo_restricted(countries=['CA'])
|
||||||
|
@ -671,26 +722,20 @@ def _real_extract(self, url):
|
||||||
fmt['preference'] = -2
|
fmt['preference'] = -2
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
'season_number': int_or_none(season_number),
|
||||||
|
**traverse_obj(video_info, {
|
||||||
|
'series': ('title', {str}),
|
||||||
|
'season_number': ('structuredMetadata', 'partofSeason', 'seasonNumber', {int_or_none}),
|
||||||
|
'genres': ('structuredMetadata', 'genre', ..., {str}),
|
||||||
|
}),
|
||||||
|
**self._extract_item_info(item_info),
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'episode_id': video_id,
|
'episode_id': video_id,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
**traverse_obj(video_info, {
|
|
||||||
'title': ('title', {str}),
|
|
||||||
'episode': ('title', {str}),
|
|
||||||
'description': ('description', {str}),
|
|
||||||
'thumbnail': ('image', {url_or_none}),
|
|
||||||
'series': ('series', {str}),
|
|
||||||
'season_number': ('season', {int_or_none}),
|
|
||||||
'episode_number': ('episode', {int_or_none}),
|
|
||||||
'duration': ('duration', {int_or_none}),
|
|
||||||
'categories': ('category', {str}, all),
|
|
||||||
'release_timestamp': ('airDate', {int_or_none(scale=1000)}),
|
|
||||||
'timestamp': ('availableDate', {int_or_none(scale=1000)}),
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CBCGemPlaylistIE(InfoExtractor):
|
class CBCGemPlaylistIE(CBCGemBaseIE):
|
||||||
IE_NAME = 'gem.cbc.ca:playlist'
|
IE_NAME = 'gem.cbc.ca:playlist'
|
||||||
_VALID_URL = r'https?://gem\.cbc\.ca/(?:media/)?(?P<id>(?P<show>[0-9a-z-]+)/s(?P<season>[0-9]+))/?(?:[?#]|$)'
|
_VALID_URL = r'https?://gem\.cbc\.ca/(?:media/)?(?P<id>(?P<show>[0-9a-z-]+)/s(?P<season>[0-9]+))/?(?:[?#]|$)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
@ -700,70 +745,35 @@ class CBCGemPlaylistIE(InfoExtractor):
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'schitts-creek/s06',
|
'id': 'schitts-creek/s06',
|
||||||
'title': 'Season 6',
|
'title': 'Season 6',
|
||||||
'description': 'md5:6a92104a56cbeb5818cc47884d4326a2',
|
|
||||||
'series': 'Schitt\'s Creek',
|
'series': 'Schitt\'s Creek',
|
||||||
'season_number': 6,
|
'season_number': 6,
|
||||||
'season': 'Season 6',
|
'season': 'Season 6',
|
||||||
'thumbnail': 'https://images.radio-canada.ca/v1/synps-cbc/season/perso/cbc_schitts_creek_season_06_carousel_v03.jpg?impolicy=ott&im=Resize=(_Size_)&quality=75',
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://gem.cbc.ca/schitts-creek/s06',
|
'url': 'https://gem.cbc.ca/schitts-creek/s06',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
_API_BASE = 'https://services.radio-canada.ca/ott/cbc-api/v2/shows/'
|
|
||||||
|
def _entries(self, season_info):
|
||||||
|
for episode in traverse_obj(season_info, ('items', lambda _, v: v['url'])):
|
||||||
|
yield self.url_result(
|
||||||
|
f'https://gem.cbc.ca/media/{episode["url"]}', CBCGemIE,
|
||||||
|
**self._extract_item_info(episode))
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
match = self._match_valid_url(url)
|
season_id, show, season = self._match_valid_url(url).group('id', 'show', 'season')
|
||||||
season_id = match.group('id')
|
show_info = self._call_show_api(show, display_id=season_id)
|
||||||
show = match.group('show')
|
season_info = traverse_obj(show_info, (
|
||||||
show_info = self._download_json(self._API_BASE + show, season_id, expected_status=426)
|
'content', ..., 'lineups',
|
||||||
season = int(match.group('season'))
|
lambda _, v: v['seasonNumber'] == int(season), any, {require('season info')}))
|
||||||
|
|
||||||
season_info = next((s for s in show_info['seasons'] if s.get('season') == season), None)
|
return self.playlist_result(
|
||||||
|
self._entries(season_info), season_id,
|
||||||
if season_info is None:
|
**traverse_obj(season_info, {
|
||||||
raise ExtractorError(f'Couldn\'t find season {season} of {show}')
|
'title': ('title', {str}),
|
||||||
|
'season': ('title', {str}),
|
||||||
episodes = []
|
'season_number': ('seasonNumber', {int_or_none}),
|
||||||
for episode in season_info['assets']:
|
}), series=traverse_obj(show_info, ('title', {str})))
|
||||||
episodes.append({
|
|
||||||
'_type': 'url_transparent',
|
|
||||||
'ie_key': 'CBCGem',
|
|
||||||
'url': 'https://gem.cbc.ca/media/' + episode['id'],
|
|
||||||
'id': episode['id'],
|
|
||||||
'title': episode.get('title'),
|
|
||||||
'description': episode.get('description'),
|
|
||||||
'thumbnail': episode.get('image'),
|
|
||||||
'series': episode.get('series'),
|
|
||||||
'season_number': episode.get('season'),
|
|
||||||
'season': season_info['title'],
|
|
||||||
'season_id': season_info.get('id'),
|
|
||||||
'episode_number': episode.get('episode'),
|
|
||||||
'episode': episode.get('title'),
|
|
||||||
'episode_id': episode['id'],
|
|
||||||
'duration': episode.get('duration'),
|
|
||||||
'categories': [episode.get('category')],
|
|
||||||
})
|
|
||||||
|
|
||||||
thumbnail = None
|
|
||||||
tn_uri = season_info.get('image')
|
|
||||||
# the-national was observed to use a "data:image/png;base64"
|
|
||||||
# URI for their 'image' value. The image was 1x1, and is
|
|
||||||
# probably just a placeholder, so it is ignored.
|
|
||||||
if tn_uri is not None and not tn_uri.startswith('data:'):
|
|
||||||
thumbnail = tn_uri
|
|
||||||
|
|
||||||
return {
|
|
||||||
'_type': 'playlist',
|
|
||||||
'entries': episodes,
|
|
||||||
'id': season_id,
|
|
||||||
'title': season_info['title'],
|
|
||||||
'description': season_info.get('description'),
|
|
||||||
'thumbnail': thumbnail,
|
|
||||||
'series': show_info.get('title'),
|
|
||||||
'season_number': season_info.get('season'),
|
|
||||||
'season': season_info['title'],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CBCGemLiveIE(InfoExtractor):
|
class CBCGemLiveIE(InfoExtractor):
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import collections
|
import collections
|
||||||
import functools
|
import functools
|
||||||
import getpass
|
import getpass
|
||||||
import hashlib
|
|
||||||
import http.client
|
import http.client
|
||||||
import http.cookiejar
|
import http.cookiejar
|
||||||
import http.cookies
|
import http.cookies
|
||||||
|
@ -79,7 +78,6 @@
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
parse_m3u8_attributes,
|
parse_m3u8_attributes,
|
||||||
parse_resolution,
|
parse_resolution,
|
||||||
sanitize_filename,
|
|
||||||
sanitize_url,
|
sanitize_url,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
|
@ -101,6 +99,7 @@
|
||||||
xpath_text,
|
xpath_text,
|
||||||
xpath_with_ns,
|
xpath_with_ns,
|
||||||
)
|
)
|
||||||
|
from ..utils._utils import _request_dump_filename
|
||||||
|
|
||||||
|
|
||||||
class InfoExtractor:
|
class InfoExtractor:
|
||||||
|
@ -202,6 +201,11 @@ class InfoExtractor:
|
||||||
fragment_base_url
|
fragment_base_url
|
||||||
* "duration" (optional, int or float)
|
* "duration" (optional, int or float)
|
||||||
* "filesize" (optional, int)
|
* "filesize" (optional, int)
|
||||||
|
* hls_media_playlist_data
|
||||||
|
The M3U8 media playlist data as a string.
|
||||||
|
Only use if the data must be modified during extraction and
|
||||||
|
the native HLS downloader should bypass requesting the URL.
|
||||||
|
Does not apply if ffmpeg is used as external downloader
|
||||||
* is_from_start Is a live format that can be downloaded
|
* is_from_start Is a live format that can be downloaded
|
||||||
from the start. Boolean
|
from the start. Boolean
|
||||||
* preference Order number of this format. If this field is
|
* preference Order number of this format. If this field is
|
||||||
|
@ -1018,23 +1022,6 @@ def __check_blocked(self, content):
|
||||||
'Visit http://blocklist.rkn.gov.ru/ for a block reason.',
|
'Visit http://blocklist.rkn.gov.ru/ for a block reason.',
|
||||||
expected=True)
|
expected=True)
|
||||||
|
|
||||||
def _request_dump_filename(self, url, video_id, data=None):
|
|
||||||
if data is not None:
|
|
||||||
data = hashlib.md5(data).hexdigest()
|
|
||||||
basen = join_nonempty(video_id, data, url, delim='_')
|
|
||||||
trim_length = self.get_param('trim_file_name') or 240
|
|
||||||
if len(basen) > trim_length:
|
|
||||||
h = '___' + hashlib.md5(basen.encode()).hexdigest()
|
|
||||||
basen = basen[:trim_length - len(h)] + h
|
|
||||||
filename = sanitize_filename(f'{basen}.dump', restricted=True)
|
|
||||||
# Working around MAX_PATH limitation on Windows (see
|
|
||||||
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx)
|
|
||||||
if os.name == 'nt':
|
|
||||||
absfilepath = os.path.abspath(filename)
|
|
||||||
if len(absfilepath) > 259:
|
|
||||||
filename = fR'\\?\{absfilepath}'
|
|
||||||
return filename
|
|
||||||
|
|
||||||
def __decode_webpage(self, webpage_bytes, encoding, headers):
|
def __decode_webpage(self, webpage_bytes, encoding, headers):
|
||||||
if not encoding:
|
if not encoding:
|
||||||
encoding = self._guess_encoding_from_content(headers.get('Content-Type', ''), webpage_bytes)
|
encoding = self._guess_encoding_from_content(headers.get('Content-Type', ''), webpage_bytes)
|
||||||
|
@ -1063,7 +1050,9 @@ def _webpage_read_content(self, urlh, url_or_request, video_id, note=None, errno
|
||||||
if self.get_param('write_pages'):
|
if self.get_param('write_pages'):
|
||||||
if isinstance(url_or_request, Request):
|
if isinstance(url_or_request, Request):
|
||||||
data = self._create_request(url_or_request, data).data
|
data = self._create_request(url_or_request, data).data
|
||||||
filename = self._request_dump_filename(urlh.url, video_id, data)
|
filename = _request_dump_filename(
|
||||||
|
urlh.url, video_id, data,
|
||||||
|
trim_length=self.get_param('trim_file_name'))
|
||||||
self.to_screen(f'Saving request to {filename}')
|
self.to_screen(f'Saving request to {filename}')
|
||||||
with open(filename, 'wb') as outf:
|
with open(filename, 'wb') as outf:
|
||||||
outf.write(webpage_bytes)
|
outf.write(webpage_bytes)
|
||||||
|
@ -1124,7 +1113,9 @@ def download_content(self, url_or_request, video_id, note=note, errnote=errnote,
|
||||||
impersonate=None, require_impersonation=False):
|
impersonate=None, require_impersonation=False):
|
||||||
if self.get_param('load_pages'):
|
if self.get_param('load_pages'):
|
||||||
url_or_request = self._create_request(url_or_request, data, headers, query)
|
url_or_request = self._create_request(url_or_request, data, headers, query)
|
||||||
filename = self._request_dump_filename(url_or_request.url, video_id, url_or_request.data)
|
filename = _request_dump_filename(
|
||||||
|
url_or_request.url, video_id, url_or_request.data,
|
||||||
|
trim_length=self.get_param('trim_file_name'))
|
||||||
self.to_screen(f'Loading request from {filename}')
|
self.to_screen(f'Loading request from {filename}')
|
||||||
try:
|
try:
|
||||||
with open(filename, 'rb') as dumpf:
|
with open(filename, 'rb') as dumpf:
|
||||||
|
|
130
yt_dlp/extractor/digiview.py
Normal file
130
yt_dlp/extractor/digiview.py
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from .youtube import YoutubeIE
|
||||||
|
from ..utils import clean_html, int_or_none, traverse_obj, url_or_none, urlencode_postdata
|
||||||
|
|
||||||
|
|
||||||
|
class DigiviewIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?ladigitale\.dev/digiview/#/v/(?P<id>[0-9a-f]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
# normal video
|
||||||
|
'url': 'https://ladigitale.dev/digiview/#/v/67a8e50aee2ec',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '67a8e50aee2ec',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Big Buck Bunny 60fps 4K - Official Blender Foundation Short Film',
|
||||||
|
'thumbnail': 'https://i.ytimg.com/vi/aqz-KE-bpKQ/hqdefault.jpg',
|
||||||
|
'upload_date': '20141110',
|
||||||
|
'playable_in_embed': True,
|
||||||
|
'duration': 635,
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'channel': 'Blender',
|
||||||
|
'license': 'Creative Commons Attribution license (reuse allowed)',
|
||||||
|
'like_count': int,
|
||||||
|
'tags': 'count:8',
|
||||||
|
'live_status': 'not_live',
|
||||||
|
'channel_id': 'UCSMOQeBJ2RAnuFungnQOxLg',
|
||||||
|
'channel_follower_count': int,
|
||||||
|
'channel_url': 'https://www.youtube.com/channel/UCSMOQeBJ2RAnuFungnQOxLg',
|
||||||
|
'uploader_id': '@BlenderOfficial',
|
||||||
|
'description': 'md5:8f3ed18a53a1bb36cbb3b70a15782fd0',
|
||||||
|
'categories': ['Film & Animation'],
|
||||||
|
'channel_is_verified': True,
|
||||||
|
'heatmap': 'count:100',
|
||||||
|
'section_end': 635,
|
||||||
|
'uploader': 'Blender',
|
||||||
|
'timestamp': 1415628355,
|
||||||
|
'uploader_url': 'https://www.youtube.com/@BlenderOfficial',
|
||||||
|
'age_limit': 0,
|
||||||
|
'section_start': 0,
|
||||||
|
'availability': 'public',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# cut video
|
||||||
|
'url': 'https://ladigitale.dev/digiview/#/v/67a8e51d0dd58',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '67a8e51d0dd58',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Big Buck Bunny 60fps 4K - Official Blender Foundation Short Film',
|
||||||
|
'thumbnail': 'https://i.ytimg.com/vi/aqz-KE-bpKQ/hqdefault.jpg',
|
||||||
|
'upload_date': '20141110',
|
||||||
|
'playable_in_embed': True,
|
||||||
|
'duration': 5,
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'channel': 'Blender',
|
||||||
|
'license': 'Creative Commons Attribution license (reuse allowed)',
|
||||||
|
'like_count': int,
|
||||||
|
'tags': 'count:8',
|
||||||
|
'live_status': 'not_live',
|
||||||
|
'channel_id': 'UCSMOQeBJ2RAnuFungnQOxLg',
|
||||||
|
'channel_follower_count': int,
|
||||||
|
'channel_url': 'https://www.youtube.com/channel/UCSMOQeBJ2RAnuFungnQOxLg',
|
||||||
|
'uploader_id': '@BlenderOfficial',
|
||||||
|
'description': 'md5:8f3ed18a53a1bb36cbb3b70a15782fd0',
|
||||||
|
'categories': ['Film & Animation'],
|
||||||
|
'channel_is_verified': True,
|
||||||
|
'heatmap': 'count:100',
|
||||||
|
'section_end': 10,
|
||||||
|
'uploader': 'Blender',
|
||||||
|
'timestamp': 1415628355,
|
||||||
|
'uploader_url': 'https://www.youtube.com/@BlenderOfficial',
|
||||||
|
'age_limit': 0,
|
||||||
|
'section_start': 5,
|
||||||
|
'availability': 'public',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# changed title
|
||||||
|
'url': 'https://ladigitale.dev/digiview/#/v/67a8ea5644d7a',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '67a8ea5644d7a',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Big Buck Bunny (with title changed)',
|
||||||
|
'thumbnail': 'https://i.ytimg.com/vi/aqz-KE-bpKQ/hqdefault.jpg',
|
||||||
|
'upload_date': '20141110',
|
||||||
|
'playable_in_embed': True,
|
||||||
|
'duration': 5,
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'channel': 'Blender',
|
||||||
|
'license': 'Creative Commons Attribution license (reuse allowed)',
|
||||||
|
'like_count': int,
|
||||||
|
'tags': 'count:8',
|
||||||
|
'live_status': 'not_live',
|
||||||
|
'channel_id': 'UCSMOQeBJ2RAnuFungnQOxLg',
|
||||||
|
'channel_follower_count': int,
|
||||||
|
'channel_url': 'https://www.youtube.com/channel/UCSMOQeBJ2RAnuFungnQOxLg',
|
||||||
|
'uploader_id': '@BlenderOfficial',
|
||||||
|
'description': 'md5:8f3ed18a53a1bb36cbb3b70a15782fd0',
|
||||||
|
'categories': ['Film & Animation'],
|
||||||
|
'channel_is_verified': True,
|
||||||
|
'heatmap': 'count:100',
|
||||||
|
'section_end': 15,
|
||||||
|
'uploader': 'Blender',
|
||||||
|
'timestamp': 1415628355,
|
||||||
|
'uploader_url': 'https://www.youtube.com/@BlenderOfficial',
|
||||||
|
'age_limit': 0,
|
||||||
|
'section_start': 10,
|
||||||
|
'availability': 'public',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
video_data = self._download_json(
|
||||||
|
'https://ladigitale.dev/digiview/inc/recuperer_video.php', video_id,
|
||||||
|
data=urlencode_postdata({'id': video_id}))
|
||||||
|
|
||||||
|
clip_id = video_data['videoId']
|
||||||
|
return self.url_result(
|
||||||
|
f'https://www.youtube.com/watch?v={clip_id}',
|
||||||
|
YoutubeIE, video_id, url_transparent=True,
|
||||||
|
**traverse_obj(video_data, {
|
||||||
|
'section_start': ('debut', {int_or_none}),
|
||||||
|
'section_end': ('fin', {int_or_none}),
|
||||||
|
'description': ('description', {clean_html}, filter),
|
||||||
|
'title': ('titre', {str}),
|
||||||
|
'thumbnail': ('vignette', {url_or_none}),
|
||||||
|
'view_count': ('vues', {int_or_none}),
|
||||||
|
}),
|
||||||
|
)
|
|
@ -1,10 +1,24 @@
|
||||||
from .zdf import ZDFIE
|
from .zdf import ZDFBaseIE
|
||||||
|
|
||||||
|
|
||||||
class DreiSatIE(ZDFIE): # XXX: Do not subclass from concrete IE
|
class DreiSatIE(ZDFBaseIE):
|
||||||
IE_NAME = '3sat'
|
IE_NAME = '3sat'
|
||||||
_VALID_URL = r'https?://(?:www\.)?3sat\.de/(?:[^/]+/)*(?P<id>[^/?#&]+)\.html'
|
_VALID_URL = r'https?://(?:www\.)?3sat\.de/(?:[^/]+/)*(?P<id>[^/?#&]+)\.html'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://www.3sat.de/dokumentation/reise/traumziele-suedostasiens-die-philippinen-und-vietnam-102.html',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '231124_traumziele_philippinen_und_vietnam_dokreise',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Traumziele Südostasiens (1/2): Die Philippinen und Vietnam',
|
||||||
|
'description': 'md5:26329ce5197775b596773b939354079d',
|
||||||
|
'duration': 2625.0,
|
||||||
|
'thumbnail': 'https://www.3sat.de/assets/traumziele-suedostasiens-die-philippinen-und-vietnam-100~2400x1350?cb=1699870351148',
|
||||||
|
'episode': 'Traumziele Südostasiens (1/2): Die Philippinen und Vietnam',
|
||||||
|
'episode_id': 'POS_cc7ff51c-98cf-4d12-b99d-f7a551de1c95',
|
||||||
|
'timestamp': 1738593000,
|
||||||
|
'upload_date': '20250203',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
# Same as https://www.zdf.de/dokumentation/ab-18/10-wochen-sommer-102.html
|
# Same as https://www.zdf.de/dokumentation/ab-18/10-wochen-sommer-102.html
|
||||||
'url': 'https://www.3sat.de/film/ab-18/10-wochen-sommer-108.html',
|
'url': 'https://www.3sat.de/film/ab-18/10-wochen-sommer-108.html',
|
||||||
'md5': '0aff3e7bc72c8813f5e0fae333316a1d',
|
'md5': '0aff3e7bc72c8813f5e0fae333316a1d',
|
||||||
|
@ -17,6 +31,7 @@ class DreiSatIE(ZDFIE): # XXX: Do not subclass from concrete IE
|
||||||
'timestamp': 1608604200,
|
'timestamp': 1608604200,
|
||||||
'upload_date': '20201222',
|
'upload_date': '20201222',
|
||||||
},
|
},
|
||||||
|
'skip': '410 Gone',
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.3sat.de/gesellschaft/schweizweit/waidmannsheil-100.html',
|
'url': 'https://www.3sat.de/gesellschaft/schweizweit/waidmannsheil-100.html',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
@ -30,6 +45,7 @@ class DreiSatIE(ZDFIE): # XXX: Do not subclass from concrete IE
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
'skip': '404 Not Found',
|
||||||
}, {
|
}, {
|
||||||
# Same as https://www.zdf.de/filme/filme-sonstige/der-hauptmann-112.html
|
# Same as https://www.zdf.de/filme/filme-sonstige/der-hauptmann-112.html
|
||||||
'url': 'https://www.3sat.de/film/spielfilm/der-hauptmann-100.html',
|
'url': 'https://www.3sat.de/film/spielfilm/der-hauptmann-100.html',
|
||||||
|
@ -39,3 +55,14 @@ class DreiSatIE(ZDFIE): # XXX: Do not subclass from concrete IE
|
||||||
'url': 'https://www.3sat.de/wissen/nano/nano-21-mai-2019-102.html',
|
'url': 'https://www.3sat.de/wissen/nano/nano-21-mai-2019-102.html',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id, fatal=False)
|
||||||
|
if webpage:
|
||||||
|
player = self._extract_player(webpage, url, fatal=False)
|
||||||
|
if player:
|
||||||
|
return self._extract_regular(url, player, video_id)
|
||||||
|
|
||||||
|
return self._extract_mobile(video_id)
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
clean_html,
|
clean_html,
|
||||||
determine_ext,
|
determine_ext,
|
||||||
|
extract_attributes,
|
||||||
filter_dict,
|
filter_dict,
|
||||||
format_field,
|
format_field,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
@ -18,7 +19,7 @@
|
||||||
unsmuggle_url,
|
unsmuggle_url,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
from ..utils.traversal import traverse_obj
|
from ..utils.traversal import find_element, traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class FranceTVBaseInfoExtractor(InfoExtractor):
|
class FranceTVBaseInfoExtractor(InfoExtractor):
|
||||||
|
@ -358,7 +359,8 @@ def _real_extract(self, url):
|
||||||
# For livestreams we need the id of the stream instead of the currently airing episode id
|
# For livestreams we need the id of the stream instead of the currently airing episode id
|
||||||
video_id = traverse_obj(nextjs_data, (
|
video_id = traverse_obj(nextjs_data, (
|
||||||
..., ..., 'children', ..., 'children', ..., 'children', ..., 'children', ..., ...,
|
..., ..., 'children', ..., 'children', ..., 'children', ..., 'children', ..., ...,
|
||||||
'children', ..., ..., 'children', ..., ..., 'children', ..., 'options', 'id', {str}, any))
|
'children', ..., ..., 'children', ..., ..., 'children', (..., (..., ...)),
|
||||||
|
'options', 'id', {str}, any))
|
||||||
else:
|
else:
|
||||||
video_id = traverse_obj(nextjs_data, (
|
video_id = traverse_obj(nextjs_data, (
|
||||||
..., ..., ..., 'children',
|
..., ..., ..., 'children',
|
||||||
|
@ -459,11 +461,16 @@ def _real_extract(self, url):
|
||||||
self.url_result(dailymotion_url, DailymotionIE.ie_key())
|
self.url_result(dailymotion_url, DailymotionIE.ie_key())
|
||||||
for dailymotion_url in dailymotion_urls])
|
for dailymotion_url in dailymotion_urls])
|
||||||
|
|
||||||
video_id = self._search_regex(
|
video_id = (
|
||||||
(r'player\.load[^;]+src:\s*["\']([^"\']+)',
|
traverse_obj(webpage, (
|
||||||
r'id-video=([^@]+@[^"]+)',
|
{find_element(tag='button', attr='data-cy', value='francetv-player-wrapper', html=True)},
|
||||||
r'<a[^>]+href="(?:https?:)?//videos\.francetv\.fr/video/([^@]+@[^"]+)"',
|
{extract_attributes}, 'id'))
|
||||||
r'(?:data-id|<figure[^<]+\bid)=["\']([\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})'),
|
or self._search_regex(
|
||||||
webpage, 'video id')
|
(r'player\.load[^;]+src:\s*["\']([^"\']+)',
|
||||||
|
r'id-video=([^@]+@[^"]+)',
|
||||||
|
r'<a[^>]+href="(?:https?:)?//videos\.francetv\.fr/video/([^@]+@[^"]+)"',
|
||||||
|
r'(?:data-id|<figure[^<]+\bid)=["\']([\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})'),
|
||||||
|
webpage, 'video id')
|
||||||
|
)
|
||||||
|
|
||||||
return self._make_url_result(video_id, url=url)
|
return self._make_url_result(video_id, url=url)
|
||||||
|
|
|
@ -293,6 +293,19 @@ class GenericIE(InfoExtractor):
|
||||||
'timestamp': 1378272859.0,
|
'timestamp': 1378272859.0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
# Live DASH MPD
|
||||||
|
{
|
||||||
|
'url': 'https://livesim2.dashif.org/livesim2/ato_10/testpic_2s/Manifest.mpd',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'Manifest',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': r're:Manifest \d{4}-\d{2}-\d{2} \d{2}:\d{2}$',
|
||||||
|
'live_status': 'is_live',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': 'livestream',
|
||||||
|
},
|
||||||
|
},
|
||||||
# m3u8 served with Content-Type: audio/x-mpegURL; charset=utf-8
|
# m3u8 served with Content-Type: audio/x-mpegURL; charset=utf-8
|
||||||
{
|
{
|
||||||
'url': 'http://once.unicornmedia.com/now/master/playlist/bb0b18ba-64f5-4b1b-a29f-0ac252f06b68/77a785f3-5188-4806-b788-0893a61634ed/93677179-2d99-4ef4-9e17-fe70d49abfbf/content.m3u8',
|
'url': 'http://once.unicornmedia.com/now/master/playlist/bb0b18ba-64f5-4b1b-a29f-0ac252f06b68/77a785f3-5188-4806-b788-0893a61634ed/93677179-2d99-4ef4-9e17-fe70d49abfbf/content.m3u8',
|
||||||
|
@ -2436,10 +2449,9 @@ def _real_extract(self, url):
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
if format_id.endswith('mpegurl') or ext == 'm3u8':
|
if format_id.endswith('mpegurl') or ext == 'm3u8':
|
||||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(url, video_id, 'mp4', headers=headers)
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(url, video_id, 'mp4', headers=headers)
|
||||||
elif format_id.endswith(('mpd', 'dash+xml')) or ext == 'mpd':
|
|
||||||
formats, subtitles = self._extract_mpd_formats_and_subtitles(url, video_id, headers=headers)
|
|
||||||
elif format_id == 'f4m' or ext == 'f4m':
|
elif format_id == 'f4m' or ext == 'f4m':
|
||||||
formats = self._extract_f4m_formats(url, video_id, headers=headers)
|
formats = self._extract_f4m_formats(url, video_id, headers=headers)
|
||||||
|
# Don't check for DASH/mpd here, do it later w/ first_bytes. Same number of requests either way
|
||||||
else:
|
else:
|
||||||
formats = [{
|
formats = [{
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
|
@ -2521,6 +2533,7 @@ def _real_extract(self, url):
|
||||||
doc,
|
doc,
|
||||||
mpd_base_url=full_response.url.rpartition('/')[0],
|
mpd_base_url=full_response.url.rpartition('/')[0],
|
||||||
mpd_url=url)
|
mpd_url=url)
|
||||||
|
info_dict['live_status'] = 'is_live' if doc.get('type') == 'dynamic' else None
|
||||||
self._extra_manifest_info(info_dict, url)
|
self._extra_manifest_info(info_dict, url)
|
||||||
self.report_detected('DASH manifest')
|
self.report_detected('DASH manifest')
|
||||||
return info_dict
|
return info_dict
|
||||||
|
|
|
@ -13,11 +13,13 @@
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
OnDemandPagedList,
|
OnDemandPagedList,
|
||||||
clean_html,
|
clean_html,
|
||||||
|
determine_ext,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
join_nonempty,
|
join_nonempty,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
|
parse_qs,
|
||||||
parse_resolution,
|
parse_resolution,
|
||||||
qualities,
|
qualities,
|
||||||
remove_start,
|
remove_start,
|
||||||
|
@ -1033,6 +1035,7 @@ def _real_extract(self, url):
|
||||||
thumbnails.append({
|
thumbnails.append({
|
||||||
'id': f'{name}_{width}x{height}',
|
'id': f'{name}_{width}x{height}',
|
||||||
'url': img_url,
|
'url': img_url,
|
||||||
|
'ext': traverse_obj(parse_qs(img_url), ('image', 0, {determine_ext(default_ext='jpg')})),
|
||||||
**res,
|
**res,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -501,7 +501,7 @@ def _extract_webpage(self, url):
|
||||||
r"div\s*:\s*'videoembed'\s*,\s*mediaid\s*:\s*'(\d+)'", # frontline video embed
|
r"div\s*:\s*'videoembed'\s*,\s*mediaid\s*:\s*'(\d+)'", # frontline video embed
|
||||||
r'class="coveplayerid">([^<]+)<', # coveplayer
|
r'class="coveplayerid">([^<]+)<', # coveplayer
|
||||||
r'<section[^>]+data-coveid="(\d+)"', # coveplayer from http://www.pbs.org/wgbh/frontline/film/real-csi/
|
r'<section[^>]+data-coveid="(\d+)"', # coveplayer from http://www.pbs.org/wgbh/frontline/film/real-csi/
|
||||||
r'\bclass="passportcoveplayer"[^>]+\bdata-media="(\d+)', # https://www.thirteen.org/programs/the-woodwrights-shop/who-wrote-the-book-of-sloyd-fggvvq/
|
r'\sclass="passportcoveplayer"[^>]*\sdata-media="(\d+)', # https://www.thirteen.org/programs/the-woodwrights-shop/who-wrote-the-book-of-sloyd-fggvvq/
|
||||||
r'<input type="hidden" id="pbs_video_id_[0-9]+" value="([0-9]+)"/>', # jwplayer
|
r'<input type="hidden" id="pbs_video_id_[0-9]+" value="([0-9]+)"/>', # jwplayer
|
||||||
r"(?s)window\.PBS\.playerConfig\s*=\s*{.*?id\s*:\s*'([0-9]+)',",
|
r"(?s)window\.PBS\.playerConfig\s*=\s*{.*?id\s*:\s*'([0-9]+)',",
|
||||||
r'<div[^>]+\bdata-cove-id=["\'](\d+)"', # http://www.pbs.org/wgbh/roadshow/watch/episode/2105-indianapolis-hour-2/
|
r'<div[^>]+\bdata-cove-id=["\'](\d+)"', # http://www.pbs.org/wgbh/roadshow/watch/episode/2105-indianapolis-hour-2/
|
||||||
|
|
|
@ -198,6 +198,25 @@ class RedditIE(InfoExtractor):
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
'writesubtitles': True,
|
'writesubtitles': True,
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
# "gated" subreddit post
|
||||||
|
'url': 'https://old.reddit.com/r/ketamine/comments/degtjo/when_the_k_hits/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'gqsbxts133r31',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'display_id': 'degtjo',
|
||||||
|
'title': 'When the K hits',
|
||||||
|
'uploader': '[deleted]',
|
||||||
|
'channel_id': 'ketamine',
|
||||||
|
'comment_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'dislike_count': int,
|
||||||
|
'age_limit': 18,
|
||||||
|
'duration': 34,
|
||||||
|
'thumbnail': r're:https?://.+/.+\.(?:jpg|png)',
|
||||||
|
'timestamp': 1570438713.0,
|
||||||
|
'upload_date': '20191007',
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.reddit.com/r/videos/comments/6rrwyj',
|
'url': 'https://www.reddit.com/r/videos/comments/6rrwyj',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
@ -245,6 +264,15 @@ def _perform_login(self, username, password):
|
||||||
elif not traverse_obj(login, ('json', 'data', 'cookie', {str})):
|
elif not traverse_obj(login, ('json', 'data', 'cookie', {str})):
|
||||||
raise ExtractorError('Unable to login, no cookie was returned')
|
raise ExtractorError('Unable to login, no cookie was returned')
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
# Set cookie to opt-in to age-restricted subreddits
|
||||||
|
self._set_cookie('reddit.com', 'over18', '1')
|
||||||
|
# Set cookie to opt-in to "gated" subreddits
|
||||||
|
options = traverse_obj(self._get_cookies('https://www.reddit.com/'), (
|
||||||
|
'_options', 'value', {urllib.parse.unquote}, {json.loads}, {dict})) or {}
|
||||||
|
options['pref_gated_sr_optin'] = True
|
||||||
|
self._set_cookie('reddit.com', '_options', urllib.parse.quote(json.dumps(options)))
|
||||||
|
|
||||||
def _get_subtitles(self, video_id):
|
def _get_subtitles(self, video_id):
|
||||||
# Fallback if there were no subtitles provided by DASH or HLS manifests
|
# Fallback if there were no subtitles provided by DASH or HLS manifests
|
||||||
caption_url = f'https://v.redd.it/{video_id}/wh_ben_en.vtt'
|
caption_url = f'https://v.redd.it/{video_id}/wh_ben_en.vtt'
|
||||||
|
|
|
@ -53,6 +53,7 @@ class SoundcloudBaseIE(InfoExtractor):
|
||||||
_HEADERS = {}
|
_HEADERS = {}
|
||||||
|
|
||||||
_IMAGE_REPL_RE = r'-([0-9a-z]+)\.jpg'
|
_IMAGE_REPL_RE = r'-([0-9a-z]+)\.jpg'
|
||||||
|
_TAGS_RE = re.compile(r'"([^"]+)"|([^ ]+)')
|
||||||
|
|
||||||
_ARTWORK_MAP = {
|
_ARTWORK_MAP = {
|
||||||
'mini': 16,
|
'mini': 16,
|
||||||
|
@ -372,6 +373,7 @@ def extract_count(key):
|
||||||
'comment_count': extract_count('comment'),
|
'comment_count': extract_count('comment'),
|
||||||
'repost_count': extract_count('reposts'),
|
'repost_count': extract_count('reposts'),
|
||||||
'genres': traverse_obj(info, ('genre', {str}, filter, all, filter)),
|
'genres': traverse_obj(info, ('genre', {str}, filter, all, filter)),
|
||||||
|
'tags': traverse_obj(info, ('tag_list', {self._TAGS_RE.findall}, ..., ..., filter)),
|
||||||
'artists': traverse_obj(info, ('publisher_metadata', 'artist', {str}, filter, all, filter)),
|
'artists': traverse_obj(info, ('publisher_metadata', 'artist', {str}, filter, all, filter)),
|
||||||
'formats': formats if not extract_flat else None,
|
'formats': formats if not extract_flat else None,
|
||||||
}
|
}
|
||||||
|
@ -425,6 +427,7 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||||
'repost_count': int,
|
'repost_count': int,
|
||||||
'thumbnail': 'https://i1.sndcdn.com/artworks-000031955188-rwb18x-original.jpg',
|
'thumbnail': 'https://i1.sndcdn.com/artworks-000031955188-rwb18x-original.jpg',
|
||||||
'uploader_url': 'https://soundcloud.com/ethmusic',
|
'uploader_url': 'https://soundcloud.com/ethmusic',
|
||||||
|
'tags': 'count:14',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
# geo-restricted
|
# geo-restricted
|
||||||
|
@ -440,7 +443,7 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||||
'uploader_id': '9615865',
|
'uploader_id': '9615865',
|
||||||
'timestamp': 1337635207,
|
'timestamp': 1337635207,
|
||||||
'upload_date': '20120521',
|
'upload_date': '20120521',
|
||||||
'duration': 227.155,
|
'duration': 227.103,
|
||||||
'license': 'all-rights-reserved',
|
'license': 'all-rights-reserved',
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
|
@ -450,6 +453,7 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||||
'thumbnail': 'https://i1.sndcdn.com/artworks-v8bFHhXm7Au6-0-original.jpg',
|
'thumbnail': 'https://i1.sndcdn.com/artworks-v8bFHhXm7Au6-0-original.jpg',
|
||||||
'genres': ['Alternative'],
|
'genres': ['Alternative'],
|
||||||
'artists': ['The Royal Concept'],
|
'artists': ['The Royal Concept'],
|
||||||
|
'tags': [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
# private link
|
# private link
|
||||||
|
@ -475,6 +479,7 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||||
'uploader_url': 'https://soundcloud.com/jaimemf',
|
'uploader_url': 'https://soundcloud.com/jaimemf',
|
||||||
'thumbnail': 'https://a1.sndcdn.com/images/default_avatar_large.png',
|
'thumbnail': 'https://a1.sndcdn.com/images/default_avatar_large.png',
|
||||||
'genres': ['youtubedl'],
|
'genres': ['youtubedl'],
|
||||||
|
'tags': [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
# private link (alt format)
|
# private link (alt format)
|
||||||
|
@ -500,15 +505,16 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||||
'uploader_url': 'https://soundcloud.com/jaimemf',
|
'uploader_url': 'https://soundcloud.com/jaimemf',
|
||||||
'thumbnail': 'https://a1.sndcdn.com/images/default_avatar_large.png',
|
'thumbnail': 'https://a1.sndcdn.com/images/default_avatar_large.png',
|
||||||
'genres': ['youtubedl'],
|
'genres': ['youtubedl'],
|
||||||
|
'tags': [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
# downloadable song
|
# downloadable song
|
||||||
{
|
{
|
||||||
'url': 'https://soundcloud.com/the80m/the-following',
|
'url': 'https://soundcloud.com/the80m/the-following',
|
||||||
'md5': '9ffcddb08c87d74fb5808a3c183a1d04',
|
'md5': 'ecb87d7705d5f53e6c02a63760573c75', # wav: '9ffcddb08c87d74fb5808a3c183a1d04'
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '343609555',
|
'id': '343609555',
|
||||||
'ext': 'wav',
|
'ext': 'opus', # wav original available with auth
|
||||||
'title': 'The Following',
|
'title': 'The Following',
|
||||||
'track': 'The Following',
|
'track': 'The Following',
|
||||||
'description': '',
|
'description': '',
|
||||||
|
@ -526,15 +532,18 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'genres': ['Dance & EDM'],
|
'genres': ['Dance & EDM'],
|
||||||
'artists': ['80M'],
|
'artists': ['80M'],
|
||||||
|
'tags': ['80M', 'EDM', 'Dance', 'Music'],
|
||||||
},
|
},
|
||||||
|
'expected_warnings': ['Original download format is only available for registered users'],
|
||||||
},
|
},
|
||||||
# private link, downloadable format
|
# private link, downloadable format
|
||||||
|
# tags with spaces (e.g. "Uplifting Trance", "Ori Uplift")
|
||||||
{
|
{
|
||||||
'url': 'https://soundcloud.com/oriuplift/uponly-238-no-talking-wav/s-AyZUd',
|
'url': 'https://soundcloud.com/oriuplift/uponly-238-no-talking-wav/s-AyZUd',
|
||||||
'md5': '64a60b16e617d41d0bef032b7f55441e',
|
'md5': '2e1530d0e9986a833a67cb34fc90ece0', # wav: '64a60b16e617d41d0bef032b7f55441e'
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '340344461',
|
'id': '340344461',
|
||||||
'ext': 'wav',
|
'ext': 'opus', # wav original available with auth
|
||||||
'title': 'Uplifting Only 238 [No Talking] (incl. Alex Feed Guestmix) (Aug 31, 2017) [wav]',
|
'title': 'Uplifting Only 238 [No Talking] (incl. Alex Feed Guestmix) (Aug 31, 2017) [wav]',
|
||||||
'track': 'Uplifting Only 238 [No Talking] (incl. Alex Feed Guestmix) (Aug 31, 2017) [wav]',
|
'track': 'Uplifting Only 238 [No Talking] (incl. Alex Feed Guestmix) (Aug 31, 2017) [wav]',
|
||||||
'description': 'md5:fa20ee0fca76a3d6df8c7e57f3715366',
|
'description': 'md5:fa20ee0fca76a3d6df8c7e57f3715366',
|
||||||
|
@ -552,7 +561,9 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||||
'uploader_url': 'https://soundcloud.com/oriuplift',
|
'uploader_url': 'https://soundcloud.com/oriuplift',
|
||||||
'genres': ['Trance'],
|
'genres': ['Trance'],
|
||||||
'artists': ['Ori Uplift'],
|
'artists': ['Ori Uplift'],
|
||||||
|
'tags': ['Orchestral', 'Emotional', 'Uplifting Trance', 'Trance', 'Ori Uplift', 'UpOnly'],
|
||||||
},
|
},
|
||||||
|
'expected_warnings': ['Original download format is only available for registered users'],
|
||||||
},
|
},
|
||||||
# no album art, use avatar pic for thumbnail
|
# no album art, use avatar pic for thumbnail
|
||||||
{
|
{
|
||||||
|
@ -577,6 +588,7 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||||
'repost_count': int,
|
'repost_count': int,
|
||||||
'uploader_url': 'https://soundcloud.com/garyvee',
|
'uploader_url': 'https://soundcloud.com/garyvee',
|
||||||
'artists': ['MadReal'],
|
'artists': ['MadReal'],
|
||||||
|
'tags': [],
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
|
@ -604,6 +616,7 @@ class SoundcloudIE(SoundcloudBaseIE):
|
||||||
'repost_count': int,
|
'repost_count': int,
|
||||||
'genres': ['Piano'],
|
'genres': ['Piano'],
|
||||||
'uploader_url': 'https://soundcloud.com/giovannisarani',
|
'uploader_url': 'https://soundcloud.com/giovannisarani',
|
||||||
|
'tags': 'count:10',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
import random
|
import math
|
||||||
import re
|
import re
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .periscope import PeriscopeBaseIE, PeriscopeIE
|
from .periscope import PeriscopeBaseIE, PeriscopeIE
|
||||||
|
from ..jsinterp import js_number_to_string
|
||||||
from ..networking.exceptions import HTTPError
|
from ..networking.exceptions import HTTPError
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
@ -1330,6 +1331,11 @@ def _build_graphql_query(self, media_id):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _generate_syndication_token(self, twid):
|
||||||
|
# ((Number(twid) / 1e15) * Math.PI).toString(36).replace(/(0+|\.)/g, '')
|
||||||
|
translation = str.maketrans(dict.fromkeys('0.'))
|
||||||
|
return js_number_to_string((int(twid) / 1e15) * math.PI, 36).translate(translation)
|
||||||
|
|
||||||
def _call_syndication_api(self, twid):
|
def _call_syndication_api(self, twid):
|
||||||
self.report_warning(
|
self.report_warning(
|
||||||
'Not all metadata or media is available via syndication endpoint', twid, only_once=True)
|
'Not all metadata or media is available via syndication endpoint', twid, only_once=True)
|
||||||
|
@ -1337,8 +1343,7 @@ def _call_syndication_api(self, twid):
|
||||||
'https://cdn.syndication.twimg.com/tweet-result', twid, 'Downloading syndication JSON',
|
'https://cdn.syndication.twimg.com/tweet-result', twid, 'Downloading syndication JSON',
|
||||||
headers={'User-Agent': 'Googlebot'}, query={
|
headers={'User-Agent': 'Googlebot'}, query={
|
||||||
'id': twid,
|
'id': twid,
|
||||||
# TODO: token = ((Number(twid) / 1e15) * Math.PI).toString(36).replace(/(0+|\.)/g, '')
|
'token': self._generate_syndication_token(twid),
|
||||||
'token': ''.join(random.choices('123456789abcdefghijklmnopqrstuvwxyz', k=10)),
|
|
||||||
})
|
})
|
||||||
if not status:
|
if not status:
|
||||||
raise ExtractorError('Syndication endpoint returned empty JSON response')
|
raise ExtractorError('Syndication endpoint returned empty JSON response')
|
||||||
|
|
|
@ -857,6 +857,18 @@ def generate_api_headers(
|
||||||
}
|
}
|
||||||
return filter_dict(headers)
|
return filter_dict(headers)
|
||||||
|
|
||||||
|
def _download_webpage_with_retries(self, *args, retry_fatal=False, retry_on_status=None, **kwargs):
|
||||||
|
for retry in self.RetryManager(fatal=retry_fatal):
|
||||||
|
try:
|
||||||
|
return self._download_webpage(*args, **kwargs)
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, network_exceptions):
|
||||||
|
if not isinstance(e.cause, HTTPError) or e.cause.status not in (retry_on_status or (403, 429)):
|
||||||
|
retry.error = e
|
||||||
|
continue
|
||||||
|
self._error_or_warning(e, fatal=retry_fatal)
|
||||||
|
break
|
||||||
|
|
||||||
def _download_ytcfg(self, client, video_id):
|
def _download_ytcfg(self, client, video_id):
|
||||||
url = {
|
url = {
|
||||||
'web': 'https://www.youtube.com',
|
'web': 'https://www.youtube.com',
|
||||||
|
@ -866,8 +878,8 @@ def _download_ytcfg(self, client, video_id):
|
||||||
}.get(client)
|
}.get(client)
|
||||||
if not url:
|
if not url:
|
||||||
return {}
|
return {}
|
||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage_with_retries(
|
||||||
url, video_id, fatal=False, note=f'Downloading {client.replace("_", " ").strip()} client config',
|
url, video_id, note=f'Downloading {client.replace("_", " ").strip()} client config',
|
||||||
headers=traverse_obj(self._get_default_ytcfg(client), {
|
headers=traverse_obj(self._get_default_ytcfg(client), {
|
||||||
'User-Agent': ('INNERTUBE_CONTEXT', 'client', 'userAgent', {str}),
|
'User-Agent': ('INNERTUBE_CONTEXT', 'client', 'userAgent', {str}),
|
||||||
}))
|
}))
|
||||||
|
@ -3135,15 +3147,22 @@ def _extract_player_url(self, *ytcfgs, webpage=None):
|
||||||
get_all=False, expected_type=str)
|
get_all=False, expected_type=str)
|
||||||
if not player_url:
|
if not player_url:
|
||||||
return
|
return
|
||||||
|
# TODO: Add proper support for the 'tce' variant players
|
||||||
|
# See https://github.com/yt-dlp/yt-dlp/issues/12398
|
||||||
|
if '/player_ias_tce.vflset/' in player_url:
|
||||||
|
self.write_debug(f'Modifying tce player URL: {player_url}')
|
||||||
|
player_url = player_url.replace('/player_ias_tce.vflset/', '/player_ias.vflset/')
|
||||||
return urljoin('https://www.youtube.com', player_url)
|
return urljoin('https://www.youtube.com', player_url)
|
||||||
|
|
||||||
def _download_player_url(self, video_id, fatal=False):
|
def _download_player_url(self, video_id, fatal=False):
|
||||||
res = self._download_webpage(
|
iframe_webpage = self._download_webpage_with_retries(
|
||||||
'https://www.youtube.com/iframe_api',
|
'https://www.youtube.com/iframe_api',
|
||||||
note='Downloading iframe API JS', video_id=video_id, fatal=fatal)
|
note='Downloading iframe API JS',
|
||||||
if res:
|
video_id=video_id, retry_fatal=fatal)
|
||||||
|
|
||||||
|
if iframe_webpage:
|
||||||
player_version = self._search_regex(
|
player_version = self._search_regex(
|
||||||
r'player\\?/([0-9a-fA-F]{8})\\?/', res, 'player version', fatal=fatal)
|
r'player\\?/([0-9a-fA-F]{8})\\?/', iframe_webpage, 'player version', fatal=fatal)
|
||||||
if player_version:
|
if player_version:
|
||||||
return f'https://www.youtube.com/s/player/{player_version}/player_ias.vflset/en_US/base.js'
|
return f'https://www.youtube.com/s/player/{player_version}/player_ias.vflset/en_US/base.js'
|
||||||
|
|
||||||
|
@ -3369,7 +3388,7 @@ def _fixup_n_function_code(self, argnames, code):
|
||||||
|
|
||||||
def _extract_n_function_code(self, video_id, player_url):
|
def _extract_n_function_code(self, video_id, player_url):
|
||||||
player_id = self._extract_player_info(player_url)
|
player_id = self._extract_player_info(player_url)
|
||||||
func_code = self.cache.load('youtube-nsig', player_id, min_ver='2024.07.09')
|
func_code = self.cache.load('youtube-nsig', player_id, min_ver='2025.02.19')
|
||||||
jscode = func_code or self._load_player(video_id, player_url)
|
jscode = func_code or self._load_player(video_id, player_url)
|
||||||
jsi = JSInterpreter(jscode)
|
jsi = JSInterpreter(jscode)
|
||||||
|
|
||||||
|
@ -4556,8 +4575,7 @@ def _download_player_responses(self, url, smuggled_data, video_id, webpage_url):
|
||||||
pp = self._configuration_arg('player_params', [None], casesense=True)[0]
|
pp = self._configuration_arg('player_params', [None], casesense=True)[0]
|
||||||
if pp:
|
if pp:
|
||||||
query['pp'] = pp
|
query['pp'] = pp
|
||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage_with_retries(webpage_url, video_id, query=query)
|
||||||
webpage_url, video_id, fatal=False, query=query)
|
|
||||||
|
|
||||||
master_ytcfg = self.extract_ytcfg(video_id, webpage) or self._get_default_ytcfg()
|
master_ytcfg = self.extract_ytcfg(video_id, webpage) or self._get_default_ytcfg()
|
||||||
|
|
||||||
|
|
|
@ -137,6 +137,116 @@ def _extract_player(self, webpage, video_id, fatal=True):
|
||||||
group='json'),
|
group='json'),
|
||||||
video_id)
|
video_id)
|
||||||
|
|
||||||
|
def _extract_entry(self, url, player, content, video_id):
|
||||||
|
title = content.get('title') or content['teaserHeadline']
|
||||||
|
|
||||||
|
t = content['mainVideoContent']['http://zdf.de/rels/target']
|
||||||
|
ptmd_path = traverse_obj(t, (
|
||||||
|
(('streams', 'default'), None),
|
||||||
|
('http://zdf.de/rels/streams/ptmd', 'http://zdf.de/rels/streams/ptmd-template'),
|
||||||
|
), get_all=False)
|
||||||
|
if not ptmd_path:
|
||||||
|
raise ExtractorError('Could not extract ptmd_path')
|
||||||
|
|
||||||
|
info = self._extract_ptmd(
|
||||||
|
urljoin(url, ptmd_path.replace('{playerId}', 'android_native_5')), video_id, player['apiToken'], url)
|
||||||
|
|
||||||
|
thumbnails = []
|
||||||
|
layouts = try_get(
|
||||||
|
content, lambda x: x['teaserImageRef']['layouts'], dict)
|
||||||
|
if layouts:
|
||||||
|
for layout_key, layout_url in layouts.items():
|
||||||
|
layout_url = url_or_none(layout_url)
|
||||||
|
if not layout_url:
|
||||||
|
continue
|
||||||
|
thumbnail = {
|
||||||
|
'url': layout_url,
|
||||||
|
'format_id': layout_key,
|
||||||
|
}
|
||||||
|
mobj = re.search(r'(?P<width>\d+)x(?P<height>\d+)', layout_key)
|
||||||
|
if mobj:
|
||||||
|
thumbnail.update({
|
||||||
|
'width': int(mobj.group('width')),
|
||||||
|
'height': int(mobj.group('height')),
|
||||||
|
})
|
||||||
|
thumbnails.append(thumbnail)
|
||||||
|
|
||||||
|
chapter_marks = t.get('streamAnchorTag') or []
|
||||||
|
chapter_marks.append({'anchorOffset': int_or_none(t.get('duration'))})
|
||||||
|
chapters = [{
|
||||||
|
'start_time': chap.get('anchorOffset'),
|
||||||
|
'end_time': next_chap.get('anchorOffset'),
|
||||||
|
'title': chap.get('anchorLabel'),
|
||||||
|
} for chap, next_chap in zip(chapter_marks, chapter_marks[1:])]
|
||||||
|
|
||||||
|
return merge_dicts(info, {
|
||||||
|
'title': title,
|
||||||
|
'description': content.get('leadParagraph') or content.get('teasertext'),
|
||||||
|
'duration': int_or_none(t.get('duration')),
|
||||||
|
'timestamp': unified_timestamp(content.get('editorialDate')),
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
'chapters': chapters or None,
|
||||||
|
'episode': title,
|
||||||
|
**traverse_obj(content, ('programmeItem', 0, 'http://zdf.de/rels/target', {
|
||||||
|
'series_id': ('http://zdf.de/rels/cmdm/series', 'seriesUuid', {str}),
|
||||||
|
'series': ('http://zdf.de/rels/cmdm/series', 'seriesTitle', {str}),
|
||||||
|
'season': ('http://zdf.de/rels/cmdm/season', 'seasonTitle', {str}),
|
||||||
|
'season_number': ('http://zdf.de/rels/cmdm/season', 'seasonNumber', {int_or_none}),
|
||||||
|
'season_id': ('http://zdf.de/rels/cmdm/season', 'seasonUuid', {str}),
|
||||||
|
'episode_number': ('episodeNumber', {int_or_none}),
|
||||||
|
'episode_id': ('contentId', {str}),
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
def _extract_regular(self, url, player, video_id, query=None):
|
||||||
|
player_url = player['content']
|
||||||
|
|
||||||
|
content = self._call_api(
|
||||||
|
update_url_query(player_url, query),
|
||||||
|
video_id, 'content', player['apiToken'], url)
|
||||||
|
|
||||||
|
return self._extract_entry(player_url, player, content, video_id)
|
||||||
|
|
||||||
|
def _extract_mobile(self, video_id):
|
||||||
|
video = self._download_v2_doc(video_id)
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
formitaeten = try_get(video, lambda x: x['document']['formitaeten'], list)
|
||||||
|
document = formitaeten and video['document']
|
||||||
|
if formitaeten:
|
||||||
|
title = document['titel']
|
||||||
|
content_id = document['basename']
|
||||||
|
|
||||||
|
format_urls = set()
|
||||||
|
for f in formitaeten or []:
|
||||||
|
self._extract_format(content_id, formats, format_urls, f)
|
||||||
|
|
||||||
|
thumbnails = []
|
||||||
|
teaser_bild = document.get('teaserBild')
|
||||||
|
if isinstance(teaser_bild, dict):
|
||||||
|
for thumbnail_key, thumbnail in teaser_bild.items():
|
||||||
|
thumbnail_url = try_get(
|
||||||
|
thumbnail, lambda x: x['url'], str)
|
||||||
|
if thumbnail_url:
|
||||||
|
thumbnails.append({
|
||||||
|
'url': thumbnail_url,
|
||||||
|
'id': thumbnail_key,
|
||||||
|
'width': int_or_none(thumbnail.get('width')),
|
||||||
|
'height': int_or_none(thumbnail.get('height')),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': content_id,
|
||||||
|
'title': title,
|
||||||
|
'description': document.get('beschreibung'),
|
||||||
|
'duration': int_or_none(document.get('length')),
|
||||||
|
'timestamp': unified_timestamp(document.get('date')) or unified_timestamp(
|
||||||
|
try_get(video, lambda x: x['meta']['editorialDate'], str)),
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
'subtitles': self._extract_subtitles(document),
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ZDFIE(ZDFBaseIE):
|
class ZDFIE(ZDFBaseIE):
|
||||||
_VALID_URL = r'https?://www\.zdf\.de/(?:[^/]+/)*(?P<id>[^/?#&]+)\.html'
|
_VALID_URL = r'https?://www\.zdf\.de/(?:[^/]+/)*(?P<id>[^/?#&]+)\.html'
|
||||||
|
@ -187,12 +297,20 @@ class ZDFIE(ZDFBaseIE):
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '151025_magie_farben2_tex',
|
'id': '151025_magie_farben2_tex',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
|
'duration': 2615.0,
|
||||||
'title': 'Die Magie der Farben (2/2)',
|
'title': 'Die Magie der Farben (2/2)',
|
||||||
'description': 'md5:a89da10c928c6235401066b60a6d5c1a',
|
'description': 'md5:a89da10c928c6235401066b60a6d5c1a',
|
||||||
'duration': 2615,
|
|
||||||
'timestamp': 1465021200,
|
'timestamp': 1465021200,
|
||||||
'upload_date': '20160604',
|
|
||||||
'thumbnail': 'https://www.zdf.de/assets/mauve-im-labor-100~768x432?cb=1464909117806',
|
'thumbnail': 'https://www.zdf.de/assets/mauve-im-labor-100~768x432?cb=1464909117806',
|
||||||
|
'upload_date': '20160604',
|
||||||
|
'episode': 'Die Magie der Farben (2/2)',
|
||||||
|
'episode_id': 'POS_954f4170-36a5-4a41-a6cf-78f1f3b1f127',
|
||||||
|
'season': 'Staffel 1',
|
||||||
|
'series': 'Die Magie der Farben',
|
||||||
|
'season_number': 1,
|
||||||
|
'series_id': 'a39900dd-cdbd-4a6a-a413-44e8c6ae18bc',
|
||||||
|
'season_id': '5a92e619-8a0f-4410-a3d5-19c76fbebb37',
|
||||||
|
'episode_number': 2,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.zdf.de/funk/druck-11790/funk-alles-ist-verzaubert-102.html',
|
'url': 'https://www.zdf.de/funk/druck-11790/funk-alles-ist-verzaubert-102.html',
|
||||||
|
@ -200,12 +318,13 @@ class ZDFIE(ZDFBaseIE):
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'id': 'video_funk_1770473',
|
'id': 'video_funk_1770473',
|
||||||
'duration': 1278,
|
'duration': 1278.0,
|
||||||
'description': 'Die Neue an der Schule verdreht Ismail den Kopf.',
|
|
||||||
'title': 'Alles ist verzaubert',
|
'title': 'Alles ist verzaubert',
|
||||||
|
'description': 'Die Neue an der Schule verdreht Ismail den Kopf.',
|
||||||
'timestamp': 1635520560,
|
'timestamp': 1635520560,
|
||||||
'upload_date': '20211029',
|
|
||||||
'thumbnail': 'https://www.zdf.de/assets/teaser-funk-alles-ist-verzaubert-102~1920x1080?cb=1663848412907',
|
'thumbnail': 'https://www.zdf.de/assets/teaser-funk-alles-ist-verzaubert-102~1920x1080?cb=1663848412907',
|
||||||
|
'upload_date': '20211029',
|
||||||
|
'episode': 'Alles ist verzaubert',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# Same as https://www.phoenix.de/sendungen/dokumentationen/gesten-der-maechtigen-i-a-89468.html?ref=suche
|
# Same as https://www.phoenix.de/sendungen/dokumentationen/gesten-der-maechtigen-i-a-89468.html?ref=suche
|
||||||
|
@ -248,121 +367,55 @@ class ZDFIE(ZDFBaseIE):
|
||||||
'title': 'Das Geld anderer Leute',
|
'title': 'Das Geld anderer Leute',
|
||||||
'description': 'md5:cb6f660850dc5eb7d1ab776ea094959d',
|
'description': 'md5:cb6f660850dc5eb7d1ab776ea094959d',
|
||||||
'duration': 2581.0,
|
'duration': 2581.0,
|
||||||
'timestamp': 1675160100,
|
'timestamp': 1728983700,
|
||||||
'upload_date': '20230131',
|
'upload_date': '20241015',
|
||||||
'thumbnail': 'https://epg-image.zdf.de/fotobase-webdelivery/images/e2d7e55a-09f0-424e-ac73-6cac4dd65f35?layout=2400x1350',
|
'thumbnail': 'https://epg-image.zdf.de/fotobase-webdelivery/images/e2d7e55a-09f0-424e-ac73-6cac4dd65f35?layout=2400x1350',
|
||||||
|
'series': 'SOKO Stuttgart',
|
||||||
|
'series_id': 'f862ce9a-6dd1-4388-a698-22b36ac4c9e9',
|
||||||
|
'season': 'Staffel 11',
|
||||||
|
'season_number': 11,
|
||||||
|
'season_id': 'ae1b4990-6d87-4970-a571-caccf1ba2879',
|
||||||
|
'episode': 'Das Geld anderer Leute',
|
||||||
|
'episode_number': 10,
|
||||||
|
'episode_id': 'POS_7f367934-f2f0-45cb-9081-736781ff2d23',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.zdf.de/dokumentation/terra-x/unser-gruener-planet-wuesten-doku-100.html',
|
'url': 'https://www.zdf.de/dokumentation/terra-x/unser-gruener-planet-wuesten-doku-100.html',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '220605_dk_gruener_planet_wuesten_tex',
|
'id': '220525_green_planet_makingof_1_tropen_tex',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Unser grüner Planet - Wüsten',
|
'title': 'Making-of Unser grüner Planet - Tropen',
|
||||||
'description': 'md5:4fc647b6f9c3796eea66f4a0baea2862',
|
'description': 'md5:d7c6949dc7c75c73c4ad51c785fb0b79',
|
||||||
'duration': 2613.0,
|
'duration': 435.0,
|
||||||
'timestamp': 1654450200,
|
'timestamp': 1653811200,
|
||||||
'upload_date': '20220605',
|
'upload_date': '20220529',
|
||||||
'format_note': 'uhd, main',
|
'format_note': 'hd, main',
|
||||||
'thumbnail': 'https://www.zdf.de/assets/saguaro-kakteen-102~3840x2160?cb=1655910690796',
|
'thumbnail': 'https://www.zdf.de/assets/unser-gruener-planet-making-of-1-tropen-100~3840x2160?cb=1653493335577',
|
||||||
|
'episode': 'Making-of Unser grüner Planet - Tropen',
|
||||||
|
},
|
||||||
|
'skip': 'No longer available: "Leider kein Video verfügbar"',
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.zdf.de/serien/northern-lights/begegnung-auf-der-bruecke-100.html',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '240319_2310_sendung_not',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Begegnung auf der Brücke',
|
||||||
|
'description': 'md5:e53a555da87447f7f1207f10353f8e45',
|
||||||
|
'thumbnail': 'https://epg-image.zdf.de/fotobase-webdelivery/images/c5ff1d1f-f5c8-4468-86ac-1b2f1dbecc76?layout=2400x1350',
|
||||||
|
'upload_date': '20250203',
|
||||||
|
'duration': 3083.0,
|
||||||
|
'timestamp': 1738546500,
|
||||||
|
'series_id': '1d7a1879-01ee-4468-8237-c6b4ecd633c7',
|
||||||
|
'series': 'Northern Lights',
|
||||||
|
'season': 'Staffel 1',
|
||||||
|
'season_number': 1,
|
||||||
|
'season_id': '22ac26a2-4ea2-4055-ac0b-98b755cdf718',
|
||||||
|
'episode': 'Begegnung auf der Brücke',
|
||||||
|
'episode_number': 1,
|
||||||
|
'episode_id': 'POS_71049438-024b-471f-b472-4fe2e490d1fb',
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _extract_entry(self, url, player, content, video_id):
|
|
||||||
title = content.get('title') or content['teaserHeadline']
|
|
||||||
|
|
||||||
t = content['mainVideoContent']['http://zdf.de/rels/target']
|
|
||||||
ptmd_path = traverse_obj(t, (
|
|
||||||
(('streams', 'default'), None),
|
|
||||||
('http://zdf.de/rels/streams/ptmd', 'http://zdf.de/rels/streams/ptmd-template'),
|
|
||||||
), get_all=False)
|
|
||||||
if not ptmd_path:
|
|
||||||
raise ExtractorError('Could not extract ptmd_path')
|
|
||||||
|
|
||||||
info = self._extract_ptmd(
|
|
||||||
urljoin(url, ptmd_path.replace('{playerId}', 'android_native_5')), video_id, player['apiToken'], url)
|
|
||||||
|
|
||||||
thumbnails = []
|
|
||||||
layouts = try_get(
|
|
||||||
content, lambda x: x['teaserImageRef']['layouts'], dict)
|
|
||||||
if layouts:
|
|
||||||
for layout_key, layout_url in layouts.items():
|
|
||||||
layout_url = url_or_none(layout_url)
|
|
||||||
if not layout_url:
|
|
||||||
continue
|
|
||||||
thumbnail = {
|
|
||||||
'url': layout_url,
|
|
||||||
'format_id': layout_key,
|
|
||||||
}
|
|
||||||
mobj = re.search(r'(?P<width>\d+)x(?P<height>\d+)', layout_key)
|
|
||||||
if mobj:
|
|
||||||
thumbnail.update({
|
|
||||||
'width': int(mobj.group('width')),
|
|
||||||
'height': int(mobj.group('height')),
|
|
||||||
})
|
|
||||||
thumbnails.append(thumbnail)
|
|
||||||
|
|
||||||
chapter_marks = t.get('streamAnchorTag') or []
|
|
||||||
chapter_marks.append({'anchorOffset': int_or_none(t.get('duration'))})
|
|
||||||
chapters = [{
|
|
||||||
'start_time': chap.get('anchorOffset'),
|
|
||||||
'end_time': next_chap.get('anchorOffset'),
|
|
||||||
'title': chap.get('anchorLabel'),
|
|
||||||
} for chap, next_chap in zip(chapter_marks, chapter_marks[1:])]
|
|
||||||
|
|
||||||
return merge_dicts(info, {
|
|
||||||
'title': title,
|
|
||||||
'description': content.get('leadParagraph') or content.get('teasertext'),
|
|
||||||
'duration': int_or_none(t.get('duration')),
|
|
||||||
'timestamp': unified_timestamp(content.get('editorialDate')),
|
|
||||||
'thumbnails': thumbnails,
|
|
||||||
'chapters': chapters or None,
|
|
||||||
})
|
|
||||||
|
|
||||||
def _extract_regular(self, url, player, video_id):
|
|
||||||
content = self._call_api(
|
|
||||||
player['content'], video_id, 'content', player['apiToken'], url)
|
|
||||||
return self._extract_entry(player['content'], player, content, video_id)
|
|
||||||
|
|
||||||
def _extract_mobile(self, video_id):
|
|
||||||
video = self._download_v2_doc(video_id)
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
formitaeten = try_get(video, lambda x: x['document']['formitaeten'], list)
|
|
||||||
document = formitaeten and video['document']
|
|
||||||
if formitaeten:
|
|
||||||
title = document['titel']
|
|
||||||
content_id = document['basename']
|
|
||||||
|
|
||||||
format_urls = set()
|
|
||||||
for f in formitaeten or []:
|
|
||||||
self._extract_format(content_id, formats, format_urls, f)
|
|
||||||
|
|
||||||
thumbnails = []
|
|
||||||
teaser_bild = document.get('teaserBild')
|
|
||||||
if isinstance(teaser_bild, dict):
|
|
||||||
for thumbnail_key, thumbnail in teaser_bild.items():
|
|
||||||
thumbnail_url = try_get(
|
|
||||||
thumbnail, lambda x: x['url'], str)
|
|
||||||
if thumbnail_url:
|
|
||||||
thumbnails.append({
|
|
||||||
'url': thumbnail_url,
|
|
||||||
'id': thumbnail_key,
|
|
||||||
'width': int_or_none(thumbnail.get('width')),
|
|
||||||
'height': int_or_none(thumbnail.get('height')),
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': content_id,
|
|
||||||
'title': title,
|
|
||||||
'description': document.get('beschreibung'),
|
|
||||||
'duration': int_or_none(document.get('length')),
|
|
||||||
'timestamp': unified_timestamp(document.get('date')) or unified_timestamp(
|
|
||||||
try_get(video, lambda x: x['meta']['editorialDate'], str)),
|
|
||||||
'thumbnails': thumbnails,
|
|
||||||
'subtitles': self._extract_subtitles(document),
|
|
||||||
'formats': formats,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
@ -370,7 +423,7 @@ def _real_extract(self, url):
|
||||||
if webpage:
|
if webpage:
|
||||||
player = self._extract_player(webpage, url, fatal=False)
|
player = self._extract_player(webpage, url, fatal=False)
|
||||||
if player:
|
if player:
|
||||||
return self._extract_regular(url, player, video_id)
|
return self._extract_regular(url, player, video_id, query={'profile': 'player-3'})
|
||||||
|
|
||||||
return self._extract_mobile(video_id)
|
return self._extract_mobile(video_id)
|
||||||
|
|
||||||
|
@ -416,7 +469,8 @@ def _extract_entry(self, entry):
|
||||||
'title': ('titel', {str}),
|
'title': ('titel', {str}),
|
||||||
'description': ('beschreibung', {str}),
|
'description': ('beschreibung', {str}),
|
||||||
'duration': ('length', {float_or_none}),
|
'duration': ('length', {float_or_none}),
|
||||||
# TODO: seasonNumber and episodeNumber can be extracted but need to also be in ZDFIE
|
'season_number': ('seasonNumber', {int_or_none}),
|
||||||
|
'episode_number': ('episodeNumber', {int_or_none}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
def _entries(self, data, document_id):
|
def _entries(self, data, document_id):
|
||||||
|
|
|
@ -25,7 +25,7 @@ def zeroise(x):
|
||||||
with contextlib.suppress(TypeError):
|
with contextlib.suppress(TypeError):
|
||||||
if math.isnan(x): # NB: NaN cannot be checked by membership
|
if math.isnan(x): # NB: NaN cannot be checked by membership
|
||||||
return 0
|
return 0
|
||||||
return x
|
return int(float(x))
|
||||||
|
|
||||||
def wrapped(a, b):
|
def wrapped(a, b):
|
||||||
return op(zeroise(a), zeroise(b)) & 0xffffffff
|
return op(zeroise(a), zeroise(b)) & 0xffffffff
|
||||||
|
@ -95,6 +95,61 @@ def _js_ternary(cndn, if_true=True, if_false=False):
|
||||||
return if_true
|
return if_true
|
||||||
|
|
||||||
|
|
||||||
|
# Ref: https://es5.github.io/#x9.8.1
|
||||||
|
def js_number_to_string(val: float, radix: int = 10):
|
||||||
|
if radix in (JS_Undefined, None):
|
||||||
|
radix = 10
|
||||||
|
assert radix in range(2, 37), 'radix must be an integer at least 2 and no greater than 36'
|
||||||
|
|
||||||
|
if math.isnan(val):
|
||||||
|
return 'NaN'
|
||||||
|
if val == 0:
|
||||||
|
return '0'
|
||||||
|
if math.isinf(val):
|
||||||
|
return '-Infinity' if val < 0 else 'Infinity'
|
||||||
|
if radix == 10:
|
||||||
|
# TODO: implement special cases
|
||||||
|
...
|
||||||
|
|
||||||
|
ALPHABET = b'0123456789abcdefghijklmnopqrstuvwxyz.-'
|
||||||
|
|
||||||
|
result = collections.deque()
|
||||||
|
sign = val < 0
|
||||||
|
val = abs(val)
|
||||||
|
fraction, integer = math.modf(val)
|
||||||
|
delta = max(math.nextafter(.0, math.inf), math.ulp(val) / 2)
|
||||||
|
|
||||||
|
if fraction >= delta:
|
||||||
|
result.append(-2) # `.`
|
||||||
|
while fraction >= delta:
|
||||||
|
delta *= radix
|
||||||
|
fraction, digit = math.modf(fraction * radix)
|
||||||
|
result.append(int(digit))
|
||||||
|
# if we need to round, propagate potential carry through fractional part
|
||||||
|
needs_rounding = fraction > 0.5 or (fraction == 0.5 and int(digit) & 1)
|
||||||
|
if needs_rounding and fraction + delta > 1:
|
||||||
|
for index in reversed(range(1, len(result))):
|
||||||
|
if result[index] + 1 < radix:
|
||||||
|
result[index] += 1
|
||||||
|
break
|
||||||
|
result.pop()
|
||||||
|
|
||||||
|
else:
|
||||||
|
integer += 1
|
||||||
|
break
|
||||||
|
|
||||||
|
integer, digit = divmod(int(integer), radix)
|
||||||
|
result.appendleft(digit)
|
||||||
|
while integer > 0:
|
||||||
|
integer, digit = divmod(integer, radix)
|
||||||
|
result.appendleft(digit)
|
||||||
|
|
||||||
|
if sign:
|
||||||
|
result.appendleft(-1) # `-`
|
||||||
|
|
||||||
|
return bytes(ALPHABET[digit] for digit in result).decode('ascii')
|
||||||
|
|
||||||
|
|
||||||
# Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
|
# Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
|
||||||
_OPERATORS = { # None => Defined in JSInterpreter._operator
|
_OPERATORS = { # None => Defined in JSInterpreter._operator
|
||||||
'?': None,
|
'?': None,
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
_configuration_args,
|
_configuration_args,
|
||||||
deprecation_warning,
|
deprecation_warning,
|
||||||
)
|
)
|
||||||
|
from ..utils._utils import _ProgressState
|
||||||
|
|
||||||
|
|
||||||
class PostProcessorMetaClass(type):
|
class PostProcessorMetaClass(type):
|
||||||
|
@ -189,7 +190,7 @@ def report_progress(self, s):
|
||||||
|
|
||||||
self._downloader.to_console_title(self._downloader.evaluate_outtmpl(
|
self._downloader.to_console_title(self._downloader.evaluate_outtmpl(
|
||||||
progress_template.get('postprocess-title') or 'yt-dlp %(progress._default_template)s',
|
progress_template.get('postprocess-title') or 'yt-dlp %(progress._default_template)s',
|
||||||
progress_dict))
|
progress_dict), _ProgressState.from_dict(s), s.get('_percent'))
|
||||||
|
|
||||||
def _retry_download(self, err, count, retries):
|
def _retry_download(self, err, count, retries):
|
||||||
# While this is not an extractor, it behaves similar to one and
|
# While this is not an extractor, it behaves similar to one and
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import email.header
|
import email.header
|
||||||
import email.utils
|
import email.utils
|
||||||
|
import enum
|
||||||
import errno
|
import errno
|
||||||
import functools
|
import functools
|
||||||
import hashlib
|
import hashlib
|
||||||
|
@ -5627,6 +5628,24 @@ def filesize_from_tbr(tbr, duration):
|
||||||
return int(duration * tbr * (1000 / 8))
|
return int(duration * tbr * (1000 / 8))
|
||||||
|
|
||||||
|
|
||||||
|
def _request_dump_filename(url, video_id, data=None, trim_length=None):
|
||||||
|
if data is not None:
|
||||||
|
data = hashlib.md5(data).hexdigest()
|
||||||
|
basen = join_nonempty(video_id, data, url, delim='_')
|
||||||
|
trim_length = trim_length or 240
|
||||||
|
if len(basen) > trim_length:
|
||||||
|
h = '___' + hashlib.md5(basen.encode()).hexdigest()
|
||||||
|
basen = basen[:trim_length - len(h)] + h
|
||||||
|
filename = sanitize_filename(f'{basen}.dump', restricted=True)
|
||||||
|
# Working around MAX_PATH limitation on Windows (see
|
||||||
|
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx)
|
||||||
|
if os.name == 'nt':
|
||||||
|
absfilepath = os.path.abspath(filename)
|
||||||
|
if len(absfilepath) > 259:
|
||||||
|
filename = fR'\\?\{absfilepath}'
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
# XXX: Temporary
|
# XXX: Temporary
|
||||||
class _YDLLogger:
|
class _YDLLogger:
|
||||||
def __init__(self, ydl=None):
|
def __init__(self, ydl=None):
|
||||||
|
@ -5655,3 +5674,32 @@ def stdout(self, message):
|
||||||
def stderr(self, message):
|
def stderr(self, message):
|
||||||
if self._ydl:
|
if self._ydl:
|
||||||
self._ydl.to_stderr(message)
|
self._ydl.to_stderr(message)
|
||||||
|
|
||||||
|
|
||||||
|
class _ProgressState(enum.Enum):
|
||||||
|
"""
|
||||||
|
Represents a state for a progress bar.
|
||||||
|
|
||||||
|
See: https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
|
||||||
|
"""
|
||||||
|
|
||||||
|
HIDDEN = 0
|
||||||
|
INDETERMINATE = 3
|
||||||
|
VISIBLE = 1
|
||||||
|
WARNING = 4
|
||||||
|
ERROR = 2
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, s, /):
|
||||||
|
if s['status'] == 'finished':
|
||||||
|
return cls.INDETERMINATE
|
||||||
|
|
||||||
|
# Not currently used
|
||||||
|
if s['status'] == 'error':
|
||||||
|
return cls.ERROR
|
||||||
|
|
||||||
|
return cls.INDETERMINATE if s.get('_percent') is None else cls.VISIBLE
|
||||||
|
|
||||||
|
def get_ansi_escape(self, /, percent=None):
|
||||||
|
percent = 0 if percent is None else int(percent)
|
||||||
|
return f'\033]9;4;{self.value};{percent}\007'
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# Autogenerated by devscripts/update-version.py
|
# Autogenerated by devscripts/update-version.py
|
||||||
|
|
||||||
__version__ = '2025.01.26'
|
__version__ = '2025.02.19'
|
||||||
|
|
||||||
RELEASE_GIT_HEAD = '3b4531934465580be22937fecbb6e1a3a9e2334f'
|
RELEASE_GIT_HEAD = '4985a4041770eaa0016271809a1fd950dc809a55'
|
||||||
|
|
||||||
VARIANT = None
|
VARIANT = None
|
||||||
|
|
||||||
|
@ -12,4 +12,4 @@
|
||||||
|
|
||||||
ORIGIN = 'yt-dlp/yt-dlp'
|
ORIGIN = 'yt-dlp/yt-dlp'
|
||||||
|
|
||||||
_pkg_version = '2025.01.26'
|
_pkg_version = '2025.02.19'
|
||||||
|
|
Loading…
Reference in a new issue