mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-08-08 21:28:38 +03:00
Compare commits
67
Commits
2021.09.25
...
2021.10.09
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a170527e1f | ||
|
|
90d55df330 | ||
|
|
81bcd43a03 | ||
|
|
b5ae35ee6d | ||
|
|
4e3b637d5b | ||
|
|
8cd69fc407 | ||
|
|
2614f64600 | ||
|
|
b922db9fe5 | ||
|
|
f2cad2e496 | ||
|
|
d6124e191e | ||
|
|
8c6f4daa4c | ||
|
|
ac56cf38a4 | ||
|
|
c08b8873ea | ||
|
|
819e05319b | ||
|
|
fee3f44f5f | ||
|
|
705e7c2005 | ||
|
|
49e7e9c3ce | ||
|
|
8472674399 | ||
|
|
1276a43a77 | ||
|
|
519804a92f | ||
|
|
1b6bb4a85a | ||
|
|
644149afec | ||
|
|
4e3d1898a8 | ||
|
|
f85e6be42e | ||
|
|
762e509d91 | ||
|
|
d92125aeba | ||
|
|
0f0ac87be3 | ||
|
|
755203fc3f | ||
|
|
943d5ab133 | ||
|
|
3001a84dca | ||
|
|
ebf2fb4d61 | ||
|
|
efc947fb3e | ||
|
|
b11c04a8ae | ||
|
|
5d535b4a55 | ||
|
|
a1c3967307 | ||
|
|
e919569e67 | ||
|
|
ff1dec819a | ||
|
|
9359f3d4f0 | ||
|
|
0eaec13ba6 | ||
|
|
ad095c4283 | ||
|
|
e6f21b3d92 | ||
|
|
d710cc6d36 | ||
|
|
3ae5e79774 | ||
|
|
8e3fd7e034 | ||
|
|
80c03fa98f | ||
|
|
1f2a268bd3 | ||
|
|
804ca01cc7 | ||
|
|
851876095b | ||
|
|
2d997542ca | ||
|
|
7756277882 | ||
|
|
7687c8ac6e | ||
|
|
80c360d7aa | ||
|
|
250a938de8 | ||
|
|
f1d42a83ab | ||
|
|
3cf4b91dc5 | ||
|
|
fecb20a503 | ||
|
|
360167b9fc | ||
|
|
28234287f1 | ||
|
|
91dd88b90f | ||
|
|
d31dab7084 | ||
|
|
c470901ccf | ||
|
|
2333ea1029 | ||
|
|
9a13345439 | ||
|
|
524e2e4fda | ||
|
|
f440b14f87 | ||
|
|
8dc831f715 | ||
|
|
e99b2d2771 |
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Broken site support
|
||||
about: Report broken or misfunctioning site
|
||||
title: "[Broken]"
|
||||
labels: Broken
|
||||
title: "[Broken] Website Name: A short description of the issue"
|
||||
labels: ['triage', 'extractor-bug']
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -21,18 +21,21 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.09.25. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.10.09. If it's not, see https://github.com/yt-dlp/yt-dlp#update on how to update. Issues with outdated version will be REJECTED.
|
||||
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
||||
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/yt-dlp/yt-dlp.
|
||||
- Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
|
||||
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
||||
- Make sure that all URLs and arguments with special characters are properly quoted or escaped.
|
||||
- Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp/issues. DO NOT post duplicates.
|
||||
- Read "opening an issue" section in CONTRIBUTING.md: https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue
|
||||
- Finally, confirm all RELEVANT tasks from the following by putting x into all the boxes like this [x] (Dont forget to delete the empty space)
|
||||
-->
|
||||
|
||||
- [ ] I'm reporting a broken site support
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.09.25**
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.10.09**
|
||||
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
||||
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
|
||||
- [ ] I've searched the bugtracker for similar issues including closed ones
|
||||
- [ ] I've read the opening an issue section in CONTRIBUTING.md
|
||||
- [ ] I have given an appropriate title to the issue
|
||||
|
||||
|
||||
## Verbose log
|
||||
@@ -44,7 +47,7 @@ Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your com
|
||||
[debug] User config: []
|
||||
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKc']
|
||||
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
||||
[debug] yt-dlp version 2021.09.25
|
||||
[debug] yt-dlp version 2021.10.09
|
||||
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
|
||||
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
||||
[debug] Proxy map: {}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Site support request
|
||||
about: Request support for a new site
|
||||
title: "[Site Request]"
|
||||
labels: Request
|
||||
title: "[Site Request] Website Name"
|
||||
labels: ['triage', 'site-request']
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -21,19 +21,22 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.09.25. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.10.09. If it's not, see https://github.com/yt-dlp/yt-dlp#update on how to update. Issues with outdated version will be REJECTED.
|
||||
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
||||
- Make sure that site you are requesting is not dedicated to copyright infringement, see https://github.com/yt-dlp/yt-dlp. yt-dlp does not support such sites. In order for site support request to be accepted all provided example URLs should not violate any copyrights.
|
||||
- Search the bugtracker for similar site support requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
|
||||
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
||||
- Make sure that site you are requesting is not dedicated to copyright infringement. yt-dlp does not support such sites. In order for site support request to be accepted all provided example URLs should not violate any copyrights.
|
||||
- Search the bugtracker for similar site support requests: https://github.com/yt-dlp/yt-dlp/issues. DO NOT post duplicates.
|
||||
- Read "opening an issue" section in CONTRIBUTING.md: https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue
|
||||
- Finally, confirm all RELEVANT tasks from the following by putting x into all the boxes like this [x] (Dont forget to delete the empty space)
|
||||
-->
|
||||
|
||||
- [ ] I'm reporting a new site support request
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.09.25**
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.10.09**
|
||||
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
||||
- [ ] I've checked that none of provided URLs violate any copyrights
|
||||
- [ ] The provided URLs do not contain any DRM to the best of my knowledge
|
||||
- [ ] I've searched the bugtracker for similar site support requests including closed ones
|
||||
- [ ] I've read the opening an issue section in CONTRIBUTING.md
|
||||
- [ ] I have given an appropriate title to the issue
|
||||
|
||||
|
||||
## Example URLs
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Site feature request
|
||||
about: Request a new functionality for a site
|
||||
title: "[Site Request]"
|
||||
labels: Request
|
||||
title: "[Site Feature] Website Name: A short description of the feature"
|
||||
labels: ['triage', 'site-enhancement']
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -21,14 +21,17 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.09.25. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- Search the bugtracker for similar site feature requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
|
||||
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.10.09. If it's not, see https://github.com/yt-dlp/yt-dlp#update on how to update. Issues with outdated version will be REJECTED.
|
||||
- Search the bugtracker for similar site feature requests: https://github.com/yt-dlp/yt-dlp/issues. DO NOT post duplicates.
|
||||
- Read "opening an issue" section in CONTRIBUTING.md: https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue
|
||||
- Finally, confirm all RELEVANT tasks from the following by putting x into all the boxes like this [x] (Dont forget to delete the empty space)
|
||||
-->
|
||||
|
||||
- [ ] I'm reporting a site feature request
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.09.25**
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.10.09**
|
||||
- [ ] I've searched the bugtracker for similar site feature requests including closed ones
|
||||
- [ ] I've read the opening an issue section in CONTRIBUTING.md
|
||||
- [ ] I have given an appropriate title to the issue
|
||||
|
||||
|
||||
## Description
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a bug unrelated to any particular site or extractor
|
||||
title: ''
|
||||
labels: ''
|
||||
title: '[Bug] A short description of the issue'
|
||||
labels: ['triage', 'bug']
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -21,21 +21,22 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.09.25. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.10.09. If it's not, see https://github.com/yt-dlp/yt-dlp#update on how to update. Issues with outdated version will be REJECTED.
|
||||
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
||||
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/yt-dlp/yt-dlp.
|
||||
- Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
|
||||
- Read bugs section in FAQ: https://github.com/yt-dlp/yt-dlp
|
||||
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
||||
- Make sure that all URLs and arguments with special characters are properly quoted or escaped.
|
||||
- Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp/issues. DO NOT post duplicates.
|
||||
- Read "opening an issue" section in CONTRIBUTING.md: https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue
|
||||
- Finally, confirm all RELEVANT tasks from the following by putting x into all the boxes like this [x] (Dont forget to delete the empty space)
|
||||
-->
|
||||
|
||||
- [ ] I'm reporting a bug unrelated to a specific site
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.09.25**
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.10.09**
|
||||
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
||||
- [ ] The provided URLs do not contain any DRM to the best of my knowledge
|
||||
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
|
||||
- [ ] I've searched the bugtracker for similar bug reports including closed ones
|
||||
- [ ] I've read bugs section in FAQ
|
||||
- [ ] I've read the opening an issue section in CONTRIBUTING.md
|
||||
- [ ] I have given an appropriate title to the issue
|
||||
|
||||
|
||||
## Verbose log
|
||||
@@ -47,7 +48,7 @@ Add the `-v` flag to your command line you run yt-dlp with (`yt-dlp -v <your com
|
||||
[debug] User config: []
|
||||
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKc']
|
||||
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
||||
[debug] yt-dlp version 2021.09.25
|
||||
[debug] yt-dlp version 2021.10.09
|
||||
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
|
||||
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
||||
[debug] Proxy map: {}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Request a new functionality unrelated to any particular site or extractor
|
||||
title: "[Feature Request]"
|
||||
labels: Request
|
||||
title: "[Feature Request] A short description of your feature"
|
||||
labels: ['triage', 'enhancement']
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -21,14 +21,17 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.09.25. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- Search the bugtracker for similar feature requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.10.09. If it's not, see https://github.com/yt-dlp/yt-dlp#update on how to update. Issues with outdated version will be REJECTED.
|
||||
- Search the bugtracker for similar feature requests: https://github.com/yt-dlp/yt-dlp/issues. DO NOT post duplicates.
|
||||
- Read "opening an issue" section in CONTRIBUTING.md: https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue
|
||||
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
||||
-->
|
||||
|
||||
- [ ] I'm reporting a feature request
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.09.25**
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.10.09**
|
||||
- [ ] I've searched the bugtracker for similar feature requests including closed ones
|
||||
- [ ] I've read the opening an issue section in CONTRIBUTING.md
|
||||
- [ ] I have given an appropriate title to the issue
|
||||
|
||||
|
||||
## Description
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: Ask question
|
||||
about: Ask yt-dlp related question
|
||||
title: "[Question]"
|
||||
title: "[Question] A short description of your question"
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
@@ -21,14 +21,17 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- Look through the README (https://github.com/yt-dlp/yt-dlp) and FAQ (https://github.com/yt-dlp/yt-dlp) for similar questions
|
||||
- Search the bugtracker for similar questions: https://github.com/yt-dlp/yt-dlp
|
||||
- Look through the README (https://github.com/yt-dlp/yt-dlp)
|
||||
- Read "opening an issue" section in CONTRIBUTING.md: https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue
|
||||
- Search the bugtracker for similar questions: https://github.com/yt-dlp/yt-dlp/issues
|
||||
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
||||
-->
|
||||
|
||||
- [ ] I'm asking a question
|
||||
- [ ] I've looked through the README and FAQ for similar questions
|
||||
- [ ] I've looked through the README
|
||||
- [ ] I've read the opening an issue section in CONTRIBUTING.md
|
||||
- [ ] I've searched the bugtracker for similar questions including closed ones
|
||||
- [ ] I have given an appropriate title to the issue
|
||||
|
||||
|
||||
## Question
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Broken site support
|
||||
about: Report broken or misfunctioning site
|
||||
title: "[Broken]"
|
||||
labels: Broken
|
||||
title: "[Broken] Website Name: A short description of the issue"
|
||||
labels: ['triage', 'extractor-bug']
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -21,11 +21,12 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is %(version)s. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is %(version)s. If it's not, see https://github.com/yt-dlp/yt-dlp#update on how to update. Issues with outdated version will be REJECTED.
|
||||
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
||||
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/yt-dlp/yt-dlp.
|
||||
- Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
|
||||
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
||||
- Make sure that all URLs and arguments with special characters are properly quoted or escaped.
|
||||
- Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp/issues. DO NOT post duplicates.
|
||||
- Read "opening an issue" section in CONTRIBUTING.md: https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue
|
||||
- Finally, confirm all RELEVANT tasks from the following by putting x into all the boxes like this [x] (Dont forget to delete the empty space)
|
||||
-->
|
||||
|
||||
- [ ] I'm reporting a broken site support
|
||||
@@ -33,6 +34,8 @@ Carefully read and work through this check list in order to prevent the most com
|
||||
- [ ] I've checked that all provided URLs are alive and playable in a browser
|
||||
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
|
||||
- [ ] I've searched the bugtracker for similar issues including closed ones
|
||||
- [ ] I've read the opening an issue section in CONTRIBUTING.md
|
||||
- [ ] I have given an appropriate title to the issue
|
||||
|
||||
|
||||
## Verbose log
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Site support request
|
||||
about: Request support for a new site
|
||||
title: "[Site Request]"
|
||||
labels: Request
|
||||
title: "[Site Request] Website Name"
|
||||
labels: ['triage', 'site-request']
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -21,11 +21,12 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is %(version)s. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is %(version)s. If it's not, see https://github.com/yt-dlp/yt-dlp#update on how to update. Issues with outdated version will be REJECTED.
|
||||
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
||||
- Make sure that site you are requesting is not dedicated to copyright infringement, see https://github.com/yt-dlp/yt-dlp. yt-dlp does not support such sites. In order for site support request to be accepted all provided example URLs should not violate any copyrights.
|
||||
- Search the bugtracker for similar site support requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
|
||||
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
||||
- Make sure that site you are requesting is not dedicated to copyright infringement. yt-dlp does not support such sites. In order for site support request to be accepted all provided example URLs should not violate any copyrights.
|
||||
- Search the bugtracker for similar site support requests: https://github.com/yt-dlp/yt-dlp/issues. DO NOT post duplicates.
|
||||
- Read "opening an issue" section in CONTRIBUTING.md: https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue
|
||||
- Finally, confirm all RELEVANT tasks from the following by putting x into all the boxes like this [x] (Dont forget to delete the empty space)
|
||||
-->
|
||||
|
||||
- [ ] I'm reporting a new site support request
|
||||
@@ -34,6 +35,8 @@ Carefully read and work through this check list in order to prevent the most com
|
||||
- [ ] I've checked that none of provided URLs violate any copyrights
|
||||
- [ ] The provided URLs do not contain any DRM to the best of my knowledge
|
||||
- [ ] I've searched the bugtracker for similar site support requests including closed ones
|
||||
- [ ] I've read the opening an issue section in CONTRIBUTING.md
|
||||
- [ ] I have given an appropriate title to the issue
|
||||
|
||||
|
||||
## Example URLs
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Site feature request
|
||||
about: Request a new functionality for a site
|
||||
title: "[Site Request]"
|
||||
labels: Request
|
||||
title: "[Site Feature] Website Name: A short description of the feature"
|
||||
labels: ['triage', 'site-enhancement']
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -21,14 +21,17 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is %(version)s. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- Search the bugtracker for similar site feature requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
|
||||
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is %(version)s. If it's not, see https://github.com/yt-dlp/yt-dlp#update on how to update. Issues with outdated version will be REJECTED.
|
||||
- Search the bugtracker for similar site feature requests: https://github.com/yt-dlp/yt-dlp/issues. DO NOT post duplicates.
|
||||
- Read "opening an issue" section in CONTRIBUTING.md: https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue
|
||||
- Finally, confirm all RELEVANT tasks from the following by putting x into all the boxes like this [x] (Dont forget to delete the empty space)
|
||||
-->
|
||||
|
||||
- [ ] I'm reporting a site feature request
|
||||
- [ ] I've verified that I'm running yt-dlp version **%(version)s**
|
||||
- [ ] I've searched the bugtracker for similar site feature requests including closed ones
|
||||
- [ ] I've read the opening an issue section in CONTRIBUTING.md
|
||||
- [ ] I have given an appropriate title to the issue
|
||||
|
||||
|
||||
## Description
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a bug unrelated to any particular site or extractor
|
||||
title: ''
|
||||
labels: ''
|
||||
title: '[Bug] A short description of the issue'
|
||||
labels: ['triage', 'bug']
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -21,12 +21,12 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is %(version)s. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is %(version)s. If it's not, see https://github.com/yt-dlp/yt-dlp#update on how to update. Issues with outdated version will be REJECTED.
|
||||
- Make sure that all provided video/audio/playlist URLs (if any) are alive and playable in a browser.
|
||||
- Make sure that all URLs and arguments with special characters are properly quoted or escaped as explained in https://github.com/yt-dlp/yt-dlp.
|
||||
- Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
|
||||
- Read bugs section in FAQ: https://github.com/yt-dlp/yt-dlp
|
||||
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
||||
- Make sure that all URLs and arguments with special characters are properly quoted or escaped.
|
||||
- Search the bugtracker for similar issues: https://github.com/yt-dlp/yt-dlp/issues. DO NOT post duplicates.
|
||||
- Read "opening an issue" section in CONTRIBUTING.md: https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue
|
||||
- Finally, confirm all RELEVANT tasks from the following by putting x into all the boxes like this [x] (Dont forget to delete the empty space)
|
||||
-->
|
||||
|
||||
- [ ] I'm reporting a bug unrelated to a specific site
|
||||
@@ -35,7 +35,8 @@ Carefully read and work through this check list in order to prevent the most com
|
||||
- [ ] The provided URLs do not contain any DRM to the best of my knowledge
|
||||
- [ ] I've checked that all URLs and arguments with special characters are properly quoted or escaped
|
||||
- [ ] I've searched the bugtracker for similar bug reports including closed ones
|
||||
- [ ] I've read bugs section in FAQ
|
||||
- [ ] I've read the opening an issue section in CONTRIBUTING.md
|
||||
- [ ] I have given an appropriate title to the issue
|
||||
|
||||
|
||||
## Verbose log
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Request a new functionality unrelated to any particular site or extractor
|
||||
title: "[Feature Request]"
|
||||
labels: Request
|
||||
title: "[Feature Request] A short description of your feature"
|
||||
labels: ['triage', 'enhancement']
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -21,14 +21,17 @@ assignees: ''
|
||||
|
||||
<!--
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is %(version)s. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- Search the bugtracker for similar feature requests: https://github.com/yt-dlp/yt-dlp. DO NOT post duplicates.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is %(version)s. If it's not, see https://github.com/yt-dlp/yt-dlp#update on how to update. Issues with outdated version will be REJECTED.
|
||||
- Search the bugtracker for similar feature requests: https://github.com/yt-dlp/yt-dlp/issues. DO NOT post duplicates.
|
||||
- Read "opening an issue" section in CONTRIBUTING.md: https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue
|
||||
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
|
||||
-->
|
||||
|
||||
- [ ] I'm reporting a feature request
|
||||
- [ ] I've verified that I'm running yt-dlp version **%(version)s**
|
||||
- [ ] I've searched the bugtracker for similar feature requests including closed ones
|
||||
- [ ] I've read the opening an issue section in CONTRIBUTING.md
|
||||
- [ ] I have given an appropriate title to the issue
|
||||
|
||||
|
||||
## Description
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
---
|
||||
|
||||
### Before submitting a *pull request* make sure you have:
|
||||
- [ ] At least skimmed through [adding new extractor tutorial](https://github.com/ytdl-org/youtube-dl#adding-support-for-a-new-site) and [youtube-dl coding conventions](https://github.com/ytdl-org/youtube-dl#youtube-dl-coding-conventions) sections
|
||||
- [ ] 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
|
||||
- [ ] Checked the code with [flake8](https://pypi.python.org/pypi/flake8)
|
||||
|
||||
|
||||
@@ -154,14 +154,14 @@ jobs:
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
- name: Install Requirements
|
||||
# Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
|
||||
run: pip install "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodome websockets
|
||||
run: pip install "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodomex websockets
|
||||
- name: Bump version
|
||||
id: bump_version
|
||||
run: python devscripts/update-version.py
|
||||
- name: Print version
|
||||
run: echo "${{ steps.bump_version.outputs.ytdlp_version }}"
|
||||
- name: Run PyInstaller Script
|
||||
run: python pyinst.py 64
|
||||
run: python pyinst.py
|
||||
- name: Upload yt-dlp.exe Windows binary
|
||||
id: upload-release-windows
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -179,7 +179,7 @@ jobs:
|
||||
id: sha512_win
|
||||
run: echo "::set-output name=sha512_win::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA512).Hash.ToLower())"
|
||||
- name: Run PyInstaller Script with --onedir
|
||||
run: python pyinst.py 64 --onedir
|
||||
run: python pyinst.py --onedir
|
||||
- uses: papeloto/action-zip@v1
|
||||
with:
|
||||
files: ./dist/yt-dlp
|
||||
@@ -220,14 +220,14 @@ jobs:
|
||||
- name: Upgrade pip and enable wheel support
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
- name: Install Requirements
|
||||
run: pip install "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodome websockets
|
||||
run: pip install "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodomex websockets
|
||||
- name: Bump version
|
||||
id: bump_version
|
||||
run: python devscripts/update-version.py
|
||||
- name: Print version
|
||||
run: echo "${{ steps.bump_version.outputs.ytdlp_version }}"
|
||||
- name: Run PyInstaller Script for 32 Bit
|
||||
run: python pyinst.py 32
|
||||
run: python pyinst.py
|
||||
- name: Upload Executable yt-dlp_x86.exe
|
||||
id: upload-release-windows32
|
||||
uses: actions/upload-release-asset@v1
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Install test requirements
|
||||
run: pip install pytest pycryptodome
|
||||
run: pip install pytest pycryptodomex
|
||||
- name: Run tests
|
||||
run: ./devscripts/run_tests.sh core
|
||||
flake8:
|
||||
|
||||
+194
-75
@@ -1,26 +1,59 @@
|
||||
**Please include the full output of youtube-dl when run with `-v`**, i.e. **add** `-v` flag to **your command line**, copy the **whole** output and post it in the issue body wrapped in \`\`\` for better formatting. It should look similar to this:
|
||||
# CONTRIBUTING TO YT-DLP
|
||||
|
||||
- [OPENING AN ISSUE](#opening-an-issue)
|
||||
- [Is the description of the issue itself sufficient?](#is-the-description-of-the-issue-itself-sufficient)
|
||||
- [Are you using the latest version?](#are-you-using-the-latest-version)
|
||||
- [Is the issue already documented?](#is-the-issue-already-documented)
|
||||
- [Why are existing options not enough?](#why-are-existing-options-not-enough)
|
||||
- [Have you read and understood the changes, between youtube-dl and yt-dlp](#have-you-read-and-understood-the-changes-between-youtube-dl-and-yt-dlp)
|
||||
- [Is there enough context in your bug report?](#is-there-enough-context-in-your-bug-report)
|
||||
- [Does the issue involve one problem, and one problem only?](#does-the-issue-involve-one-problem-and-one-problem-only)
|
||||
- [Is anyone going to need the feature?](#is-anyone-going-to-need-the-feature)
|
||||
- [Is your question about yt-dlp?](#is-your-question-about-yt-dlp)
|
||||
- [DEVELOPER INSTRUCTIONS](#developer-instructions)
|
||||
- [Adding new feature or making overarching changes](#adding-new-feature-or-making-overarching-changes)
|
||||
- [Adding support for a new site](#adding-support-for-a-new-site)
|
||||
- [yt-dlp coding conventions](#yt-dlp-coding-conventions)
|
||||
- [Mandatory and optional metafields](#mandatory-and-optional-metafields)
|
||||
- [Provide fallbacks](#provide-fallbacks)
|
||||
- [Regular expressions](#regular-expressions)
|
||||
- [Long lines policy](#long-lines-policy)
|
||||
- [Inline values](#inline-values)
|
||||
- [Collapse fallbacks](#collapse-fallbacks)
|
||||
- [Trailing parentheses](#trailing-parentheses)
|
||||
- [Use convenience conversion and parsing functions](#use-convenience-conversion-and-parsing-functions)
|
||||
- [EMBEDDING YT-DLP](README.md#embedding-yt-dlp)
|
||||
|
||||
|
||||
|
||||
# OPENING AN ISSUE
|
||||
|
||||
Bugs and suggestions should be reported at: [yt-dlp/yt-dlp/issues](https://github.com/yt-dlp/yt-dlp/issues). Unless you were prompted to or there is another pertinent reason (e.g. GitHub fails to accept the bug report), please do not send bug reports via personal email. For discussions, join us in our [discord server](https://discord.gg/H5MNcFW63r).
|
||||
|
||||
**Please include the full output of yt-dlp when run with `-Uv`**, i.e. **add** `-Uv` flag to **your command line**, copy the **whole** output and post it in the issue body wrapped in \`\`\` for better formatting. It should look similar to this:
|
||||
```
|
||||
$ youtube-dl -v <your command line>
|
||||
[debug] System config: []
|
||||
[debug] User config: []
|
||||
[debug] Command-line args: [u'-v', u'https://www.youtube.com/watch?v=BaW_jenozKc']
|
||||
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
||||
[debug] youtube-dl version 2015.12.06
|
||||
[debug] Git HEAD: 135392e
|
||||
[debug] Python version 2.6.6 - Windows-2003Server-5.2.3790-SP2
|
||||
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
||||
$ yt-dlp -Uv <your command line>
|
||||
[debug] Command-line config: ['-v', 'demo.com']
|
||||
[debug] Encodings: locale UTF-8, fs utf-8, out utf-8, pref UTF-8
|
||||
[debug] yt-dlp version 2021.09.25 (zip)
|
||||
[debug] Python version 3.8.10 (CPython 64bit) - Linux-5.4.0-74-generic-x86_64-with-glibc2.29
|
||||
[debug] exe versions: ffmpeg 4.2.4, ffprobe 4.2.4
|
||||
[debug] Proxy map: {}
|
||||
Current Build Hash 25cc412d1d3c0725a1f2f5b7e4682f6fb40e6d15f7024e96f7afd572e9919535
|
||||
yt-dlp is up to date (2021.09.25)
|
||||
...
|
||||
```
|
||||
**Do not post screenshots of verbose logs; only plain text is acceptable.**
|
||||
|
||||
The output (including the first lines) contains important debugging information. Issues without the full output are often not reproducible and therefore do not get solved in short order, if ever.
|
||||
The output (including the first lines) contains important debugging information. Issues without the full output are often not reproducible and therefore will be closed as `incomplete`.
|
||||
|
||||
The templates provided for the Issues, should be completed and **not removed**, this helps aide the resolution of the issue.
|
||||
|
||||
Please re-read your issue once again to avoid a couple of common mistakes (you can and should use this as a checklist):
|
||||
|
||||
### Is the description of the issue itself sufficient?
|
||||
|
||||
We often get issue reports that we cannot really decipher. While in most cases we eventually get the required information after asking back multiple times, this poses an unnecessary drain on our resources. Many contributors, including myself, are also not native speakers, so we may misread some parts.
|
||||
We often get issue reports that we cannot really decipher. While in most cases we eventually get the required information after asking back multiple times, this poses an unnecessary drain on our resources.
|
||||
|
||||
So please elaborate on what feature you are requesting, or what bug you want to be fixed. Make sure that it's obvious
|
||||
|
||||
@@ -28,25 +61,31 @@ So please elaborate on what feature you are requesting, or what bug you want to
|
||||
- How it could be fixed
|
||||
- How your proposed solution would look like
|
||||
|
||||
If your report is shorter than two lines, it is almost certainly missing some of these, which makes it hard for us to respond to it. We're often too polite to close the issue outright, but the missing info makes misinterpretation likely. As a committer myself, I often get frustrated by these issues, since the only possible way for me to move forward on them is to ask for clarification over and over.
|
||||
If your report is shorter than two lines, it is almost certainly missing some of these, which makes it hard for us to respond to it. We're often too polite to close the issue outright, but the missing info makes misinterpretation likely. We often get frustrated by these issues, since the only possible way for us to move forward on them is to ask for clarification over and over.
|
||||
|
||||
For bug reports, this means that your report should contain the *complete* output of youtube-dl when called with the `-v` flag. The error message you get for (most) bugs even says so, but you would not believe how many of our bug reports do not contain this information.
|
||||
For bug reports, this means that your report should contain the **complete** output of yt-dlp when called with the `-Uv` flag. The error message you get for (most) bugs even says so, but you would not believe how many of our bug reports do not contain this information.
|
||||
|
||||
If your server has multiple IPs or you suspect censorship, adding `--call-home` may be a good idea to get more diagnostics. If the error is `ERROR: Unable to extract ...` and you cannot reproduce it from multiple countries, add `--dump-pages` (warning: this will yield a rather large output, redirect it to the file `log.txt` by adding `>log.txt 2>&1` to your command-line) or upload the `.dump` files you get when you add `--write-pages` [somewhere](https://gist.github.com/).
|
||||
If the error is `ERROR: Unable to extract ...` and you cannot reproduce it from multiple countries, add `--write-pages` and upload the `.dump` files you get [somewhere](https://gist.github.com).
|
||||
|
||||
**Site support requests must contain an example URL**. An example URL is a URL you might want to download, like `https://www.youtube.com/watch?v=BaW_jenozKc`. There should be an obvious video present. Except under very special circumstances, the main page of a video service (e.g. `https://www.youtube.com/`) is *not* an example URL.
|
||||
|
||||
### Are you using the latest version?
|
||||
|
||||
Before reporting any issue, type `youtube-dl -U`. This should report that you're up-to-date. About 20% of the reports we receive are already fixed, but people are using outdated versions. This goes for feature requests as well.
|
||||
Before reporting any issue, type `yt-dlp -U`. This should report that you're up-to-date. This goes for feature requests as well.
|
||||
|
||||
### Is the issue already documented?
|
||||
|
||||
Make sure that someone has not already opened the issue you're trying to open. Search at the top of the window or browse the [GitHub Issues](https://github.com/ytdl-org/youtube-dl/search?type=Issues) of this repository. If there is an issue, feel free to write something along the lines of "This affects me as well, with version 2015.01.01. Here is some more information on the issue: ...". While some issues may be old, a new post into them often spurs rapid activity.
|
||||
Make sure that someone has not already opened the issue you're trying to open. Search at the top of the window or browse the [GitHub Issues](https://github.com/yt-dlp/yt-dlp/search?type=Issues) of this repository. If there is an issue, feel free to write something along the lines of "This affects me as well, with version 2021.01.01. Here is some more information on the issue: ...". While some issues may be old, a new post into them often spurs rapid activity.
|
||||
|
||||
Additionally, it is also helpful to see if the issue has already been documented in the [youtube-dl issue tracker](https://github.com/ytdl-org/youtube-dl/issues). If similar issues have already been reported in youtube-dl (but not in our issue tracker), links to them can be included in your issue report here.
|
||||
|
||||
### Why are existing options not enough?
|
||||
|
||||
Before requesting a new feature, please have a quick peek at [the list of supported options](https://github.com/ytdl-org/youtube-dl/blob/master/README.md#options). Many feature requests are for features that actually exist already! Please, absolutely do show off your work in the issue report and detail how the existing similar options do *not* solve your problem.
|
||||
Before requesting a new feature, please have a quick peek at [the list of supported options](README.md#usage-and-options). Many feature requests are for features that actually exist already! Please, absolutely do show off your work in the issue report and detail how the existing similar options do *not* solve your problem.
|
||||
|
||||
### Have you read and understood the changes, between youtube-dl and yt-dlp
|
||||
|
||||
There are many changes between youtube-dl and yt-dlp [(changes to default behavior)](README.md#differences-in-default-behavior), and some of the options available have a different behaviour in yt-dlp, or have been removed all together [(list of changes to options)](README.md#deprecated-options). Make sure you have read and understand the differences in the options and how this may impact your downloads before opening an issue.
|
||||
|
||||
### Is there enough context in your bug report?
|
||||
|
||||
@@ -58,23 +97,28 @@ We are then presented with a very complicated request when the original problem
|
||||
|
||||
Some of our users seem to think there is a limit of issues they can or should open. There is no limit of issues they can or should open. While it may seem appealing to be able to dump all your issues into one ticket, that means that someone who solves one of your issues cannot mark the issue as closed. Typically, reporting a bunch of issues leads to the ticket lingering since nobody wants to attack that behemoth, until someone mercifully splits the issue into multiple ones.
|
||||
|
||||
In particular, every site support request issue should only pertain to services at one site (generally under a common domain, but always using the same backend technology). Do not request support for vimeo user videos, White house podcasts, and Google Plus pages in the same issue. Also, make sure that you don't post bug reports alongside feature requests. As a rule of thumb, a feature request does not include outputs of youtube-dl that are not immediately related to the feature at hand. Do not post reports of a network error alongside the request for a new video service.
|
||||
In particular, every site support request issue should only pertain to services at one site (generally under a common domain, but always using the same backend technology). Do not request support for vimeo user videos, White house podcasts, and Google Plus pages in the same issue. Also, make sure that you don't post bug reports alongside feature requests. As a rule of thumb, a feature request does not include outputs of yt-dlp that are not immediately related to the feature at hand. Do not post reports of a network error alongside the request for a new video service.
|
||||
|
||||
### Is anyone going to need the feature?
|
||||
|
||||
Only post features that you (or an incapacitated friend you can personally talk to) require. Do not post features because they seem like a good idea. If they are really useful, they will be requested by someone who requires them.
|
||||
|
||||
### Is your question about youtube-dl?
|
||||
### Is your question about yt-dlp?
|
||||
|
||||
Some bug reports are completely unrelated to yt-dlp and relate to a different, or even the reporter's own, application. Please make sure that you are actually using yt-dlp. If you are using a UI for yt-dlp, report the bug to the maintainer of the actual application providing the UI. On the other hand, if your UI for yt-dlp fails in some way you believe is related to yt-dlp, by all means, go ahead and report the bug.
|
||||
|
||||
If the issue is with `youtube-dl` (the upstream fork of yt-dlp) and not with yt-dlp, the issue should be raised in the youtube-dl project.
|
||||
|
||||
|
||||
|
||||
It may sound strange, but some bug reports we receive are completely unrelated to youtube-dl and relate to a different, or even the reporter's own, application. Please make sure that you are actually using youtube-dl. If you are using a UI for youtube-dl, report the bug to the maintainer of the actual application providing the UI. On the other hand, if your UI for youtube-dl fails in some way you believe is related to youtube-dl, by all means, go ahead and report the bug.
|
||||
|
||||
# DEVELOPER INSTRUCTIONS
|
||||
|
||||
Most users do not need to build youtube-dl and can [download the builds](https://ytdl-org.github.io/youtube-dl/download.html) or get them from their distribution.
|
||||
Most users do not need to build yt-dlp and can [download the builds](https://github.com/yt-dlp/yt-dlp/releases) or get them via [the other installation methods](README.md#installation).
|
||||
|
||||
To run youtube-dl as a developer, you don't need to build anything either. Simply execute
|
||||
To run yt-dlp as a developer, you don't need to build anything either. Simply execute
|
||||
|
||||
python -m youtube_dl
|
||||
python -m yt_dlp
|
||||
|
||||
To run the test, simply invoke your favorite test runner, or execute a test file directly; any of the following work:
|
||||
|
||||
@@ -85,42 +129,42 @@ To run the test, simply invoke your favorite test runner, or execute a test file
|
||||
|
||||
See item 6 of [new extractor tutorial](#adding-support-for-a-new-site) for how to run extractor specific test cases.
|
||||
|
||||
If you want to create a build of youtube-dl yourself, you'll need
|
||||
If you want to create a build of yt-dlp yourself, you can follow the instructions [here](README.md#compile).
|
||||
|
||||
* python3
|
||||
* make (only GNU make is supported)
|
||||
* pandoc
|
||||
* zip
|
||||
* pytest
|
||||
|
||||
### Adding support for a new site
|
||||
## Adding new feature or making overarching changes
|
||||
|
||||
If you want to add support for a new site, first of all **make sure** this site is **not dedicated to [copyright infringement](README.md#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free)**. youtube-dl does **not support** such sites thus pull requests adding support for them **will be rejected**.
|
||||
Before you start writing code for implementing a new feature, open an issue explaining your feature request and atleast one use case. This allows the maintainers to decide whether such a feature is desired for the project in the first place, and will provide an avenue to discuss some implementation details. If you open a pull request for a new feature without discussing with us first, do not be surprised when we ask for large changes to the code, or even reject it outright.
|
||||
|
||||
The same applies for overarching changes to the architecture, documentation or code style
|
||||
|
||||
|
||||
## Adding support for a new site
|
||||
|
||||
If you want to add support for a new site, first of all **make sure** this site is **not dedicated to [copyright infringement](https://www.github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free)**. yt-dlp does **not support** such sites thus pull requests adding support for them **will be rejected**.
|
||||
|
||||
After you have ensured this site is distributing its content legally, you can follow this quick list (assuming your service is called `yourextractor`):
|
||||
|
||||
1. [Fork this repository](https://github.com/ytdl-org/youtube-dl/fork)
|
||||
2. Check out the source code with:
|
||||
1. [Fork this repository](https://github.com/yt-dlp/yt-dlp/fork)
|
||||
1. Check out the source code with:
|
||||
|
||||
git clone git@github.com:YOUR_GITHUB_USERNAME/youtube-dl.git
|
||||
git clone git@github.com:YOUR_GITHUB_USERNAME/yt-dlp.git
|
||||
|
||||
3. Start a new git branch with
|
||||
1. Start a new git branch with
|
||||
|
||||
cd youtube-dl
|
||||
cd yt-dlp
|
||||
git checkout -b yourextractor
|
||||
|
||||
4. Start with this simple template and save it to `youtube_dl/extractor/yourextractor.py`:
|
||||
1. Start with this simple template and save it to `yt_dlp/extractor/yourextractor.py`:
|
||||
|
||||
```python
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
|
||||
|
||||
|
||||
|
||||
class YourExtractorIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?yourextractor\.com/watch/(?P<id>[0-9]+)'
|
||||
_TEST = {
|
||||
_TESTS = [{
|
||||
'url': 'https://yourextractor.com/watch/42',
|
||||
'md5': 'TODO: md5 sum of the first 10241 bytes of the video file (use --test)',
|
||||
'info_dict': {
|
||||
@@ -134,12 +178,12 @@ After you have ensured this site is distributing its content legally, you can fo
|
||||
# * A regular expression; start the string with re:
|
||||
# * Any Python type (for example int or float)
|
||||
}
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
|
||||
# TODO more code goes here, for example ...
|
||||
title = self._html_search_regex(r'<h1>(.+?)</h1>', webpage, 'title')
|
||||
|
||||
@@ -148,45 +192,48 @@ After you have ensured this site is distributing its content legally, you can fo
|
||||
'title': title,
|
||||
'description': self._og_search_description(webpage),
|
||||
'uploader': self._search_regex(r'<div[^>]+id="uploader"[^>]*>([^<]+)<', webpage, 'uploader', fatal=False),
|
||||
# TODO more properties (see youtube_dl/extractor/common.py)
|
||||
# TODO more properties (see yt_dlp/extractor/common.py)
|
||||
}
|
||||
```
|
||||
5. Add an import in [`youtube_dl/extractor/extractors.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/extractors.py).
|
||||
6. Run `python test/test_download.py TestDownload.test_YourExtractor`. This *should fail* at first, but you can continually re-run it until you're done. If you decide to add more than one test, then rename ``_TEST`` to ``_TESTS`` and make it into a list of dictionaries. The tests will then be named `TestDownload.test_YourExtractor`, `TestDownload.test_YourExtractor_1`, `TestDownload.test_YourExtractor_2`, etc. Note that tests with `only_matching` key in test's dict are not counted in.
|
||||
7. Have a look at [`youtube_dl/extractor/common.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should and may return](https://github.com/ytdl-org/youtube-dl/blob/7f41a598b3fba1bcab2817de64a08941200aa3c8/youtube_dl/extractor/common.py#L94-L303). Add tests and code for as many as you want.
|
||||
8. Make sure your code follows [youtube-dl coding conventions](#youtube-dl-coding-conventions) and check the code with [flake8](https://flake8.pycqa.org/en/latest/index.html#quickstart):
|
||||
1. Add an import in [`yt_dlp/extractor/extractors.py`](yt_dlp/extractor/extractors.py).
|
||||
1. Run `python test/test_download.py TestDownload.test_YourExtractor`. This *should fail* at first, but you can continually re-run it until you're done. If you decide to add more than one test, the tests will then be named `TestDownload.test_YourExtractor`, `TestDownload.test_YourExtractor_1`, `TestDownload.test_YourExtractor_2`, etc. Note that tests with `only_matching` key in test's dict are not counted in. You can also run all the tests in one go with `TestDownload.test_YourExtractor_all`
|
||||
1. Make sure you have atleast one test for your extractor. Even if all videos covered by the extractor are expected to be inaccessible for automated testing, tests should still be added with a `skip` parameter indicating why the purticular test is disabled from running.
|
||||
1. Have a look at [`yt_dlp/extractor/common.py`](yt_dlp/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should and may return](yt_dlp/extractor/common.py#L91-L426). Add tests and code for as many as you want.
|
||||
1. Make sure your code follows [yt-dlp coding conventions](#yt-dlp-coding-conventions) and check the code with [flake8](https://flake8.pycqa.org/en/latest/index.html#quickstart):
|
||||
|
||||
$ flake8 youtube_dl/extractor/yourextractor.py
|
||||
$ flake8 yt_dlp/extractor/yourextractor.py
|
||||
|
||||
9. Make sure your code works under all [Python](https://www.python.org/) versions claimed supported by youtube-dl, namely 2.6, 2.7, and 3.2+.
|
||||
10. When the tests pass, [add](https://git-scm.com/docs/git-add) the new files and [commit](https://git-scm.com/docs/git-commit) them and [push](https://git-scm.com/docs/git-push) the result, like this:
|
||||
1. Make sure your code works under all [Python](https://www.python.org/) versions supported by yt-dlp, namely CPython and PyPy for Python 3.6 and above. Backward compatability is not required for even older versions of Python.
|
||||
1. When the tests pass, [add](https://git-scm.com/docs/git-add) the new files, [commit](https://git-scm.com/docs/git-commit) them and [push](https://git-scm.com/docs/git-push) the result, like this:
|
||||
|
||||
$ git add youtube_dl/extractor/extractors.py
|
||||
$ git add youtube_dl/extractor/yourextractor.py
|
||||
$ git commit -m '[yourextractor] Add new extractor'
|
||||
$ git add yt_dlp/extractor/extractors.py
|
||||
$ git add yt_dlp/extractor/yourextractor.py
|
||||
$ git commit -m '[yourextractor] Add extractor'
|
||||
$ git push origin yourextractor
|
||||
|
||||
11. Finally, [create a pull request](https://help.github.com/articles/creating-a-pull-request). We'll then review and merge it.
|
||||
1. Finally, [create a pull request](https://help.github.com/articles/creating-a-pull-request). We'll then review and merge it.
|
||||
|
||||
In any case, thank you very much for your contributions!
|
||||
|
||||
## youtube-dl coding conventions
|
||||
|
||||
## yt-dlp coding conventions
|
||||
|
||||
This section introduces a guide lines for writing idiomatic, robust and future-proof extractor code.
|
||||
|
||||
Extractors are very fragile by nature since they depend on the layout of the source data provided by 3rd party media hosters out of your control and this layout tends to change. As an extractor implementer your task is not only to write code that will extract media links and metadata correctly but also to minimize dependency on the source's layout and even to make the code foresee potential future changes and be ready for that. This is important because it will allow the extractor not to break on minor layout changes thus keeping old youtube-dl versions working. Even though this breakage issue is easily fixed by emitting a new version of youtube-dl with a fix incorporated, all the previous versions become broken in all repositories and distros' packages that may not be so prompt in fetching the update from us. Needless to say, some non rolling release distros may never receive an update at all.
|
||||
Extractors are very fragile by nature since they depend on the layout of the source data provided by 3rd party media hosters out of your control and this layout tends to change. As an extractor implementer your task is not only to write code that will extract media links and metadata correctly but also to minimize dependency on the source's layout and even to make the code foresee potential future changes and be ready for that. This is important because it will allow the extractor not to break on minor layout changes thus keeping old yt-dlp versions working. Even though this breakage issue may be easily fixed by a new version of yt-dlp, this could take some time, during which the the extractor will remain broken.
|
||||
|
||||
|
||||
### Mandatory and optional metafields
|
||||
|
||||
For extraction to work youtube-dl relies on metadata your extractor extracts and provides to youtube-dl expressed by an [information dictionary](https://github.com/ytdl-org/youtube-dl/blob/7f41a598b3fba1bcab2817de64a08941200aa3c8/youtube_dl/extractor/common.py#L94-L303) or simply *info dict*. Only the following meta fields in the *info dict* are considered mandatory for a successful extraction process by youtube-dl:
|
||||
For extraction to work yt-dlp relies on metadata your extractor extracts and provides to yt-dlp expressed by an [information dictionary](yt_dlp/extractor/common.py#L91-L426) or simply *info dict*. Only the following meta fields in the *info dict* are considered mandatory for a successful extraction process by yt-dlp:
|
||||
|
||||
- `id` (media identifier)
|
||||
- `title` (media title)
|
||||
- `url` (media download URL) or `formats`
|
||||
|
||||
In fact only the last option is technically mandatory (i.e. if you can't figure out the download location of the media the extraction does not make any sense). But by convention youtube-dl also treats `id` and `title` as mandatory. Thus the aforementioned metafields are the critical data that the extraction does not make any sense without and if any of them fail to be extracted then the extractor is considered completely broken.
|
||||
The aforementioned metafields are the critical data that the extraction does not make any sense without and if any of them fail to be extracted then the extractor is considered completely broken. While, in fact, only `id` is technically mandatory, due to compatability reasons, yt-dlp also treats `title` as mandatory. The extractor is allowed to return the info dict without url or formats in some special cases if it allows the user to extract usefull information with `--ignore-no-formats-error` - Eg: when the video is a live stream that has not started yet.
|
||||
|
||||
[Any field](https://github.com/ytdl-org/youtube-dl/blob/7f41a598b3fba1bcab2817de64a08941200aa3c8/youtube_dl/extractor/common.py#L188-L303) apart from the aforementioned ones are considered **optional**. That means that extraction should be **tolerant** to situations when sources for these fields can potentially be unavailable (even if they are always available at the moment) and **future-proof** in order not to break the extraction of general purpose mandatory fields.
|
||||
[Any field](yt_dlp/extractor/common.py#219-L426) apart from the aforementioned ones are considered **optional**. That means that extraction should be **tolerant** to situations when sources for these fields can potentially be unavailable (even if they are always available at the moment) and **future-proof** in order not to break the extraction of general purpose mandatory fields.
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -200,8 +247,10 @@ Assume at this point `meta`'s layout is:
|
||||
|
||||
```python
|
||||
{
|
||||
...
|
||||
"summary": "some fancy summary text",
|
||||
"user": {
|
||||
"name": "uploader name"
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
@@ -220,6 +269,30 @@ description = meta['summary'] # incorrect
|
||||
|
||||
The latter will break extraction process with `KeyError` if `summary` disappears from `meta` at some later time but with the former approach extraction will just go ahead with `description` set to `None` which is perfectly fine (remember `None` is equivalent to the absence of data).
|
||||
|
||||
|
||||
If the data is nested, do not use `.get` chains, but instead make use of the utility functions `try_get` or `traverse_obj`
|
||||
|
||||
Considering the above `meta` again, assume you want to extract `["user"]["name"]` and put it in the resulting info dict as `uploader`
|
||||
|
||||
```python
|
||||
uploader = try_get(meta, lambda x: x['user']['name']) # correct
|
||||
```
|
||||
or
|
||||
```python
|
||||
uploader = traverse_obj(meta, ('user', 'name')) # correct
|
||||
```
|
||||
|
||||
and not like:
|
||||
|
||||
```python
|
||||
uploader = meta['user']['name'] # incorrect
|
||||
```
|
||||
or
|
||||
```python
|
||||
uploader = meta.get('user', {}).get('name') # incorrect
|
||||
```
|
||||
|
||||
|
||||
Similarly, you should pass `fatal=False` when extracting optional data from a webpage with `_search_regex`, `_html_search_regex` or similar methods, for instance:
|
||||
|
||||
```python
|
||||
@@ -239,11 +312,36 @@ description = self._search_regex(
|
||||
```
|
||||
|
||||
On failure this code will silently continue the extraction with `description` set to `None`. That is useful for metafields that may or may not be present.
|
||||
|
||||
|
||||
|
||||
Another thing to remember is not to try to iterate over `None`
|
||||
|
||||
Say you extracted a list of thumbnails into `thumbnail_data` using `try_get` and now want to iterate over them
|
||||
|
||||
```python
|
||||
thumbnail_data = try_get(...)
|
||||
thumbnails = [{
|
||||
'url': item['url']
|
||||
} for item in thumbnail_data or []] # correct
|
||||
```
|
||||
|
||||
and not like:
|
||||
|
||||
```python
|
||||
thumbnail_data = try_get(...)
|
||||
thumbnails = [{
|
||||
'url': item['url']
|
||||
} for item in thumbnail_data] # incorrect
|
||||
```
|
||||
|
||||
In the later case, `thumbnail_data` will be `None` if the field was not found and this will cause the loop `for item in thumbnail_data` to raise a fatal error. Using `for item in thumbnail_data or []` avoids this error and results in setting an empty list in `thumbnails` instead.
|
||||
|
||||
|
||||
### Provide fallbacks
|
||||
|
||||
When extracting metadata try to do so from multiple sources. For example if `title` is present in several places, try extracting from at least some of them. This makes it more future-proof in case some of the sources become unavailable.
|
||||
|
||||
|
||||
#### Example
|
||||
|
||||
Say `meta` from the previous example has a `title` and you are about to extract it. Since `title` is a mandatory meta field you should end up with something like:
|
||||
@@ -262,6 +360,7 @@ title = meta.get('title') or self._og_search_title(webpage)
|
||||
|
||||
This code will try to extract from `meta` first and if it fails it will try extracting `og:title` from a `webpage`.
|
||||
|
||||
|
||||
### Regular expressions
|
||||
|
||||
#### Don't capture groups you don't use
|
||||
@@ -283,11 +382,10 @@ Incorrect:
|
||||
r'(id|ID)=(?P<id>\d+)'
|
||||
```
|
||||
|
||||
|
||||
#### Make regular expressions relaxed and flexible
|
||||
|
||||
When using regular expressions try to write them fuzzy, relaxed and flexible, skipping insignificant parts that are more likely to change, allowing both single and double quotes for quoted values and so on.
|
||||
|
||||
|
||||
##### Example
|
||||
|
||||
Say you need to extract `title` from the following HTML code:
|
||||
@@ -299,14 +397,14 @@ Say you need to extract `title` from the following HTML code:
|
||||
The code for that task should look similar to:
|
||||
|
||||
```python
|
||||
title = self._search_regex(
|
||||
title = self._search_regex( # correct
|
||||
r'<span[^>]+class="title"[^>]*>([^<]+)', webpage, 'title')
|
||||
```
|
||||
|
||||
Or even better:
|
||||
|
||||
```python
|
||||
title = self._search_regex(
|
||||
title = self._search_regex( # correct
|
||||
r'<span[^>]+class=(["\'])title\1[^>]*>(?P<title>[^<]+)',
|
||||
webpage, 'title', group='title')
|
||||
```
|
||||
@@ -316,14 +414,25 @@ Note how you tolerate potential changes in the `style` attribute's value or swit
|
||||
The code definitely should not look like:
|
||||
|
||||
```python
|
||||
title = self._search_regex(
|
||||
title = self._search_regex( # incorrect
|
||||
r'<span style="position: absolute; left: 910px; width: 90px; float: right; z-index: 9999;" class="title">(.*?)</span>',
|
||||
webpage, 'title', group='title')
|
||||
```
|
||||
|
||||
or even
|
||||
|
||||
```python
|
||||
title = self._search_regex( # incorrect
|
||||
r'<span style=".*?" class="title">(.*?)</span>',
|
||||
webpage, 'title', group='title')
|
||||
```
|
||||
|
||||
Here the presence or absence of other attributes including `style` is irrelevent for the data we need, and so the regex must not depend on it
|
||||
|
||||
|
||||
### Long lines policy
|
||||
|
||||
There is a soft limit to keep lines of code under 80 characters long. This means it should be respected if possible and if it does not make readability and code maintenance worse.
|
||||
There is a soft limit to keep lines of code under 100 characters long. This means it should be respected if possible and if it does not make readability and code maintenance worse. Sometimes, it may be reasonable to go upto 120 characters and sometimes even 80 can be unreadable. Keep in mind that this is not a hard limit and is just one of many tools to make the code more readable
|
||||
|
||||
For example, you should **never** split long string literals like URLs or some other often copied entities over multiple lines to fit this limit:
|
||||
|
||||
@@ -360,6 +469,7 @@ TITLE_RE = r'<title>([^<]+)</title>'
|
||||
title = self._html_search_regex(TITLE_RE, webpage, 'title')
|
||||
```
|
||||
|
||||
|
||||
### Collapse fallbacks
|
||||
|
||||
Multiple fallback values can quickly become unwieldy. Collapse multiple fallback values into a single expression via a list of patterns.
|
||||
@@ -385,10 +495,13 @@ description = (
|
||||
|
||||
Methods supporting list of patterns are: `_search_regex`, `_html_search_regex`, `_og_search_property`, `_html_search_meta`.
|
||||
|
||||
|
||||
### Trailing parentheses
|
||||
|
||||
Always move trailing parentheses after the last argument.
|
||||
|
||||
Note that this *does not* apply to braces `}` or square brackets `]` both of which should closed be in a new line
|
||||
|
||||
#### Example
|
||||
|
||||
Correct:
|
||||
@@ -406,30 +519,36 @@ Incorrect:
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
### Use convenience conversion and parsing functions
|
||||
|
||||
Wrap all extracted numeric data into safe functions from [`youtube_dl/utils.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/utils.py): `int_or_none`, `float_or_none`. Use them for string to number conversions as well.
|
||||
Wrap all extracted numeric data into safe functions from [`yt_dlp/utils.py`](yt_dlp/utils.py): `int_or_none`, `float_or_none`. Use them for string to number conversions as well.
|
||||
|
||||
Use `url_or_none` for safe URL processing.
|
||||
|
||||
Use `try_get` for safe metadata extraction from parsed JSON.
|
||||
Use `try_get`, `dict_get` and `traverse_obj` for safe metadata extraction from parsed JSON.
|
||||
|
||||
Use `unified_strdate` for uniform `upload_date` or any `YYYYMMDD` meta field extraction, `unified_timestamp` for uniform `timestamp` extraction, `parse_filesize` for `filesize` extraction, `parse_count` for count meta fields extraction, `parse_resolution`, `parse_duration` for `duration` extraction, `parse_age_limit` for `age_limit` extraction.
|
||||
|
||||
Explore [`youtube_dl/utils.py`](https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/utils.py) for more useful convenience functions.
|
||||
Explore [`yt_dlp/utils.py`](yt_dlp/utils.py) for more useful convenience functions.
|
||||
|
||||
#### More examples
|
||||
|
||||
##### Safely extract optional description from parsed JSON
|
||||
```python
|
||||
description = try_get(response, lambda x: x['result']['video'][0]['summary'], compat_str)
|
||||
description = traverse_obj(response, ('result', 'video', 'summary'), expected_type=str)
|
||||
```
|
||||
|
||||
##### Safely extract more optional metadata
|
||||
```python
|
||||
video = try_get(response, lambda x: x['result']['video'][0], dict) or {}
|
||||
video = traverse_obj(response, ('result', 'video', 0), default={}, expected_type=dict)
|
||||
description = video.get('summary')
|
||||
duration = float_or_none(video.get('durationMs'), scale=1000)
|
||||
view_count = int_or_none(video.get('views'))
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
# EMBEDDING YT-DLP
|
||||
See [README.md#embedding-yt-dlp](README.md#embedding-yt-dlp) for instructions on how to embed yt-dlp in another Python program
|
||||
|
||||
@@ -118,3 +118,8 @@ sleaux-meaux
|
||||
sulyi
|
||||
tmarki
|
||||
Vangelis66
|
||||
AjaxGb
|
||||
ajj8
|
||||
jakubadamw
|
||||
jfogelman
|
||||
timethrow
|
||||
|
||||
+67
-1
@@ -14,6 +14,73 @@
|
||||
-->
|
||||
|
||||
|
||||
### 2021.10.09
|
||||
|
||||
* Improved progress reporting
|
||||
* Separate `--console-title` and `--no-progress`
|
||||
* Add option `--progress` to show progress-bar even in quiet mode
|
||||
* Fix and refactor `minicurses` and use it for all progress reporting
|
||||
* Standardize use of terminal sequences and enable color support for windows 10
|
||||
* Add option `--progress-template` to customize progress-bar and console-title
|
||||
* Add postprocessor hooks and progress reporting
|
||||
* [postprocessor] Add plugin support with option `--use-postprocessor`
|
||||
* [extractor] Extract storyboards from SMIL manifests by [fstirlitz](https://github.com/fstirlitz)
|
||||
* [outtmpl] Alternate form of format type `l` for `\n` delimited list
|
||||
* [outtmpl] Format type `U` for unicode normalization
|
||||
* [outtmpl] Allow empty output template to skip a type of file
|
||||
* Merge webm formats into mkv if thumbnails are to be embedded
|
||||
* [adobepass] Add RCN as MSO by [jfogelman](https://github.com/jfogelman)
|
||||
* [ciscowebex] Add extractor by [damianoamatruda](https://github.com/damianoamatruda)
|
||||
* [Gettr] Add extractor by [i6t](https://github.com/i6t)
|
||||
* [GoPro] Add extractor by [i6t](https://github.com/i6t)
|
||||
* [N1] Add extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [Theta] Add video extractor by [alerikaisattera](https://github.com/alerikaisattera)
|
||||
* [Veo] Add extractor by [i6t](https://github.com/i6t)
|
||||
* [Vupload] Add extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [bbc] Extract better quality videos by [ajj8](https://github.com/ajj8)
|
||||
* [Bilibili] Add subtitle converter by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [CBC] Cleanup tests by [makeworld-the-better-one](https://github.com/makeworld-the-better-one)
|
||||
* [Douyin] Rewrite extractor by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||
* [Funimation] Fix for /v/ urls by [pukkandan](https://github.com/pukkandan), [Jules-A](https://github.com/Jules-A)
|
||||
* [Funimation] Sort formats according to the relevant extractor-args
|
||||
* [Hidive] Fix duplicate and incorrect formats
|
||||
* [HotStarSeries] Fix cookies by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [LinkedInLearning] Add subtitles by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [Mediaite] Relax valid url by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [Newgrounds] Add age_limit and fix duration by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [Newgrounds] Fix view count on songs by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [parliamentlive.tv] Fix extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [PolskieRadio] Fix extractors by [jakubadamw](https://github.com/jakubadamw), [u-spec-png](https://github.com/u-spec-png)
|
||||
* [reddit] Add embedded url by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [reddit] Fix 429 by generating a random `reddit_session` by [AjaxGb](https://github.com/AjaxGb)
|
||||
* [Rumble] Add RumbleChannelIE by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [soundcloud:playlist] Detect last page correctly
|
||||
* [SovietsCloset] Add duration from m3u8 by [ChillingPepper](https://github.com/ChillingPepper)
|
||||
* [Streamable] Add codecs by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [vidme] Remove extractor by [alerikaisattera](https://github.com/alerikaisattera)
|
||||
* [youtube:tab] Fallback to API when webpage fails to download by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [youtube] Fix non-fatal errors in fetching player
|
||||
* Fix `--flat-playlist` when neither IE nor id is known
|
||||
* Fix `-f mp4` behaving differently from youtube-dl
|
||||
* Workaround for bug in `ssl.SSLContext.load_default_certs`
|
||||
* [aes] Improve performance slightly by [sulyi](https://github.com/sulyi)
|
||||
* [cookies] Fix keyring fallback by [mbway](https://github.com/mbway)
|
||||
* [embedsubtitle] Fix error when duration is unknown
|
||||
* [ffmpeg] Fix error when subtitle file is missing
|
||||
* [ffmpeg] Set max probesize to workaround AAC HLS stream issues by [shirt](https://github.com/shirt-dev)
|
||||
* [FixupM3u8] Remove redundant run if merged is needed
|
||||
* [hls] Fix decryption issues by [shirt](https://github.com/shirt-dev), [pukkandan](https://github.com/pukkandan)
|
||||
* [http] Respect user-provided chunk size over extractor's
|
||||
* [utils] Let traverse_obj accept functions as keys
|
||||
* [docs] Add note about our custom ffmpeg builds
|
||||
* [docs] Write embedding and contributing documentation by [pukkandan](https://github.com/pukkandan), [timethrow](https://github.com/timethrow)
|
||||
* [update] Check for new version even if not updateable
|
||||
* [build] Add more files to the tarball
|
||||
* [build] Allow building with py2exe (and misc fixes)
|
||||
* [build] Use pycryptodomex by [shirt](https://github.com/shirt-dev), [pukkandan](https://github.com/pukkandan)
|
||||
* [cleanup] Some minor refactoring, improve docs and misc cleanup
|
||||
|
||||
|
||||
### 2021.09.25
|
||||
|
||||
* Add new option `--netrc-location`
|
||||
@@ -41,7 +108,6 @@
|
||||
* [Streamanity] Add Extractor by [alerikaisattera](https://github.com/alerikaisattera)
|
||||
* [Theta] Add extractor by [alerikaisattera](https://github.com/alerikaisattera)
|
||||
* [Yandex] Add ZenYandexIE and ZenYandexChannelIE by [Ashish0804](https://github.com/Ashish0804)
|
||||
|
||||
* [9Now] handle episodes of series by [dalanmiller](https://github.com/dalanmiller)
|
||||
* [AnimalPlanet] Fix extractor by [Sipherdrakon](https://github.com/Sipherdrakon)
|
||||
* [Arte] Improve description extraction by [renalid](https://github.com/renalid)
|
||||
|
||||
@@ -112,7 +112,7 @@ _EXTRACTOR_FILES = $(shell find yt_dlp/extractor -iname '*.py' -and -not -iname
|
||||
yt_dlp/extractor/lazy_extractors.py: devscripts/make_lazy_extractors.py devscripts/lazy_load_template.py $(_EXTRACTOR_FILES)
|
||||
$(PYTHON) devscripts/make_lazy_extractors.py $@
|
||||
|
||||
yt-dlp.tar.gz: yt-dlp README.md supportedsites.md yt-dlp.1 completions Changelog.md AUTHORS
|
||||
yt-dlp.tar.gz: all
|
||||
@tar -czf $(DESTDIR)/yt-dlp.tar.gz --transform "s|^|yt-dlp/|" --owner 0 --group 0 \
|
||||
--exclude '*.DS_Store' \
|
||||
--exclude '*.kate-swp' \
|
||||
@@ -121,12 +121,12 @@ yt-dlp.tar.gz: yt-dlp README.md supportedsites.md yt-dlp.1 completions Changelog
|
||||
--exclude '*~' \
|
||||
--exclude '__pycache__' \
|
||||
--exclude '.git' \
|
||||
--exclude 'docs/_build' \
|
||||
-- \
|
||||
devscripts test \
|
||||
Changelog.md AUTHORS LICENSE README.md supportedsites.md \
|
||||
Makefile MANIFEST.in yt-dlp.1 completions \
|
||||
setup.py setup.cfg yt-dlp yt_dlp
|
||||
README.md supportedsites.md Changelog.md LICENSE \
|
||||
CONTRIBUTING.md Collaborators.md CONTRIBUTORS AUTHORS \
|
||||
Makefile MANIFEST.in yt-dlp.1 README.txt completions \
|
||||
setup.py setup.cfg yt-dlp yt_dlp requirements.txt \
|
||||
devscripts test tox.ini pytest.ini
|
||||
|
||||
AUTHORS: .mailmap
|
||||
git shortlog -s -n | cut -f2 | sort > AUTHORS
|
||||
|
||||
@@ -54,7 +54,11 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
|
||||
* [Modifying metadata examples](#modifying-metadata-examples)
|
||||
* [EXTRACTOR ARGUMENTS](#extractor-arguments)
|
||||
* [PLUGINS](#plugins)
|
||||
* [EMBEDDING YT-DLP](#embedding-yt-dlp)
|
||||
* [DEPRECATED OPTIONS](#deprecated-options)
|
||||
* [CONTRIBUTING](CONTRIBUTING.md#contributing-to-yt-dlp)
|
||||
* [Opening an Issue](CONTRIBUTING.md#opening-an-issue)
|
||||
* [Developer Instructions](CONTRIBUTING.md#developer-instructions)
|
||||
* [MORE](#more)
|
||||
</div>
|
||||
|
||||
@@ -77,7 +81,7 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
|
||||
* Most (but not all) age-gated content can be downloaded without cookies
|
||||
* Partial workaround for throttling issue
|
||||
* Redirect channel's home URL automatically to `/video` to preserve the old behaviour
|
||||
* `255kbps` audio is extracted from youtube music if premium cookies are given
|
||||
* `255kbps` audio is extracted (if available) from youtube music when premium cookies are given
|
||||
* Youtube music Albums, channels etc can be downloaded ([except self-uploaded music](https://github.com/yt-dlp/yt-dlp/issues/723))
|
||||
|
||||
* **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[:PROFILE]`
|
||||
@@ -88,9 +92,9 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/
|
||||
|
||||
* **Aria2c with HLS/DASH**: You can use `aria2c` as the external downloader for DASH(mpd) and HLS(m3u8) formats
|
||||
|
||||
* **New extractors**: AnimeLab, Philo MSO, Spectrum MSO, SlingTV MSO, Cablevision MSO, Rcs, Gedi, bitwave.tv, mildom, audius, zee5, mtv.it, wimtv, pluto.tv, niconico users, discoveryplus.in, mediathek, NFHSNetwork, nebula, ukcolumn, whowatch, MxplayerShow, parlview (au), YoutubeWebArchive, fancode, Saitosan, ShemarooMe, telemundo, VootSeries, SonyLIVSeries, HotstarSeries, VidioPremier, VidioLive, RCTIPlus, TBS Live, douyin, pornflip, ParamountPlusSeries, ScienceChannel, Utreon, OpenRec, BandcampMusic, blackboardcollaborate, eroprofile albums, mirrativ, BannedVideo, bilibili categories, Epicon, filmmodu, GabTV, HungamaAlbum, ManotoTV, Niconico search, Patreon User, peloton, ProjectVeritas, radiko, StarTV, tiktok user, Tokentube, voicy, TV2HuSeries, biliintl, 17live, NewgroundsUser, peertube channel/playlist, ZenYandex, CAM4, CGTN, damtomo, gotostage, Koo, Mediaite, Mediaklikk, MuseScore, nzherald, Olympics replay, radlive, SovietsCloset, Streamanity, Theta, Chingari
|
||||
* **New extractors**: AnimeLab, Philo MSO, Spectrum MSO, SlingTV MSO, Cablevision MSO, RCN MSO, Rcs, Gedi, bitwave.tv, mildom, audius, zee5, mtv.it, wimtv, pluto.tv, niconico users, discoveryplus.in, mediathek, NFHSNetwork, nebula, ukcolumn, whowatch, MxplayerShow, parlview (au), YoutubeWebArchive, fancode, Saitosan, ShemarooMe, telemundo, VootSeries, SonyLIVSeries, HotstarSeries, VidioPremier, VidioLive, RCTIPlus, TBS Live, douyin, pornflip, ParamountPlusSeries, ScienceChannel, Utreon, OpenRec, BandcampMusic, blackboardcollaborate, eroprofile albums, mirrativ, BannedVideo, bilibili categories, Epicon, filmmodu, GabTV, HungamaAlbum, ManotoTV, Niconico search, Patreon User, peloton, ProjectVeritas, radiko, StarTV, tiktok user, Tokentube, voicy, TV2HuSeries, biliintl, 17live, NewgroundsUser, peertube channel/playlist, ZenYandex, CAM4, CGTN, damtomo, gotostage, Koo, Mediaite, Mediaklikk, MuseScore, nzherald, Olympics replay, radlive, SovietsCloset, Streamanity, Theta, Chingari, ciscowebex, Gettr, GoPro, N1, Theta, Veo, Vupload
|
||||
|
||||
* **Fixed/improved extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, akamai, ina, rumble, tennistv, amcnetworks, la7 podcasts, linuxacadamy, nitter, twitcasting, viu, crackle, curiositystream, mediasite, rmcdecouverte, sonyliv, tubi, tenplay, patreon, videa, yahoo, BravoTV, crunchyroll playlist, RTP, viki, Hotstar, vidio, vimeo, mediaset, Mxplayer, nbcolympics, ParamountPlus, Newgrounds, SAML Verizon login, Hungama, afreecatv, aljazeera, ATV, bitchute, camtube, CDA, eroprofile, facebook, HearThisAtIE, iwara, kakao, Motherless, Nova, peertube, pornhub, reddit, tiktok, TV2, TV2Hu, tv5mondeplus, VH1, Viafree, XHamster, 9Now, AnimalPlanet, Arte, CBC, Chingari, comedycentral, DIYNetwork, niconico, dw, funimation, globo, HiDive, NDR, Nuvid, Oreilly, pbs, plutotv, reddit, redtube, soundcloud, SpankBang, VrtNU
|
||||
* **Fixed/improved extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, akamai, ina, rumble, tennistv, amcnetworks, la7 podcasts, linuxacadamy, nitter, twitcasting, viu, crackle, curiositystream, mediasite, rmcdecouverte, sonyliv, tubi, tenplay, patreon, videa, yahoo, BravoTV, crunchyroll playlist, RTP, viki, Hotstar, vidio, vimeo, mediaset, Mxplayer, nbcolympics, ParamountPlus, Newgrounds, SAML Verizon login, Hungama, afreecatv, aljazeera, ATV, bitchute, camtube, CDA, eroprofile, facebook, HearThisAtIE, iwara, kakao, Motherless, Nova, peertube, pornhub, reddit, tiktok, TV2, TV2Hu, tv5mondeplus, VH1, Viafree, XHamster, 9Now, AnimalPlanet, Arte, CBC, Chingari, comedycentral, DIYNetwork, niconico, dw, funimation, globo, HiDive, NDR, Nuvid, Oreilly, pbs, plutotv, reddit, redtube, soundcloud, SpankBang, VrtNU, bbc, Bilibili, LinkedInLearning, parliamentlive, PolskieRadio, Streamable, vidme
|
||||
|
||||
* **Subtitle extraction from manifests**: Subtitles can be extracted from streaming media manifests. See [commit/be6202f](https://github.com/yt-dlp/yt-dlp/commit/be6202f12b97858b9d716e608394b51065d0419f) for details
|
||||
|
||||
@@ -150,7 +154,7 @@ For ease of use, a few more compat options are available:
|
||||
yt-dlp is not platform specific. So it should work on your Unix box, on Windows or on macOS
|
||||
|
||||
You can install yt-dlp using one of the following methods:
|
||||
* Download the binary from the [latest release](https://github.com/yt-dlp/yt-dlp/releases/latest) (recommended method)
|
||||
* Download the binary from the [latest release](https://github.com/yt-dlp/yt-dlp/releases/latest)
|
||||
* With Homebrew, `brew install yt-dlp/taps/yt-dlp`
|
||||
* Use [PyPI package](https://pypi.org/project/yt-dlp): `python3 -m pip install --upgrade yt-dlp`
|
||||
* Use pip+git: `python3 -m pip install --upgrade git+https://github.com/yt-dlp/yt-dlp.git@release`
|
||||
@@ -195,7 +199,7 @@ On windows, [Microsoft Visual C++ 2010 SP1 Redistributable Package (x86)](https:
|
||||
While all the other dependancies are optional, `ffmpeg` and `ffprobe` are highly recommended
|
||||
* [**ffmpeg** and **ffprobe**](https://www.ffmpeg.org) - Required for [merging seperate video and audio files](#format-selection) as well as for various [post-processing](#post-processing-options) tasks. Licence [depends on the build](https://www.ffmpeg.org/legal.html)
|
||||
* [**mutagen**](https://github.com/quodlibet/mutagen) - For embedding thumbnail in certain formats. Licenced under [GPLv2+](https://github.com/quodlibet/mutagen/blob/master/COPYING)
|
||||
* [**pycryptodome**](https://github.com/Legrandin/pycryptodome) - For decrypting various data. Licenced under [BSD2](https://github.com/Legrandin/pycryptodome/blob/master/LICENSE.rst)
|
||||
* [**pycryptodomex**](https://github.com/Legrandin/pycryptodome) - For decrypting AES-128 HLS streams and various other data. Licenced under [BSD2](https://github.com/Legrandin/pycryptodome/blob/master/LICENSE.rst)
|
||||
* [**websockets**](https://github.com/aaugustin/websockets) - For downloading over websocket. Licenced under [BSD3](https://github.com/aaugustin/websockets/blob/main/LICENSE)
|
||||
* [**keyring**](https://github.com/jaraco/keyring) - For decrypting cookies of chromium-based browsers on Linux. Licenced under [MIT](https://github.com/jaraco/keyring/blob/main/LICENSE)
|
||||
* [**AtomicParsley**](https://github.com/wez/atomicparsley) - For embedding thumbnail in mp4/m4a if mutagen is not present. Licenced under [GPLv2+](https://github.com/wez/atomicparsley/blob/master/COPYING)
|
||||
@@ -207,14 +211,17 @@ While all the other dependancies are optional, `ffmpeg` and `ffprobe` are highly
|
||||
|
||||
To use or redistribute the dependencies, you must agree to their respective licensing terms.
|
||||
|
||||
Note that the windows releases are already built with the python interpreter, mutagen, pycryptodome and websockets included.
|
||||
The windows releases are already built with the python interpreter, mutagen, pycryptodomex and websockets included.
|
||||
|
||||
**Note**: There are some regressions in newer ffmpeg versions that causes various issues when used alongside yt-dlp. Since ffmpeg is such an important dependancy, we provide [custom builds](https://github.com/yt-dlp/FFmpeg-Builds/wiki/Latest#latest-autobuilds) with patches for these issues at [yt-dlp/FFmpeg-Builds](https://github.com/yt-dlp/FFmpeg-Builds). See [the readme](https://github.com/yt-dlp/FFmpeg-Builds#patches-applied) for details on the specifc issues solved by these builds
|
||||
|
||||
|
||||
### COMPILE
|
||||
|
||||
**For Windows**:
|
||||
To build the Windows executable, you must have pyinstaller (and optionally mutagen, pycryptodome, websockets)
|
||||
To build the Windows executable, you must have pyinstaller (and optionally mutagen, pycryptodomex, websockets)
|
||||
|
||||
python3 -m pip install --upgrade pyinstaller mutagen pycryptodome websockets
|
||||
python3 -m pip install -U -r requirements.txt
|
||||
|
||||
Once you have all the necessary dependencies installed, just run `py pyinst.py`. The executable will be built for the same architecture (32/64 bit) as the python used to build it.
|
||||
|
||||
@@ -280,7 +287,8 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
|
||||
--flat-playlist Do not extract the videos of a playlist,
|
||||
only list them
|
||||
--no-flat-playlist Extract the videos of a playlist
|
||||
--mark-watched Mark videos watched (YouTube only)
|
||||
--mark-watched Mark videos watched (even with --simulate).
|
||||
Currently only supported for YouTube
|
||||
--no-mark-watched Do not mark videos watched (default)
|
||||
--no-colors Do not emit color codes in output
|
||||
--compat-options OPTS Options that can help keep compatibility
|
||||
@@ -601,7 +609,18 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
|
||||
(Alias: --force-download-archive)
|
||||
--newline Output progress bar as new lines
|
||||
--no-progress Do not print progress bar
|
||||
--progress Show progress bar, even if in quiet mode
|
||||
--console-title Display progress in console titlebar
|
||||
--progress-template [TYPES:]TEMPLATE
|
||||
Template for progress outputs, optionally
|
||||
prefixed with one of "download:" (default),
|
||||
"download-title:" (the console title),
|
||||
"postprocess:", or "postprocess-title:".
|
||||
The video's fields are accessible under the
|
||||
"info" key and the progress attributes are
|
||||
accessible under "progress" key. Eg:
|
||||
--console-title --progress-template
|
||||
"download-title:%(info.id)s-%(progress.eta)s"
|
||||
-v, --verbose Print various debugging information
|
||||
--dump-pages Print downloaded pages encoded using base64
|
||||
to debug problems (very verbose)
|
||||
@@ -837,6 +856,20 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
|
||||
around the cuts
|
||||
--no-force-keyframes-at-cuts Do not force keyframes around the chapters
|
||||
when cutting/splitting (default)
|
||||
--use-postprocessor NAME[:ARGS] The (case sensitive) name of plugin
|
||||
postprocessors to be enabled, and
|
||||
(optionally) arguments to be passed to it,
|
||||
seperated by a colon ":". ARGS are a
|
||||
semicolon ";" delimited list of NAME=VALUE.
|
||||
The "when" argument determines when the
|
||||
postprocessor is invoked. It can be one of
|
||||
"pre_process" (after extraction),
|
||||
"before_dl" (before video download),
|
||||
"post_process" (after video download;
|
||||
default) or "after_move" (after moving file
|
||||
to their final locations). This option can
|
||||
be used multiple times to add different
|
||||
postprocessors
|
||||
|
||||
## SponsorBlock Options:
|
||||
Make chapter entries for, or remove various segments (sponsor,
|
||||
@@ -963,14 +996,15 @@ The field names themselves (the part inside the parenthesis) can also have some
|
||||
1. **Date/time Formatting**: Date/time fields can be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it separated from the field name using a `>`. Eg: `%(duration>%H-%M-%S)s`, `%(upload_date>%Y-%m-%d)s`, `%(epoch-3600>%H-%M-%S)s`
|
||||
1. **Alternatives**: Alternate fields can be specified seperated with a `,`. Eg: `%(release_date>%Y,upload_date>%Y|Unknown)s`
|
||||
1. **Default**: A literal default value can be specified for when the field is empty using a `|` seperator. This overrides `--output-na-template`. Eg: `%(uploader|Unknown)s`
|
||||
1. **More Conversions**: In addition to the normal format types `diouxXeEfFgGcrs`, `B`, `j`, `l`, `q` can be used for converting to **B**ytes, **j**son, a comma seperated **l**ist and a string **q**uoted for the terminal respectively
|
||||
1. **More Conversions**: In addition to the normal format types `diouxXeEfFgGcrs`, `B`, `j`, `l`, `q` can be used for converting to **B**ytes, **j**son, a comma seperated **l**ist (alternate form flag `#` makes it new line `\n` seperated) and a string **q**uoted for the terminal, respectively
|
||||
1. **Unicode normalization**: The format type `U` can be used for NFC [unicode normalization](https://docs.python.org/3/library/unicodedata.html#unicodedata.normalize). The alternate form flag (`#`) changes the normalization to NFD and the conversion flag `+` can be used for NFKC/NFKD compatibility equivalence normalization. Eg: `%(title)+.100U` is NFKC
|
||||
|
||||
To summarize, the general syntax for a field is:
|
||||
```
|
||||
%(name[.keys][addition][>strf][,alternate][|default])[flags][width][.precision][length]type
|
||||
```
|
||||
|
||||
Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation` (deprecated), `infojson`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video.
|
||||
Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation` (deprecated), `infojson`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video. If any of the templates (except default) is empty, that type of file will not be written. Eg: `--write-thumbnail -o "thumbnail:"` will write thumbnails only for playlists and not for video.
|
||||
|
||||
The available fields are:
|
||||
|
||||
@@ -984,9 +1018,10 @@ The available fields are:
|
||||
- `uploader` (string): Full name of the video uploader
|
||||
- `license` (string): License name the video is licensed under
|
||||
- `creator` (string): The creator of the video
|
||||
- `release_date` (string): The date (YYYYMMDD) when the video was released
|
||||
- `timestamp` (numeric): UNIX timestamp of the moment the video became available
|
||||
- `upload_date` (string): Video upload date (YYYYMMDD)
|
||||
- `release_date` (string): The date (YYYYMMDD) when the video was released
|
||||
- `release_timestamp` (numeric): UNIX timestamp of the moment the video was released
|
||||
- `uploader_id` (string): Nickname or id of the video uploader
|
||||
- `channel` (string): Full name of the channel the video is uploaded on
|
||||
- `channel_id` (string): Id of the channel
|
||||
@@ -1028,8 +1063,10 @@ The available fields are:
|
||||
- `extractor_key` (string): Key name of the extractor
|
||||
- `epoch` (numeric): Unix epoch when creating the file
|
||||
- `autonumber` (numeric): Number that will be increased with each download, starting at `--autonumber-start`
|
||||
- `n_entries` (numeric): Total number of extracted items in the playlist
|
||||
- `playlist` (string): Name or id of the playlist that contains the video
|
||||
- `playlist_index` (numeric): Index of the video in the playlist padded with leading zeros according to the total length of the playlist
|
||||
- `playlist_index` (numeric): Index of the video in the playlist padded with leading zeros according the final index
|
||||
- `playlist_autonumber` (numeric): Position of the video in the playlist download queue padded with leading zeros according to the total length of the playlist
|
||||
- `playlist_id` (string): Playlist identifier
|
||||
- `playlist_title` (string): Playlist title
|
||||
- `playlist_uploader` (string): Full name of the playlist uploader
|
||||
@@ -1248,7 +1285,7 @@ The available fields are:
|
||||
|
||||
All fields, unless specified otherwise, are sorted in descending order. To reverse this, prefix the field with a `+`. Eg: `+res` prefers format with the smallest resolution. Additionally, you can suffix a preferred value for the fields, separated by a `:`. Eg: `res:720` prefers larger videos, but no larger than 720p and the smallest video if there are no videos less than 720p. For `codec` and `ext`, you can provide two preferred values, the first for video and the second for audio. Eg: `+codec:avc:m4a` (equivalent to `+vcodec:avc,+acodec:m4a`) sets the video codec preference to `h264` > `h265` > `vp9` > `vp9.2` > `av01` > `vp8` > `h263` > `theora` and audio codec preference to `mp4a` > `aac` > `vorbis` > `opus` > `mp3` > `ac3` > `dts`. You can also make the sorting prefer the nearest values to the provided by using `~` as the delimiter. Eg: `filesize~1G` prefers the format with filesize closest to 1 GiB.
|
||||
|
||||
The fields `hasvid` and `ie_pref` are always given highest priority in sorting, irrespective of the user-defined order. This behaviour can be changed by using `--force-format-sort`. Apart from these, the default order used is: `lang,quality,res,fps,codec:vp9.2,size,br,asr,proto,ext,hasaud,source,id`. The extractors may override this default order, but they cannot override the user-provided order.
|
||||
The fields `hasvid` and `ie_pref` are always given highest priority in sorting, irrespective of the user-defined order. This behaviour can be changed by using `--format-sort-force`. Apart from these, the default order used is: `lang,quality,res,fps,codec:vp9.2,size,br,asr,proto,ext,hasaud,source,id`. The extractors may override this default order, but they cannot override the user-provided order.
|
||||
|
||||
Note that the default has `codec:vp9.2`; i.e. `av1` is not prefered
|
||||
|
||||
@@ -1451,6 +1488,9 @@ The following extractors use this feature:
|
||||
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side).
|
||||
* `max_comments`: Maximum amount of comments to download (default all).
|
||||
* `max_comment_depth`: Maximum depth for nested comments. YouTube supports depths 1 or 2 (default).
|
||||
* **youtubetab**
|
||||
(YouTube playlists, channels, feeds, etc.)
|
||||
* `skip`: One or more of `webpage` (skip initial webpage download), `authcheck` (allow the download of playlists requiring authentication when no initial webpage is downloaded. This may cause unwanted behavior, see [#1122](https://github.com/yt-dlp/yt-dlp/pull/1122) for more details)
|
||||
|
||||
* **funimation**
|
||||
* `language`: Languages to extract. Eg: `funimation:language=english,japanese`
|
||||
@@ -1464,9 +1504,94 @@ NOTE: These options may be changed/removed in the future without concern for bac
|
||||
|
||||
# PLUGINS
|
||||
|
||||
Plugins are loaded from `<root-dir>/ytdlp_plugins/<type>/__init__.py`. Currently only `extractor` plugins are supported. Support for `downloader` and `postprocessor` plugins may be added in the future. See [ytdlp_plugins](ytdlp_plugins) for example.
|
||||
Plugins are loaded from `<root-dir>/ytdlp_plugins/<type>/__init__.py`; where `<root-dir>` is the directory of the binary (`<root-dir>/yt-dlp`), or the root directory of the module if you are running directly from source-code (`<root dir>/yt_dlp/__main__.py`). Plugins are currently not supported for the `pip` version
|
||||
|
||||
Plugins can be of `<type>`s `extractor` or `postprocessor`. Extractor plugins do not need to be enabled from the CLI and are automatically invoked when the input URL is suitable for it. Postprocessor plugins can be invoked using `--use-postprocessor NAME`.
|
||||
|
||||
See [ytdlp_plugins](ytdlp_plugins) for example plugins.
|
||||
|
||||
Note that **all** plugins are imported even if not invoked, and that **there are no checks** performed on plugin code. Use plugins at your own risk and only if you trust the code
|
||||
|
||||
If you are a plugin author, add [ytdlp-plugins](https://github.com/topics/ytdlp-plugins) as a topic to your repository for discoverability
|
||||
|
||||
|
||||
|
||||
# EMBEDDING YT-DLP
|
||||
|
||||
yt-dlp makes the best effort to be a good command-line program, and thus should be callable from any programming language.
|
||||
|
||||
Your program should avoid parsing the normal stdout since they may change in future versions. Instead they should use options such as `-J`, `--print`, `--progress-template`, `--exec` etc to create console output that you can reliably reproduce and parse.
|
||||
|
||||
From a Python program, you can embed yt-dlp in a more powerful fashion, like this:
|
||||
|
||||
```python
|
||||
import yt_dlp
|
||||
|
||||
ydl_opts = {}
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download(['https://www.youtube.com/watch?v=BaW_jenozKc'])
|
||||
```
|
||||
|
||||
Most likely, you'll want to use various options. For a list of options available, have a look at [`yt_dlp/YoutubeDL.py`](yt_dlp/YoutubeDL.py#L154-L452).
|
||||
|
||||
Here's a more complete example of a program that outputs only errors (and a short message after the download is finished), converts the video to an mp3 file, implements a custom postprocessor and prints the final info_dict as json:
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
import yt_dlp
|
||||
from yt_dlp.postprocessor.common import PostProcessor
|
||||
|
||||
|
||||
class MyLogger:
|
||||
def debug(self, msg):
|
||||
# For compatability with youtube-dl, both debug and info are passed into debug
|
||||
# You can distinguish them by the prefix '[debug] '
|
||||
if msg.startswith('[debug] '):
|
||||
pass
|
||||
else:
|
||||
self.info(msg)
|
||||
|
||||
def info(self, msg):
|
||||
pass
|
||||
|
||||
def warning(self, msg):
|
||||
pass
|
||||
|
||||
def error(self, msg):
|
||||
print(msg)
|
||||
|
||||
|
||||
class MyCustomPP(PostProcessor):
|
||||
def run(self, info):
|
||||
self.to_screen('Doing stuff')
|
||||
return [], info
|
||||
|
||||
|
||||
def my_hook(d):
|
||||
if d['status'] == 'finished':
|
||||
print('Done downloading, now converting ...')
|
||||
|
||||
|
||||
ydl_opts = {
|
||||
'format': 'bestaudio/best',
|
||||
'postprocessors': [{
|
||||
'key': 'FFmpegExtractAudio',
|
||||
'preferredcodec': 'mp3',
|
||||
'preferredquality': '192',
|
||||
}],
|
||||
'logger': MyLogger(),
|
||||
'progress_hooks': [my_hook],
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.add_post_processor(MyCustomPP())
|
||||
info = ydl.extract_info('https://www.youtube.com/watch?v=BaW_jenozKc')
|
||||
print(json.dumps(ydl.sanitize_info(info)))
|
||||
```
|
||||
|
||||
See the public functions in [`yt_dlp/YoutubeDL.py`](yt_dlp/YoutubeDL.py) for other available functions. Eg: `ydl.download`, `ydl.download_with_info_file`
|
||||
|
||||
**Note**: `<root-dir>` is the directory of the binary (`<root-dir>/yt-dlp`), or the root directory of the module if you are running directly from source-code (`<root dir>/yt_dlp/__main__.py`)
|
||||
|
||||
# DEPRECATED OPTIONS
|
||||
|
||||
@@ -1569,6 +1694,8 @@ These options were deprecated since 2014 and have now been entirely removed
|
||||
-t, --title -o "%(title)s-%(id)s.%(ext)s"
|
||||
-l, --literal -o accepts literal names
|
||||
|
||||
# CONTRIBUTING
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md#contributing-to-yt-dlp) for instructions on [Opening an Issue](CONTRIBUTING.md#opening-an-issue) and [Contributing code to the project](CONTRIBUTING.md#developer-instructions)
|
||||
|
||||
# MORE
|
||||
For FAQ, Developer Instructions etc., see the [original README](https://github.com/ytdl-org/youtube-dl#faq)
|
||||
For FAQ see the [youtube-dl README](https://github.com/ytdl-org/youtube-dl#faq)
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
# coding: utf-8
|
||||
import re
|
||||
|
||||
from ..utils import bug_reports_message, write_string
|
||||
|
||||
|
||||
class LazyLoadMetaClass(type):
|
||||
def __getattr__(cls, name):
|
||||
if '_real_class' not in cls.__dict__:
|
||||
write_string(
|
||||
f'WARNING: Falling back to normal extractor since lazy extractor '
|
||||
f'{cls.__name__} does not have attribute {name}{bug_reports_message()}')
|
||||
return getattr(cls._get_real_class(), name)
|
||||
|
||||
|
||||
@@ -13,10 +19,10 @@ class LazyLoadExtractor(metaclass=LazyLoadMetaClass):
|
||||
|
||||
@classmethod
|
||||
def _get_real_class(cls):
|
||||
if '__real_class' not in cls.__dict__:
|
||||
if '_real_class' not in cls.__dict__:
|
||||
mod = __import__(cls._module, fromlist=(cls.__name__,))
|
||||
cls.__real_class = getattr(mod, cls.__name__)
|
||||
return cls.__real_class
|
||||
cls._real_class = getattr(mod, cls.__name__)
|
||||
return cls._real_class
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
real_cls = cls._get_real_class()
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import io
|
||||
import io
|
||||
import optparse
|
||||
# import re
|
||||
import re
|
||||
|
||||
|
||||
def main():
|
||||
return # This is unused in yt-dlp
|
||||
|
||||
parser = optparse.OptionParser(usage='%prog INFILE OUTFILE')
|
||||
options, args = parser.parse_args()
|
||||
if len(args) != 2:
|
||||
parser.error('Expected an input and an output filename')
|
||||
|
||||
|
||||
""" infile, outfile = args
|
||||
infile, outfile = args
|
||||
|
||||
with io.open(infile, encoding='utf-8') as inf:
|
||||
readme = inf.read()
|
||||
|
||||
bug_text = re.search( """
|
||||
# r'(?s)#\s*BUGS\s*[^\n]*\s*(.*?)#\s*COPYRIGHT', readme).group(1)
|
||||
# dev_text = re.search(
|
||||
# r'(?s)(#\s*DEVELOPER INSTRUCTIONS.*?)#\s*EMBEDDING yt-dlp',
|
||||
""" readme).group(1)
|
||||
bug_text = re.search(
|
||||
r'(?s)#\s*BUGS\s*[^\n]*\s*(.*?)#\s*COPYRIGHT', readme).group(1)
|
||||
dev_text = re.search(
|
||||
r'(?s)(#\s*DEVELOPER INSTRUCTIONS.*?)#\s*EMBEDDING yt-dlp', readme).group(1)
|
||||
|
||||
out = bug_text + dev_text
|
||||
|
||||
with io.open(outfile, 'w', encoding='utf-8') as outf:
|
||||
outf.write(out) """
|
||||
outf.write(out)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -7,8 +7,6 @@ import os
|
||||
from os.path import dirname as dirn
|
||||
import sys
|
||||
|
||||
print('WARNING: Lazy loading extractors is an experimental feature that may not always work', file=sys.stderr)
|
||||
|
||||
sys.path.insert(0, dirn(dirn((os.path.abspath(__file__)))))
|
||||
|
||||
lazy_extractors_filename = sys.argv[1]
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import sys
|
||||
# import os
|
||||
import platform
|
||||
|
||||
from PyInstaller.utils.hooks import collect_submodules
|
||||
@@ -13,19 +12,22 @@ from PyInstaller.utils.win32.versioninfo import (
|
||||
)
|
||||
import PyInstaller.__main__
|
||||
|
||||
arch = sys.argv[1] if len(sys.argv) > 1 else platform.architecture()[0][:2]
|
||||
arch = platform.architecture()[0][:2]
|
||||
assert arch in ('32', '64')
|
||||
_x86 = '_x86' if arch == '32' else ''
|
||||
|
||||
opts = sys.argv[2:] or ['--onefile']
|
||||
# Compatability with older arguments
|
||||
opts = sys.argv[1:]
|
||||
if opts[0:1] in (['32'], ['64']):
|
||||
if arch != opts[0]:
|
||||
raise Exception(f'{opts[0]}bit executable cannot be built on a {arch}bit system')
|
||||
opts = opts[1:]
|
||||
opts = opts or ['--onefile']
|
||||
|
||||
print(f'Building {arch}bit version with options {opts}')
|
||||
|
||||
FILE_DESCRIPTION = 'yt-dlp%s' % (' (32 Bit)' if _x86 else '')
|
||||
|
||||
# root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
# print('Changing working directory to %s' % root_dir)
|
||||
# os.chdir(root_dir)
|
||||
|
||||
exec(compile(open('yt_dlp/version.py').read(), 'yt_dlp/version.py', 'exec'))
|
||||
VERSION = locals()['__version__']
|
||||
|
||||
@@ -69,7 +71,22 @@ VERSION_FILE = VSVersionInfo(
|
||||
]
|
||||
)
|
||||
|
||||
dependancies = ['Crypto', 'mutagen'] + collect_submodules('websockets')
|
||||
|
||||
def pycryptodome_module():
|
||||
try:
|
||||
import Cryptodome # noqa: F401
|
||||
except ImportError:
|
||||
try:
|
||||
import Crypto # noqa: F401
|
||||
print('WARNING: Using Crypto since Cryptodome is not available. '
|
||||
'Install with: pip install pycryptodomex', file=sys.stderr)
|
||||
return 'Crypto'
|
||||
except ImportError:
|
||||
pass
|
||||
return 'Cryptodome'
|
||||
|
||||
|
||||
dependancies = [pycryptodome_module(), 'mutagen'] + collect_submodules('websockets')
|
||||
excluded_modules = ['test', 'ytdlp_plugins', 'youtube-dl', 'youtube-dlc']
|
||||
|
||||
PyInstaller.__main__.run([
|
||||
@@ -82,4 +99,4 @@ PyInstaller.__main__.run([
|
||||
*opts,
|
||||
'yt_dlp/__main__.py',
|
||||
])
|
||||
SetVersion('dist/yt-dlp%s.exe' % _x86, VERSION_FILE)
|
||||
SetVersion('dist/%syt-dlp%s.exe' % ('yt-dlp/' if '--onedir' in opts else '', _x86), VERSION_FILE)
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
mutagen
|
||||
pycryptodome
|
||||
pycryptodomex
|
||||
websockets
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
|
||||
from setuptools import setup, Command, find_packages
|
||||
import os.path
|
||||
import warnings
|
||||
import sys
|
||||
from distutils.spawn import spawn
|
||||
|
||||
try:
|
||||
from setuptools import setup, Command, find_packages
|
||||
setuptools_available = True
|
||||
except ImportError:
|
||||
from distutils.core import setup, Command
|
||||
setuptools_available = False
|
||||
from distutils.spawn import spawn
|
||||
|
||||
# Get the version from yt_dlp/version.py without importing the package
|
||||
exec(compile(open('yt_dlp/version.py').read(), 'yt_dlp/version.py', 'exec'))
|
||||
@@ -19,34 +23,64 @@ LONG_DESCRIPTION = '\n\n'.join((
|
||||
'**PS**: Some links in this document will not work since this is a copy of the README.md from Github',
|
||||
open('README.md', 'r', encoding='utf-8').read()))
|
||||
|
||||
REQUIREMENTS = ['mutagen', 'pycryptodome', 'websockets']
|
||||
REQUIREMENTS = ['mutagen', 'pycryptodomex', 'websockets']
|
||||
|
||||
|
||||
if sys.argv[1:2] == ['py2exe']:
|
||||
raise NotImplementedError('py2exe is not currently supported; instead, use "pyinst.py" to build with pyinstaller')
|
||||
import py2exe
|
||||
warnings.warn(
|
||||
'Building with py2exe is not officially supported. '
|
||||
'The recommended way is to use "pyinst.py" to build using pyinstaller')
|
||||
params = {
|
||||
'console': [{
|
||||
'script': './yt_dlp/__main__.py',
|
||||
'dest_base': 'yt-dlp',
|
||||
'version': __version__,
|
||||
'description': DESCRIPTION,
|
||||
'comments': LONG_DESCRIPTION.split('\n')[0],
|
||||
'product_name': 'yt-dlp',
|
||||
'product_version': __version__,
|
||||
}],
|
||||
'options': {
|
||||
'py2exe': {
|
||||
'bundle_files': 0,
|
||||
'compressed': 1,
|
||||
'optimize': 2,
|
||||
'dist_dir': './dist',
|
||||
'excludes': ['Crypto', 'Cryptodome'], # py2exe cannot import Crypto
|
||||
'dll_excludes': ['w9xpopen.exe', 'crypt32.dll'],
|
||||
}
|
||||
},
|
||||
'zipfile': None
|
||||
}
|
||||
|
||||
else:
|
||||
files_spec = [
|
||||
('share/bash-completion/completions', ['completions/bash/yt-dlp']),
|
||||
('share/zsh/site-functions', ['completions/zsh/_yt-dlp']),
|
||||
('share/fish/vendor_completions.d', ['completions/fish/yt-dlp.fish']),
|
||||
('share/doc/yt_dlp', ['README.txt']),
|
||||
('share/man/man1', ['yt-dlp.1'])
|
||||
]
|
||||
root = os.path.dirname(os.path.abspath(__file__))
|
||||
data_files = []
|
||||
for dirname, files in files_spec:
|
||||
resfiles = []
|
||||
for fn in files:
|
||||
if not os.path.exists(fn):
|
||||
warnings.warn('Skipping file %s since it is not present. Try running `make pypi-files` first' % fn)
|
||||
else:
|
||||
resfiles.append(fn)
|
||||
data_files.append((dirname, resfiles))
|
||||
|
||||
files_spec = [
|
||||
('share/bash-completion/completions', ['completions/bash/yt-dlp']),
|
||||
('share/zsh/site-functions', ['completions/zsh/_yt-dlp']),
|
||||
('share/fish/vendor_completions.d', ['completions/fish/yt-dlp.fish']),
|
||||
('share/doc/yt_dlp', ['README.txt']),
|
||||
('share/man/man1', ['yt-dlp.1'])
|
||||
]
|
||||
root = os.path.dirname(os.path.abspath(__file__))
|
||||
data_files = []
|
||||
for dirname, files in files_spec:
|
||||
resfiles = []
|
||||
for fn in files:
|
||||
if not os.path.exists(fn):
|
||||
warnings.warn('Skipping file %s since it is not present. Try running `make pypi-files` first' % fn)
|
||||
else:
|
||||
resfiles.append(fn)
|
||||
data_files.append((dirname, resfiles))
|
||||
params = {
|
||||
'data_files': data_files,
|
||||
}
|
||||
|
||||
params = {
|
||||
'data_files': data_files,
|
||||
}
|
||||
params['entry_points'] = {'console_scripts': ['yt-dlp = yt_dlp:main']}
|
||||
if setuptools_available:
|
||||
params['entry_points'] = {'console_scripts': ['yt-dlp = yt_dlp:main']}
|
||||
else:
|
||||
params['scripts'] = ['yt-dlp']
|
||||
|
||||
|
||||
class build_lazy_extractors(Command):
|
||||
@@ -64,7 +98,11 @@ class build_lazy_extractors(Command):
|
||||
dry_run=self.dry_run)
|
||||
|
||||
|
||||
packages = find_packages(exclude=('youtube_dl', 'test', 'ytdlp_plugins'))
|
||||
if setuptools_available:
|
||||
packages = find_packages(exclude=('youtube_dl', 'youtube_dlc', 'test', 'ytdlp_plugins'))
|
||||
else:
|
||||
packages = ['yt_dlp', 'yt_dlp.downloader', 'yt_dlp.extractor', 'yt_dlp.postprocessor']
|
||||
|
||||
|
||||
setup(
|
||||
name='yt-dlp',
|
||||
@@ -81,7 +119,7 @@ setup(
|
||||
'Documentation': 'https://yt-dlp.readthedocs.io',
|
||||
'Source': 'https://github.com/yt-dlp/yt-dlp',
|
||||
'Tracker': 'https://github.com/yt-dlp/yt-dlp/issues',
|
||||
#'Funding': 'https://donate.pypi.org',
|
||||
'Funding': 'https://github.com/yt-dlp/yt-dlp/blob/master/Collaborators.md#collaborators',
|
||||
},
|
||||
classifiers=[
|
||||
'Topic :: Multimedia :: Video',
|
||||
|
||||
+10
-4
@@ -199,6 +199,7 @@
|
||||
- **Cinemax**
|
||||
- **CiscoLiveSearch**
|
||||
- **CiscoLiveSession**
|
||||
- **ciscowebex**: Cisco Webex
|
||||
- **CJSW**
|
||||
- **cliphunter**
|
||||
- **Clippit**
|
||||
@@ -379,6 +380,7 @@
|
||||
- **gem.cbc.ca:live**
|
||||
- **gem.cbc.ca:playlist**
|
||||
- **generic**: Generic downloader that works on some sites
|
||||
- **Gettr**
|
||||
- **Gfycat**
|
||||
- **GiantBomb**
|
||||
- **Giga**
|
||||
@@ -392,6 +394,7 @@
|
||||
- **google:podcasts**
|
||||
- **google:podcasts:feed**
|
||||
- **GoogleDrive**
|
||||
- **GoPro**
|
||||
- **Goshgay**
|
||||
- **GoToStage**
|
||||
- **GPUTechConf**
|
||||
@@ -630,6 +633,8 @@
|
||||
- **MyviEmbed**
|
||||
- **MyVisionTV**
|
||||
- **n-tv.de**
|
||||
- **N1Info:article**
|
||||
- **N1InfoAsset**
|
||||
- **natgeo:video**
|
||||
- **NationalGeographicTV**
|
||||
- **Naver**
|
||||
@@ -905,6 +910,7 @@
|
||||
- **RTVNH**
|
||||
- **RTVS**
|
||||
- **RUHD**
|
||||
- **RumbleChannel**
|
||||
- **RumbleEmbed**
|
||||
- **rutube**: Rutube videos
|
||||
- **rutube:channel**: Rutube channels
|
||||
@@ -1065,7 +1071,8 @@
|
||||
- **TheScene**
|
||||
- **TheStar**
|
||||
- **TheSun**
|
||||
- **Theta**
|
||||
- **ThetaStream**
|
||||
- **ThetaVideo**
|
||||
- **TheWeatherChannel**
|
||||
- **ThisAmericanLife**
|
||||
- **ThisAV**
|
||||
@@ -1174,6 +1181,7 @@
|
||||
- **Varzesh3**
|
||||
- **Vbox7**
|
||||
- **VeeHD**
|
||||
- **Veo**
|
||||
- **Veoh**
|
||||
- **Vesti**: Вести.Ru
|
||||
- **Vevo**
|
||||
@@ -1202,9 +1210,6 @@
|
||||
- **VidioLive**
|
||||
- **VidioPremier**
|
||||
- **VidLii**
|
||||
- **vidme**
|
||||
- **vidme:user**
|
||||
- **vidme:user:likes**
|
||||
- **vier**: vier.be and vijf.be
|
||||
- **vier:videos**
|
||||
- **viewlift**
|
||||
@@ -1256,6 +1261,7 @@
|
||||
- **VTXTV**
|
||||
- **vube**: Vube.com
|
||||
- **VuClip**
|
||||
- **Vupload**
|
||||
- **VVVVID**
|
||||
- **VVVVIDShow**
|
||||
- **VyboryMos**
|
||||
|
||||
+3
-3
@@ -22,7 +22,7 @@ from yt_dlp.utils import (
|
||||
)
|
||||
|
||||
|
||||
if "pytest" in sys.modules:
|
||||
if 'pytest' in sys.modules:
|
||||
import pytest
|
||||
is_download_test = pytest.mark.download
|
||||
else:
|
||||
@@ -32,9 +32,9 @@ else:
|
||||
|
||||
def get_params(override=None):
|
||||
PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"parameters.json")
|
||||
'parameters.json')
|
||||
LOCAL_PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"local_parameters.json")
|
||||
'local_parameters.json')
|
||||
with io.open(PARAMETERS_FILE, encoding='utf-8') as pf:
|
||||
parameters = json.load(pf)
|
||||
if os.path.exists(LOCAL_PARAMETERS_FILE):
|
||||
|
||||
@@ -649,10 +649,11 @@ class TestYoutubeDL(unittest.TestCase):
|
||||
'title2': '%PATH%',
|
||||
'title3': 'foo/bar\\test',
|
||||
'title4': 'foo "bar" test',
|
||||
'title5': 'áéí',
|
||||
'title5': 'áéí 𝐀',
|
||||
'timestamp': 1618488000,
|
||||
'duration': 100000,
|
||||
'playlist_index': 1,
|
||||
'playlist_autonumber': 2,
|
||||
'_last_playlist_index': 100,
|
||||
'n_entries': 10,
|
||||
'formats': [{'id': 'id1'}, {'id': 'id2'}, {'id': 'id3'}]
|
||||
@@ -665,8 +666,7 @@ class TestYoutubeDL(unittest.TestCase):
|
||||
ydl._num_downloads = 1
|
||||
self.assertEqual(ydl.validate_outtmpl(tmpl), None)
|
||||
|
||||
outtmpl, tmpl_dict = ydl.prepare_outtmpl(tmpl, info or self.outtmpl_info)
|
||||
out = ydl.escape_outtmpl(outtmpl) % tmpl_dict
|
||||
out = ydl.evaluate_outtmpl(tmpl, info or self.outtmpl_info)
|
||||
fname = ydl.prepare_filename(info or self.outtmpl_info)
|
||||
|
||||
if not isinstance(expected, (list, tuple)):
|
||||
@@ -690,6 +690,7 @@ class TestYoutubeDL(unittest.TestCase):
|
||||
test('%(duration_string)s', ('27:46:40', '27-46-40'))
|
||||
test('%(resolution)s', '1080p')
|
||||
test('%(playlist_index)s', '001')
|
||||
test('%(playlist_autonumber)s', '02')
|
||||
test('%(autonumber)s', '00001')
|
||||
test('%(autonumber+2)03d', '005', autonumber_start=3)
|
||||
test('%(autonumber)s', '001', autonumber_size=3)
|
||||
@@ -765,10 +766,15 @@ class TestYoutubeDL(unittest.TestCase):
|
||||
|
||||
# Custom type casting
|
||||
test('%(formats.:.id)l', 'id1, id2, id3')
|
||||
test('%(formats.:.id)#l', ('id1\nid2\nid3', 'id1 id2 id3'))
|
||||
test('%(ext)l', 'mp4')
|
||||
test('%(formats.:.id) 15l', ' id1, id2, id3')
|
||||
test('%(formats)j', (json.dumps(FORMATS), sanitize(json.dumps(FORMATS))))
|
||||
test('%(title5).3B', 'á')
|
||||
test('%(title5)U', 'áéí 𝐀')
|
||||
test('%(title5)#U', 'a\u0301e\u0301i\u0301 𝐀')
|
||||
test('%(title5)+U', 'áéí A')
|
||||
test('%(title5)+#U', 'a\u0301e\u0301i\u0301 A')
|
||||
if compat_os_name == 'nt':
|
||||
test('%(title4)q', ('"foo \\"bar\\" test"', "'foo _'bar_' test'"))
|
||||
else:
|
||||
|
||||
+265
-199
@@ -9,6 +9,7 @@ import copy
|
||||
import datetime
|
||||
import errno
|
||||
import fileinput
|
||||
import functools
|
||||
import io
|
||||
import itertools
|
||||
import json
|
||||
@@ -25,6 +26,7 @@ import time
|
||||
import tokenize
|
||||
import traceback
|
||||
import random
|
||||
import unicodedata
|
||||
|
||||
from string import ascii_letters
|
||||
|
||||
@@ -41,6 +43,7 @@ from .compat import (
|
||||
compat_urllib_error,
|
||||
compat_urllib_request,
|
||||
compat_urllib_request_DataHandler,
|
||||
windows_enable_vt_mode,
|
||||
)
|
||||
from .cookies import load_cookies
|
||||
from .utils import (
|
||||
@@ -66,8 +69,6 @@ from .utils import (
|
||||
float_or_none,
|
||||
format_bytes,
|
||||
format_field,
|
||||
STR_FORMAT_RE_TMPL,
|
||||
STR_FORMAT_TYPES,
|
||||
formatSeconds,
|
||||
GeoRestrictedError,
|
||||
HEADRequest,
|
||||
@@ -100,9 +101,13 @@ from .utils import (
|
||||
sanitize_url,
|
||||
sanitized_Request,
|
||||
std_headers,
|
||||
STR_FORMAT_RE_TMPL,
|
||||
STR_FORMAT_TYPES,
|
||||
str_or_none,
|
||||
strftime_or_none,
|
||||
subtitles_filename,
|
||||
supports_terminal_sequences,
|
||||
TERMINAL_SEQUENCES,
|
||||
ThrottledDownload,
|
||||
to_high_limit_path,
|
||||
traverse_obj,
|
||||
@@ -122,7 +127,7 @@ from .extractor import (
|
||||
gen_extractor_classes,
|
||||
get_info_extractor,
|
||||
_LAZY_LOADER,
|
||||
_PLUGIN_CLASSES
|
||||
_PLUGIN_CLASSES as plugin_extractors
|
||||
)
|
||||
from .extractor.openload import PhantomJSwrapper
|
||||
from .downloader import (
|
||||
@@ -133,6 +138,7 @@ from .downloader import (
|
||||
from .downloader.rtmp import rtmpdump_version
|
||||
from .postprocessor import (
|
||||
get_postprocessor,
|
||||
EmbedThumbnailPP,
|
||||
FFmpegFixupDurationPP,
|
||||
FFmpegFixupM3u8PP,
|
||||
FFmpegFixupM4aPP,
|
||||
@@ -141,6 +147,7 @@ from .postprocessor import (
|
||||
FFmpegMergerPP,
|
||||
FFmpegPostProcessor,
|
||||
MoveFilesAfterDownloadPP,
|
||||
_PLUGIN_CLASSES as plugin_postprocessors
|
||||
)
|
||||
from .update import detect_variant
|
||||
from .version import __version__
|
||||
@@ -246,6 +253,7 @@ class YoutubeDL(object):
|
||||
rejecttitle: Reject downloads for matching titles.
|
||||
logger: Log messages to a logging.Logger instance.
|
||||
logtostderr: Log messages to stderr instead of stdout.
|
||||
consoletitle: Display progress in console window's titlebar.
|
||||
writedescription: Write the video description to a .description file
|
||||
writeinfojson: Write the video description to a .info.json file
|
||||
clean_infojson: Remove private fields from the infojson
|
||||
@@ -323,7 +331,8 @@ class YoutubeDL(object):
|
||||
* when: When to run the postprocessor. Can be one of
|
||||
pre_process|before_dl|post_process|after_move.
|
||||
Assumed to be 'post_process' if not given
|
||||
post_hooks: A list of functions that get called as the final step
|
||||
post_hooks: Deprecated - Register a custom postprocessor instead
|
||||
A list of functions that get called as the final step
|
||||
for each video file, after all postprocessors have been
|
||||
called. The filename will be passed as the only argument.
|
||||
progress_hooks: A list of functions that get called on download
|
||||
@@ -351,6 +360,15 @@ class YoutubeDL(object):
|
||||
|
||||
Progress hooks are guaranteed to be called at least once
|
||||
(with status "finished") if the download is successful.
|
||||
postprocessor_hooks: A list of functions that get called on postprocessing
|
||||
progress, with a dictionary with the entries
|
||||
* status: One of "started", "processing", or "finished".
|
||||
Check this first and ignore unknown values.
|
||||
* postprocessor: Name of the postprocessor
|
||||
* info_dict: The extracted info_dict
|
||||
|
||||
Progress hooks are guaranteed to be called at least twice
|
||||
(with status "started" and "finished") if the processing is successful.
|
||||
merge_output_format: Extension to use when merging formats.
|
||||
final_ext: Expected final extension; used to detect when the file was
|
||||
already downloaded and converted. "merge_output_format" is
|
||||
@@ -407,15 +425,20 @@ class YoutubeDL(object):
|
||||
use downloader suggested by extractor if None.
|
||||
compat_opts: Compatibility options. See "Differences in default behavior".
|
||||
The following options do not work when used through the API:
|
||||
filename, abort-on-error, multistreams, no-live-chat,
|
||||
filename, abort-on-error, multistreams, no-live-chat, format-sort
|
||||
no-clean-infojson, no-playlist-metafiles, no-keep-subs.
|
||||
Refer __init__.py for their implementation
|
||||
progress_template: Dictionary of templates for progress outputs.
|
||||
Allowed keys are 'download', 'postprocess',
|
||||
'download-title' (console title) and 'postprocess-title'.
|
||||
The template is mapped on a dictionary with keys 'progress' and 'info'
|
||||
|
||||
The following parameters are not used by YoutubeDL itself, they are used by
|
||||
the downloader (see yt_dlp/downloader/common.py):
|
||||
nopart, updatetime, buffersize, ratelimit, throttledratelimit, min_filesize,
|
||||
max_filesize, test, noresizebuffer, retries, continuedl, noprogress, consoletitle,
|
||||
xattr_set_filesize, external_downloader_args, hls_use_mpegts, http_chunk_size.
|
||||
max_filesize, test, noresizebuffer, retries, fragment_retries, continuedl,
|
||||
noprogress, xattr_set_filesize, hls_use_mpegts, http_chunk_size,
|
||||
external_downloader_args.
|
||||
|
||||
The following options are used by the post processors:
|
||||
prefer_ffmpeg: If False, use avconv instead of ffmpeg if both are available,
|
||||
@@ -452,13 +475,12 @@ class YoutubeDL(object):
|
||||
|
||||
_NUMERIC_FIELDS = set((
|
||||
'width', 'height', 'tbr', 'abr', 'asr', 'vbr', 'fps', 'filesize', 'filesize_approx',
|
||||
'timestamp', 'upload_year', 'upload_month', 'upload_day',
|
||||
'timestamp', 'release_timestamp',
|
||||
'duration', 'view_count', 'like_count', 'dislike_count', 'repost_count',
|
||||
'average_rating', 'comment_count', 'age_limit',
|
||||
'start_time', 'end_time',
|
||||
'chapter_number', 'season_number', 'episode_number',
|
||||
'track_number', 'disc_number', 'release_year',
|
||||
'playlist_index',
|
||||
))
|
||||
|
||||
params = None
|
||||
@@ -483,26 +505,27 @@ class YoutubeDL(object):
|
||||
self._first_webpage_request = True
|
||||
self._post_hooks = []
|
||||
self._progress_hooks = []
|
||||
self._postprocessor_hooks = []
|
||||
self._download_retcode = 0
|
||||
self._num_downloads = 0
|
||||
self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
|
||||
self._err_file = sys.stderr
|
||||
self.params = {
|
||||
# Default parameters
|
||||
'nocheckcertificate': False,
|
||||
}
|
||||
self.params.update(params)
|
||||
self.params = params
|
||||
self.cache = Cache(self)
|
||||
|
||||
windows_enable_vt_mode()
|
||||
self.params['no_color'] = self.params.get('no_color') or not supports_terminal_sequences(self._err_file)
|
||||
|
||||
if sys.version_info < (3, 6):
|
||||
self.report_warning(
|
||||
'Python version %d.%d is not supported! Please update to Python 3.6 or above' % sys.version_info[:2])
|
||||
|
||||
if self.params.get('allow_unplayable_formats'):
|
||||
self.report_warning(
|
||||
'You have asked for unplayable formats to be listed/downloaded. '
|
||||
'This is a developer option intended for debugging. '
|
||||
'If you experience any issues while using this option, DO NOT open a bug report')
|
||||
f'You have asked for {self._color_text("unplayable formats", "blue")} to be listed/downloaded. '
|
||||
'This is a developer option intended for debugging. \n'
|
||||
' If you experience any issues while using this option, '
|
||||
f'{self._color_text("DO NOT", "red")} open a bug report')
|
||||
|
||||
def check_deprecated(param, option, suggestion):
|
||||
if self.params.get(param) is not None:
|
||||
@@ -521,13 +544,13 @@ class YoutubeDL(object):
|
||||
for msg in self.params.get('warnings', []):
|
||||
self.report_warning(msg)
|
||||
|
||||
if self.params.get('overwrites') is None:
|
||||
self.params.pop('overwrites', None)
|
||||
elif self.params.get('nooverwrites') is not None:
|
||||
if 'overwrites' not in self.params and self.params.get('nooverwrites') is not None:
|
||||
# nooverwrites was unnecessarily changed to overwrites
|
||||
# in 0c3d0f51778b153f65c21906031c2e091fcfb641
|
||||
# This ensures compatibility with both keys
|
||||
self.params['overwrites'] = not self.params['nooverwrites']
|
||||
elif self.params.get('overwrites') is None:
|
||||
self.params.pop('overwrites', None)
|
||||
else:
|
||||
self.params['nooverwrites'] = not self.params['overwrites']
|
||||
|
||||
@@ -577,8 +600,8 @@ class YoutubeDL(object):
|
||||
|
||||
self._setup_opener()
|
||||
|
||||
"""Preload the archive, if any is specified"""
|
||||
def preload_download_archive(fn):
|
||||
"""Preload the archive, if any is specified"""
|
||||
if fn is None:
|
||||
return False
|
||||
self.write_debug('Loading archive file %r\n' % fn)
|
||||
@@ -674,9 +697,13 @@ class YoutubeDL(object):
|
||||
self._post_hooks.append(ph)
|
||||
|
||||
def add_progress_hook(self, ph):
|
||||
"""Add the progress hook (currently only for the file downloader)"""
|
||||
"""Add the download progress hook"""
|
||||
self._progress_hooks.append(ph)
|
||||
|
||||
def add_postprocessor_hook(self, ph):
|
||||
"""Add the postprocessing progress hook"""
|
||||
self._postprocessor_hooks.append(ph)
|
||||
|
||||
def _bidi_workaround(self, message):
|
||||
if not hasattr(self, '_output_channel'):
|
||||
return message
|
||||
@@ -789,6 +816,11 @@ class YoutubeDL(object):
|
||||
self.to_stdout(
|
||||
message, skip_eol, quiet=self.params.get('quiet', False))
|
||||
|
||||
def _color_text(self, text, color):
|
||||
if self.params.get('no_color'):
|
||||
return text
|
||||
return f'{TERMINAL_SEQUENCES[color.upper()]}{text}{TERMINAL_SEQUENCES["RESET_STYLE"]}'
|
||||
|
||||
def report_warning(self, message, only_once=False):
|
||||
'''
|
||||
Print the message to stderr, it will be prefixed with 'WARNING:'
|
||||
@@ -799,24 +831,14 @@ class YoutubeDL(object):
|
||||
else:
|
||||
if self.params.get('no_warnings'):
|
||||
return
|
||||
if not self.params.get('no_color') and self._err_file.isatty() and compat_os_name != 'nt':
|
||||
_msg_header = '\033[0;33mWARNING:\033[0m'
|
||||
else:
|
||||
_msg_header = 'WARNING:'
|
||||
warning_message = '%s %s' % (_msg_header, message)
|
||||
self.to_stderr(warning_message, only_once)
|
||||
self.to_stderr(f'{self._color_text("WARNING:", "yellow")} {message}', only_once)
|
||||
|
||||
def report_error(self, message, tb=None):
|
||||
'''
|
||||
Do the same as trouble, but prefixes the message with 'ERROR:', colored
|
||||
in red if stderr is a tty file.
|
||||
'''
|
||||
if not self.params.get('no_color') and self._err_file.isatty() and compat_os_name != 'nt':
|
||||
_msg_header = '\033[0;31mERROR:\033[0m'
|
||||
else:
|
||||
_msg_header = 'ERROR:'
|
||||
error_message = '%s %s' % (_msg_header, message)
|
||||
self.trouble(error_message, tb)
|
||||
self.trouble(f'{self._color_text("ERROR:", "red")} {message}', tb)
|
||||
|
||||
def write_debug(self, message, only_once=False):
|
||||
'''Log debug message or Print message to stderr'''
|
||||
@@ -858,7 +880,7 @@ class YoutubeDL(object):
|
||||
outtmpl_dict = {'default': outtmpl_dict}
|
||||
outtmpl_dict.update({
|
||||
k: v for k, v in DEFAULT_OUTTMPL.items()
|
||||
if not outtmpl_dict.get(k)})
|
||||
if outtmpl_dict.get(k) is None})
|
||||
for key, val in outtmpl_dict.items():
|
||||
if isinstance(val, bytes):
|
||||
self.report_warning(
|
||||
@@ -908,7 +930,7 @@ class YoutubeDL(object):
|
||||
def validate_outtmpl(cls, outtmpl):
|
||||
''' @return None or Exception object '''
|
||||
outtmpl = re.sub(
|
||||
STR_FORMAT_RE_TMPL.format('[^)]*', '[ljqB]'),
|
||||
STR_FORMAT_RE_TMPL.format('[^)]*', '[ljqBU]'),
|
||||
lambda mobj: f'{mobj.group(0)[:-1]}s',
|
||||
cls._outtmpl_expandpath(outtmpl))
|
||||
try:
|
||||
@@ -918,7 +940,7 @@ class YoutubeDL(object):
|
||||
return err
|
||||
|
||||
def prepare_outtmpl(self, outtmpl, info_dict, sanitize=None):
|
||||
""" Make the template and info_dict suitable for substitution : ydl.outtmpl_escape(outtmpl) % info_dict """
|
||||
""" Make the outtmpl and info_dict suitable for substitution: ydl.escape_outtmpl(outtmpl) % info_dict """
|
||||
info_dict.setdefault('epoch', int(time.time())) # keep epoch consistent once set
|
||||
|
||||
info_dict = dict(info_dict) # Do not sanitize so as not to consume LazyList
|
||||
@@ -932,15 +954,16 @@ class YoutubeDL(object):
|
||||
if info_dict.get('resolution') is None:
|
||||
info_dict['resolution'] = self.format_resolution(info_dict, default=None)
|
||||
|
||||
# For fields playlist_index and autonumber convert all occurrences
|
||||
# For fields playlist_index, playlist_autonumber and autonumber convert all occurrences
|
||||
# of %(field)s to %(field)0Nd for backward compatibility
|
||||
field_size_compat_map = {
|
||||
'playlist_index': len(str(info_dict.get('_last_playlist_index') or '')),
|
||||
'playlist_autonumber': len(str(info_dict.get('n_entries') or '')),
|
||||
'autonumber': self.params.get('autonumber_size') or 5,
|
||||
}
|
||||
|
||||
TMPL_DICT = {}
|
||||
EXTERNAL_FORMAT_RE = re.compile(STR_FORMAT_RE_TMPL.format('[^)]*', f'[{STR_FORMAT_TYPES}ljqB]'))
|
||||
EXTERNAL_FORMAT_RE = re.compile(STR_FORMAT_RE_TMPL.format('[^)]*', f'[{STR_FORMAT_TYPES}ljqBU]'))
|
||||
MATH_FUNCTIONS = {
|
||||
'+': float.__add__,
|
||||
'-': float.__sub__,
|
||||
@@ -1031,21 +1054,27 @@ class YoutubeDL(object):
|
||||
value = default if value is None else value
|
||||
|
||||
str_fmt = f'{fmt[:-1]}s'
|
||||
if fmt[-1] == 'l':
|
||||
value, fmt = ', '.join(variadic(value)), str_fmt
|
||||
elif fmt[-1] == 'j':
|
||||
if fmt[-1] == 'l': # list
|
||||
delim = '\n' if '#' in (outer_mobj.group('conversion') or '') else ', '
|
||||
value, fmt = delim.join(variadic(value)), str_fmt
|
||||
elif fmt[-1] == 'j': # json
|
||||
value, fmt = json.dumps(value, default=_dumpjson_default), str_fmt
|
||||
elif fmt[-1] == 'q':
|
||||
elif fmt[-1] == 'q': # quoted
|
||||
value, fmt = compat_shlex_quote(str(value)), str_fmt
|
||||
elif fmt[-1] == 'B':
|
||||
elif fmt[-1] == 'B': # bytes
|
||||
value = f'%{str_fmt}'.encode('utf-8') % str(value).encode('utf-8')
|
||||
value, fmt = value.decode('utf-8', 'ignore'), 's'
|
||||
elif fmt[-1] == 'U': # unicode normalized
|
||||
opts = outer_mobj.group('conversion') or ''
|
||||
value, fmt = unicodedata.normalize(
|
||||
# "+" = compatibility equivalence, "#" = NFD
|
||||
'NF%s%s' % ('K' if '+' in opts else '', 'D' if '#' in opts else 'C'),
|
||||
value), str_fmt
|
||||
elif fmt[-1] == 'c':
|
||||
value = str(value)
|
||||
if value is None:
|
||||
value, fmt = default, 's'
|
||||
if value:
|
||||
value = str(value)[0]
|
||||
else:
|
||||
value = value[0]
|
||||
fmt = str_fmt
|
||||
elif fmt[-1] not in 'rs': # numeric
|
||||
value = float_or_none(value)
|
||||
if value is None:
|
||||
@@ -1065,6 +1094,10 @@ class YoutubeDL(object):
|
||||
|
||||
return EXTERNAL_FORMAT_RE.sub(create_key, outtmpl), TMPL_DICT
|
||||
|
||||
def evaluate_outtmpl(self, outtmpl, info_dict, *args, **kwargs):
|
||||
outtmpl, info_dict = self.prepare_outtmpl(outtmpl, info_dict, *args, **kwargs)
|
||||
return self.escape_outtmpl(outtmpl) % info_dict
|
||||
|
||||
def _prepare_filename(self, info_dict, tmpl_type='default'):
|
||||
try:
|
||||
sanitize = lambda k, v: sanitize_filename(
|
||||
@@ -1077,7 +1110,7 @@ class YoutubeDL(object):
|
||||
filename = outtmpl % template_dict
|
||||
|
||||
force_ext = OUTTMPL_TYPES.get(tmpl_type)
|
||||
if force_ext is not None:
|
||||
if filename and force_ext is not None:
|
||||
filename = replace_extension(filename, force_ext, info_dict.get('ext'))
|
||||
|
||||
# https://github.com/blackjack4494/youtube-dlc/issues/85
|
||||
@@ -1099,6 +1132,8 @@ class YoutubeDL(object):
|
||||
"""Generate the output filename."""
|
||||
|
||||
filename = self._prepare_filename(info_dict, dir_type or 'default')
|
||||
if not filename and dir_type not in ('', 'temp'):
|
||||
return ''
|
||||
|
||||
if warn:
|
||||
if not self.params.get('paths'):
|
||||
@@ -1221,7 +1256,7 @@ class YoutubeDL(object):
|
||||
self.report_error('no suitable InfoExtractor for URL %s' % url)
|
||||
|
||||
def __handle_extraction_exceptions(func):
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
@@ -1301,7 +1336,7 @@ class YoutubeDL(object):
|
||||
or extract_flat is True):
|
||||
info_copy = ie_result.copy()
|
||||
ie = try_get(ie_result.get('ie_key'), self.get_info_extractor)
|
||||
if not ie_result.get('id'):
|
||||
if ie and not ie_result.get('id'):
|
||||
info_copy['id'] = ie.get_temp_id(ie_result['url'])
|
||||
self.add_default_extra_info(info_copy, ie, ie_result['url'])
|
||||
self.add_extra_info(info_copy, extra_info)
|
||||
@@ -1510,38 +1545,14 @@ class YoutubeDL(object):
|
||||
}
|
||||
ie_copy.update(dict(ie_result))
|
||||
|
||||
if self.params.get('writeinfojson', False):
|
||||
infofn = self.prepare_filename(ie_copy, 'pl_infojson')
|
||||
if not self._ensure_dir_exists(encodeFilename(infofn)):
|
||||
return
|
||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
|
||||
self.to_screen('[info] Playlist metadata is already present')
|
||||
else:
|
||||
self.to_screen('[info] Writing playlist metadata as JSON to: ' + infofn)
|
||||
try:
|
||||
write_json_file(self.sanitize_info(ie_result, self.params.get('clean_infojson', True)), infofn)
|
||||
except (OSError, IOError):
|
||||
self.report_error('Cannot write playlist metadata to JSON file ' + infofn)
|
||||
|
||||
if self._write_info_json('playlist', ie_result,
|
||||
self.prepare_filename(ie_copy, 'pl_infojson')) is None:
|
||||
return
|
||||
if self._write_description('playlist', ie_result,
|
||||
self.prepare_filename(ie_copy, 'pl_description')) is None:
|
||||
return
|
||||
# TODO: This should be passed to ThumbnailsConvertor if necessary
|
||||
self._write_thumbnails(ie_copy, self.prepare_filename(ie_copy, 'pl_thumbnail'))
|
||||
|
||||
if self.params.get('writedescription', False):
|
||||
descfn = self.prepare_filename(ie_copy, 'pl_description')
|
||||
if not self._ensure_dir_exists(encodeFilename(descfn)):
|
||||
return
|
||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
|
||||
self.to_screen('[info] Playlist description is already present')
|
||||
elif ie_result.get('description') is None:
|
||||
self.report_warning('There\'s no playlist description to write.')
|
||||
else:
|
||||
try:
|
||||
self.to_screen('[info] Writing playlist description to: ' + descfn)
|
||||
with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
|
||||
descfile.write(ie_result['description'])
|
||||
except (OSError, IOError):
|
||||
self.report_error('Cannot write playlist description file ' + descfn)
|
||||
return
|
||||
self._write_thumbnails('playlist', ie_copy, self.prepare_filename(ie_copy, 'pl_thumbnail'))
|
||||
|
||||
if self.params.get('playlistreverse', False):
|
||||
entries = entries[::-1]
|
||||
@@ -1958,9 +1969,14 @@ class YoutubeDL(object):
|
||||
filter_f = lambda f: _filter_f(f) and (
|
||||
f.get('vcodec') != 'none' or f.get('acodec') != 'none')
|
||||
else:
|
||||
filter_f = ((lambda f: f.get('ext') == format_spec)
|
||||
if format_spec in ['mp4', 'flv', 'webm', '3gp', 'm4a', 'mp3', 'ogg', 'aac', 'wav'] # extension
|
||||
else (lambda f: f.get('format_id') == format_spec)) # id
|
||||
if format_spec in ('m4a', 'mp3', 'ogg', 'aac'): # audio extension
|
||||
filter_f = lambda f: f.get('ext') == format_spec and f.get('acodec') != 'none'
|
||||
elif format_spec in ('mp4', 'flv', 'webm', '3gp'): # video extension
|
||||
filter_f = lambda f: f.get('ext') == format_spec and f.get('acodec') != 'none' and f.get('vcodec') != 'none'
|
||||
elif format_spec in ('mhtml', ): # storyboards extension
|
||||
filter_f = lambda f: f.get('ext') == format_spec and f.get('acodec') == 'none' and f.get('vcodec') == 'none'
|
||||
else:
|
||||
filter_f = lambda f: f.get('format_id') == format_spec # id
|
||||
|
||||
def selector_function(ctx):
|
||||
formats = list(ctx['formats'])
|
||||
@@ -2440,10 +2456,12 @@ class YoutubeDL(object):
|
||||
if self.params.get('forceprint') or self.params.get('forcejson'):
|
||||
self.post_extract(info_dict)
|
||||
for tmpl in self.params.get('forceprint', []):
|
||||
if re.match(r'\w+$', tmpl):
|
||||
mobj = re.match(r'\w+(=?)$', tmpl)
|
||||
if mobj and mobj.group(1):
|
||||
tmpl = f'{tmpl[:-1]} = %({tmpl[:-1]})s'
|
||||
elif mobj:
|
||||
tmpl = '%({})s'.format(tmpl)
|
||||
tmpl, info_copy = self.prepare_outtmpl(tmpl, info_dict)
|
||||
self.to_stdout(self.escape_outtmpl(tmpl) % info_copy)
|
||||
self.to_stdout(self.evaluate_outtmpl(tmpl, info_dict))
|
||||
|
||||
print_mandatory('title')
|
||||
print_mandatory('id')
|
||||
@@ -2521,37 +2539,43 @@ class YoutubeDL(object):
|
||||
if self.params.get('simulate'):
|
||||
if self.params.get('force_write_download_archive', False):
|
||||
self.record_download_archive(info_dict)
|
||||
|
||||
# Do nothing else if in simulate mode
|
||||
return
|
||||
|
||||
if full_filename is None:
|
||||
return
|
||||
|
||||
if not self._ensure_dir_exists(encodeFilename(full_filename)):
|
||||
return
|
||||
if not self._ensure_dir_exists(encodeFilename(temp_filename)):
|
||||
return
|
||||
|
||||
if self.params.get('writedescription', False):
|
||||
descfn = self.prepare_filename(info_dict, 'description')
|
||||
if not self._ensure_dir_exists(encodeFilename(descfn)):
|
||||
return
|
||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(descfn)):
|
||||
self.to_screen('[info] Video description is already present')
|
||||
elif info_dict.get('description') is None:
|
||||
self.report_warning('There\'s no description to write.')
|
||||
else:
|
||||
try:
|
||||
self.to_screen('[info] Writing video description to: ' + descfn)
|
||||
with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
|
||||
descfile.write(info_dict['description'])
|
||||
except (OSError, IOError):
|
||||
self.report_error('Cannot write description file ' + descfn)
|
||||
return
|
||||
if self._write_description('video', info_dict,
|
||||
self.prepare_filename(info_dict, 'description')) is None:
|
||||
return
|
||||
|
||||
sub_files = self._write_subtitles(info_dict, temp_filename)
|
||||
if sub_files is None:
|
||||
return
|
||||
files_to_move.update(dict(sub_files))
|
||||
|
||||
thumb_files = self._write_thumbnails(
|
||||
'video', info_dict, temp_filename, self.prepare_filename(info_dict, 'thumbnail'))
|
||||
if thumb_files is None:
|
||||
return
|
||||
files_to_move.update(dict(thumb_files))
|
||||
|
||||
infofn = self.prepare_filename(info_dict, 'infojson')
|
||||
_infojson_written = self._write_info_json('video', info_dict, infofn)
|
||||
if _infojson_written:
|
||||
info_dict['__infojson_filename'] = infofn
|
||||
elif _infojson_written is None:
|
||||
return
|
||||
|
||||
# Note: Annotations are deprecated
|
||||
annofn = None
|
||||
if self.params.get('writeannotations', False):
|
||||
annofn = self.prepare_filename(info_dict, 'annotation')
|
||||
if annofn:
|
||||
if not self._ensure_dir_exists(encodeFilename(annofn)):
|
||||
return
|
||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(annofn)):
|
||||
@@ -2569,69 +2593,6 @@ class YoutubeDL(object):
|
||||
self.report_error('Cannot write annotations file: ' + annofn)
|
||||
return
|
||||
|
||||
subtitles_are_requested = any([self.params.get('writesubtitles', False),
|
||||
self.params.get('writeautomaticsub')])
|
||||
|
||||
if subtitles_are_requested and info_dict.get('requested_subtitles'):
|
||||
# subtitles download errors are already managed as troubles in relevant IE
|
||||
# that way it will silently go on when used with unsupporting IE
|
||||
subtitles = info_dict['requested_subtitles']
|
||||
# ie = self.get_info_extractor(info_dict['extractor_key'])
|
||||
for sub_lang, sub_info in subtitles.items():
|
||||
sub_format = sub_info['ext']
|
||||
sub_filename = subtitles_filename(temp_filename, sub_lang, sub_format, info_dict.get('ext'))
|
||||
sub_filename_final = subtitles_filename(
|
||||
self.prepare_filename(info_dict, 'subtitle'), sub_lang, sub_format, info_dict.get('ext'))
|
||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(sub_filename)):
|
||||
self.to_screen('[info] Video subtitle %s.%s is already present' % (sub_lang, sub_format))
|
||||
sub_info['filepath'] = sub_filename
|
||||
files_to_move[sub_filename] = sub_filename_final
|
||||
else:
|
||||
self.to_screen('[info] Writing video subtitles to: ' + sub_filename)
|
||||
if sub_info.get('data') is not None:
|
||||
try:
|
||||
# Use newline='' to prevent conversion of newline characters
|
||||
# See https://github.com/ytdl-org/youtube-dl/issues/10268
|
||||
with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8', newline='') as subfile:
|
||||
subfile.write(sub_info['data'])
|
||||
sub_info['filepath'] = sub_filename
|
||||
files_to_move[sub_filename] = sub_filename_final
|
||||
except (OSError, IOError):
|
||||
self.report_error('Cannot write subtitles file ' + sub_filename)
|
||||
return
|
||||
else:
|
||||
try:
|
||||
sub_copy = sub_info.copy()
|
||||
sub_copy.setdefault('http_headers', info_dict.get('http_headers'))
|
||||
self.dl(sub_filename, sub_copy, subtitle=True)
|
||||
sub_info['filepath'] = sub_filename
|
||||
files_to_move[sub_filename] = sub_filename_final
|
||||
except (ExtractorError, IOError, OSError, ValueError) + network_exceptions as err:
|
||||
self.report_warning('Unable to download subtitle for "%s": %s' %
|
||||
(sub_lang, error_to_compat_str(err)))
|
||||
continue
|
||||
|
||||
if self.params.get('writeinfojson', False):
|
||||
infofn = self.prepare_filename(info_dict, 'infojson')
|
||||
if not self._ensure_dir_exists(encodeFilename(infofn)):
|
||||
return
|
||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(infofn)):
|
||||
self.to_screen('[info] Video metadata is already present')
|
||||
else:
|
||||
self.to_screen('[info] Writing video metadata as JSON to: ' + infofn)
|
||||
try:
|
||||
write_json_file(self.sanitize_info(info_dict, self.params.get('clean_infojson', True)), infofn)
|
||||
except (OSError, IOError):
|
||||
self.report_error('Cannot write video metadata to JSON file ' + infofn)
|
||||
return
|
||||
info_dict['__infojson_filename'] = infofn
|
||||
|
||||
for thumb_ext in self._write_thumbnails(info_dict, temp_filename):
|
||||
thumb_filename_temp = replace_extension(temp_filename, thumb_ext, info_dict.get('ext'))
|
||||
thumb_filename = replace_extension(
|
||||
self.prepare_filename(info_dict, 'thumbnail'), thumb_ext, info_dict.get('ext'))
|
||||
files_to_move[thumb_filename_temp] = thumb_filename
|
||||
|
||||
# Write internet shortcut files
|
||||
url_link = webloc_link = desktop_link = False
|
||||
if self.params.get('writelink', False):
|
||||
@@ -2743,10 +2704,19 @@ class YoutubeDL(object):
|
||||
|
||||
requested_formats = info_dict['requested_formats']
|
||||
old_ext = info_dict['ext']
|
||||
if self.params.get('merge_output_format') is None and not compatible_formats(requested_formats):
|
||||
info_dict['ext'] = 'mkv'
|
||||
self.report_warning(
|
||||
'Requested formats are incompatible for merge and will be merged into mkv.')
|
||||
if self.params.get('merge_output_format') is None:
|
||||
if not compatible_formats(requested_formats):
|
||||
info_dict['ext'] = 'mkv'
|
||||
self.report_warning(
|
||||
'Requested formats are incompatible for merge and will be merged into mkv')
|
||||
if (info_dict['ext'] == 'webm'
|
||||
and info_dict.get('thumbnails')
|
||||
# check with type instead of pp_key, __name__, or isinstance
|
||||
# since we dont want any custom PPs to trigger this
|
||||
and any(type(pp) == EmbedThumbnailPP for pp in self._pps['post_process'])):
|
||||
info_dict['ext'] = 'mkv'
|
||||
self.report_warning(
|
||||
'webm doesn\'t support embedding a thumbnail, mkv will be used')
|
||||
new_ext = info_dict['ext']
|
||||
|
||||
def correct_ext(filename, ext=new_ext):
|
||||
@@ -2886,7 +2856,8 @@ class YoutubeDL(object):
|
||||
|
||||
downloader = (get_suitable_downloader(info_dict, self.params).__name__
|
||||
if 'protocol' in info_dict else None)
|
||||
ffmpeg_fixup(downloader == 'HlsFD', 'malformed AAC bitstream detected', FFmpegFixupM3u8PP)
|
||||
ffmpeg_fixup(info_dict.get('requested_formats') is None and downloader == 'HlsFD',
|
||||
'malformed AAC bitstream detected', FFmpegFixupM3u8PP)
|
||||
ffmpeg_fixup(downloader == 'WebSocketFragmentFD', 'malformed timestamps detected', FFmpegFixupTimestampPP)
|
||||
ffmpeg_fixup(downloader == 'WebSocketFragmentFD', 'malformed duration detected', FFmpegFixupDurationPP)
|
||||
|
||||
@@ -3100,9 +3071,7 @@ class YoutubeDL(object):
|
||||
|
||||
@staticmethod
|
||||
def format_resolution(format, default='unknown'):
|
||||
if format.get('vcodec') == 'none':
|
||||
if format.get('acodec') == 'none':
|
||||
return 'images'
|
||||
if format.get('vcodec') == 'none' and format.get('acodec') != 'none':
|
||||
return 'audio only'
|
||||
if format.get('resolution') is not None:
|
||||
return format['resolution']
|
||||
@@ -3114,6 +3083,8 @@ class YoutubeDL(object):
|
||||
res = '%dx?' % format['width']
|
||||
else:
|
||||
res = default
|
||||
if format.get('vcodec') == 'none' and format.get('acodec') == 'none':
|
||||
res += ' (images)'
|
||||
return res
|
||||
|
||||
def _format_note(self, fdict):
|
||||
@@ -3273,9 +3244,10 @@ class YoutubeDL(object):
|
||||
self._write_string('[debug] yt-dlp version %s%s\n' % (__version__, '' if source == 'unknown' else f' ({source})'))
|
||||
if _LAZY_LOADER:
|
||||
self._write_string('[debug] Lazy loading extractors enabled\n')
|
||||
if _PLUGIN_CLASSES:
|
||||
self._write_string(
|
||||
'[debug] Plugin Extractors: %s\n' % [ie.ie_key() for ie in _PLUGIN_CLASSES])
|
||||
if plugin_extractors or plugin_postprocessors:
|
||||
self._write_string('[debug] Plugins: %s\n' % [
|
||||
'%s%s' % (klass.__name__, '' if klass.__name__ == name else f' as {name}')
|
||||
for name, klass in itertools.chain(plugin_extractors.items(), plugin_postprocessors.items())])
|
||||
if self.params.get('compat_opts'):
|
||||
self._write_string(
|
||||
'[debug] Compatibility options: %s\n' % ', '.join(self.params.get('compat_opts')))
|
||||
@@ -3409,39 +3381,133 @@ class YoutubeDL(object):
|
||||
encoding = preferredencoding()
|
||||
return encoding
|
||||
|
||||
def _write_thumbnails(self, info_dict, filename): # return the extensions
|
||||
def _write_info_json(self, label, ie_result, infofn):
|
||||
''' Write infojson and returns True = written, False = skip, None = error '''
|
||||
if not self.params.get('writeinfojson'):
|
||||
return False
|
||||
elif not infofn:
|
||||
self.write_debug(f'Skipping writing {label} infojson')
|
||||
return False
|
||||
elif not self._ensure_dir_exists(infofn):
|
||||
return None
|
||||
elif not self.params.get('overwrites', True) and os.path.exists(infofn):
|
||||
self.to_screen(f'[info] {label.title()} metadata is already present')
|
||||
else:
|
||||
self.to_screen(f'[info] Writing {label} metadata as JSON to: {infofn}')
|
||||
try:
|
||||
write_json_file(self.sanitize_info(ie_result, self.params.get('clean_infojson', True)), infofn)
|
||||
except (OSError, IOError):
|
||||
self.report_error(f'Cannot write {label} metadata to JSON file {infofn}')
|
||||
return None
|
||||
return True
|
||||
|
||||
def _write_description(self, label, ie_result, descfn):
|
||||
''' Write description and returns True = written, False = skip, None = error '''
|
||||
if not self.params.get('writedescription'):
|
||||
return False
|
||||
elif not descfn:
|
||||
self.write_debug(f'Skipping writing {label} description')
|
||||
return False
|
||||
elif not self._ensure_dir_exists(descfn):
|
||||
return None
|
||||
elif not self.params.get('overwrites', True) and os.path.exists(descfn):
|
||||
self.to_screen(f'[info] {label.title()} description is already present')
|
||||
elif ie_result.get('description') is None:
|
||||
self.report_warning(f'There\'s no {label} description to write')
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
self.to_screen(f'[info] Writing {label} description to: {descfn}')
|
||||
with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
|
||||
descfile.write(ie_result['description'])
|
||||
except (OSError, IOError):
|
||||
self.report_error(f'Cannot write {label} description file {descfn}')
|
||||
return None
|
||||
return True
|
||||
|
||||
def _write_subtitles(self, info_dict, filename):
|
||||
''' Write subtitles to file and return list of (sub_filename, final_sub_filename); or None if error'''
|
||||
ret = []
|
||||
subtitles = info_dict.get('requested_subtitles')
|
||||
if not subtitles or not (self.params.get('writesubtitles') or self.params.get('writeautomaticsub')):
|
||||
# subtitles download errors are already managed as troubles in relevant IE
|
||||
# that way it will silently go on when used with unsupporting IE
|
||||
return ret
|
||||
|
||||
sub_filename_base = self.prepare_filename(info_dict, 'subtitle')
|
||||
if not sub_filename_base:
|
||||
self.to_screen('[info] Skipping writing video subtitles')
|
||||
return ret
|
||||
for sub_lang, sub_info in subtitles.items():
|
||||
sub_format = sub_info['ext']
|
||||
sub_filename = subtitles_filename(filename, sub_lang, sub_format, info_dict.get('ext'))
|
||||
sub_filename_final = subtitles_filename(sub_filename_base, sub_lang, sub_format, info_dict.get('ext'))
|
||||
if not self.params.get('overwrites', True) and os.path.exists(sub_filename):
|
||||
self.to_screen(f'[info] Video subtitle {sub_lang}.{sub_format} is already present')
|
||||
sub_info['filepath'] = sub_filename
|
||||
ret.append((sub_filename, sub_filename_final))
|
||||
continue
|
||||
|
||||
self.to_screen(f'[info] Writing video subtitles to: {sub_filename}')
|
||||
if sub_info.get('data') is not None:
|
||||
try:
|
||||
# Use newline='' to prevent conversion of newline characters
|
||||
# See https://github.com/ytdl-org/youtube-dl/issues/10268
|
||||
with io.open(sub_filename, 'w', encoding='utf-8', newline='') as subfile:
|
||||
subfile.write(sub_info['data'])
|
||||
sub_info['filepath'] = sub_filename
|
||||
ret.append((sub_filename, sub_filename_final))
|
||||
continue
|
||||
except (OSError, IOError):
|
||||
self.report_error(f'Cannot write video subtitles file {sub_filename}')
|
||||
return None
|
||||
|
||||
try:
|
||||
sub_copy = sub_info.copy()
|
||||
sub_copy.setdefault('http_headers', info_dict.get('http_headers'))
|
||||
self.dl(sub_filename, sub_copy, subtitle=True)
|
||||
sub_info['filepath'] = sub_filename
|
||||
ret.append((sub_filename, sub_filename_final))
|
||||
except (ExtractorError, IOError, OSError, ValueError) + network_exceptions as err:
|
||||
self.report_warning(f'Unable to download video subtitles for {sub_lang!r}: {err}')
|
||||
continue
|
||||
return ret
|
||||
|
||||
def _write_thumbnails(self, label, info_dict, filename, thumb_filename_base=None):
|
||||
''' Write thumbnails to file and return list of (thumb_filename, final_thumb_filename) '''
|
||||
write_all = self.params.get('write_all_thumbnails', False)
|
||||
thumbnails = []
|
||||
thumbnails, ret = [], []
|
||||
if write_all or self.params.get('writethumbnail', False):
|
||||
thumbnails = info_dict.get('thumbnails') or []
|
||||
multiple = write_all and len(thumbnails) > 1
|
||||
|
||||
ret = []
|
||||
for t in thumbnails[::-1]:
|
||||
thumb_ext = determine_ext(t['url'], 'jpg')
|
||||
suffix = '%s.' % t['id'] if multiple else ''
|
||||
thumb_display_id = '%s ' % t['id'] if multiple else ''
|
||||
thumb_filename = replace_extension(filename, suffix + thumb_ext, info_dict.get('ext'))
|
||||
if thumb_filename_base is None:
|
||||
thumb_filename_base = filename
|
||||
if thumbnails and not thumb_filename_base:
|
||||
self.write_debug(f'Skipping writing {label} thumbnail')
|
||||
return ret
|
||||
|
||||
if not self.params.get('overwrites', True) and os.path.exists(encodeFilename(thumb_filename)):
|
||||
ret.append(suffix + thumb_ext)
|
||||
for t in thumbnails[::-1]:
|
||||
thumb_ext = (f'{t["id"]}.' if multiple else '') + determine_ext(t['url'], 'jpg')
|
||||
thumb_display_id = f'{label} thumbnail' + (f' {t["id"]}' if multiple else '')
|
||||
thumb_filename = replace_extension(filename, thumb_ext, info_dict.get('ext'))
|
||||
thumb_filename_final = replace_extension(thumb_filename_base, thumb_ext, info_dict.get('ext'))
|
||||
|
||||
if not self.params.get('overwrites', True) and os.path.exists(thumb_filename):
|
||||
ret.append((thumb_filename, thumb_filename_final))
|
||||
t['filepath'] = thumb_filename
|
||||
self.to_screen('[%s] %s: Thumbnail %sis already present' %
|
||||
(info_dict['extractor'], info_dict['id'], thumb_display_id))
|
||||
self.to_screen(f'[info] {thumb_display_id.title()} is already present')
|
||||
else:
|
||||
self.to_screen('[%s] %s: Downloading thumbnail %s ...' %
|
||||
(info_dict['extractor'], info_dict['id'], thumb_display_id))
|
||||
self.to_screen(f'[info] Downloading {thumb_display_id} ...')
|
||||
try:
|
||||
uf = self.urlopen(t['url'])
|
||||
self.to_screen(f'[info] Writing {thumb_display_id} to: {thumb_filename}')
|
||||
with open(encodeFilename(thumb_filename), 'wb') as thumbf:
|
||||
shutil.copyfileobj(uf, thumbf)
|
||||
ret.append(suffix + thumb_ext)
|
||||
self.to_screen('[%s] %s: Writing thumbnail %sto: %s' %
|
||||
(info_dict['extractor'], info_dict['id'], thumb_display_id, thumb_filename))
|
||||
ret.append((thumb_filename, thumb_filename_final))
|
||||
t['filepath'] = thumb_filename
|
||||
except network_exceptions as err:
|
||||
self.report_warning('Unable to download thumbnail "%s": %s' %
|
||||
(t['url'], error_to_compat_str(err)))
|
||||
self.report_warning(f'Unable to download {thumb_display_id}: {err}')
|
||||
if ret and not write_all:
|
||||
break
|
||||
return ret
|
||||
|
||||
+9
-7
@@ -302,11 +302,14 @@ def _real_main(argv=None):
|
||||
parser.error('invalid %s %r: %s' % (msg, tmpl, error_to_compat_str(err)))
|
||||
|
||||
for k, tmpl in opts.outtmpl.items():
|
||||
validate_outtmpl(tmpl, '%s output template' % k)
|
||||
validate_outtmpl(tmpl, f'{k} output template')
|
||||
opts.forceprint = opts.forceprint or []
|
||||
for tmpl in opts.forceprint or []:
|
||||
validate_outtmpl(tmpl, 'print template')
|
||||
validate_outtmpl(opts.sponsorblock_chapter_title, 'SponsorBlock chapter title')
|
||||
for k, tmpl in opts.progress_template.items():
|
||||
k = f'{k[:-6]} console title' if '-title' in k else f'{k} progress'
|
||||
validate_outtmpl(tmpl, f'{k} template')
|
||||
|
||||
if opts.extractaudio and not opts.keepvideo and opts.format is None:
|
||||
opts.format = 'bestaudio/best'
|
||||
@@ -418,7 +421,7 @@ def _real_main(argv=None):
|
||||
opts.sponskrub = False
|
||||
|
||||
# PostProcessors
|
||||
postprocessors = []
|
||||
postprocessors = list(opts.add_postprocessors)
|
||||
if sponsorblock_query:
|
||||
postprocessors.append({
|
||||
'key': 'SponsorBlock',
|
||||
@@ -513,6 +516,7 @@ def _real_main(argv=None):
|
||||
'add_chapters': opts.addchapters,
|
||||
'add_metadata': opts.addmetadata,
|
||||
})
|
||||
# Note: Deprecated
|
||||
# This should be above EmbedThumbnail since sponskrub removes the thumbnail attachment
|
||||
# but must be below EmbedSubtitle and FFmpegMetadata
|
||||
# See https://github.com/yt-dlp/yt-dlp/issues/204 , https://github.com/faissaloo/SponSkrub/issues/29
|
||||
@@ -535,6 +539,7 @@ def _real_main(argv=None):
|
||||
})
|
||||
if not already_have_thumbnail:
|
||||
opts.writethumbnail = True
|
||||
opts.outtmpl['pl_thumbnail'] = ''
|
||||
if opts.split_chapters:
|
||||
postprocessors.append({
|
||||
'key': 'FFmpegSplitChapters',
|
||||
@@ -631,8 +636,9 @@ def _real_main(argv=None):
|
||||
'noresizebuffer': opts.noresizebuffer,
|
||||
'http_chunk_size': opts.http_chunk_size,
|
||||
'continuedl': opts.continue_dl,
|
||||
'noprogress': opts.noprogress,
|
||||
'noprogress': opts.quiet if opts.noprogress is None else opts.noprogress,
|
||||
'progress_with_newline': opts.progress_with_newline,
|
||||
'progress_template': opts.progress_template,
|
||||
'playliststart': opts.playliststart,
|
||||
'playlistend': opts.playlistend,
|
||||
'playlistreverse': opts.playlist_reverse,
|
||||
@@ -729,10 +735,6 @@ def _real_main(argv=None):
|
||||
'geo_bypass_ip_block': opts.geo_bypass_ip_block,
|
||||
'warnings': warnings,
|
||||
'compat_opts': compat_opts,
|
||||
# just for deprecation check
|
||||
'autonumber': opts.autonumber or None,
|
||||
'usetitle': opts.usetitle or None,
|
||||
'useid': opts.useid or None,
|
||||
}
|
||||
|
||||
with YoutubeDL(ydl_opts) as ydl:
|
||||
|
||||
+13
-39
@@ -178,7 +178,7 @@ def aes_encrypt(data, expanded_key):
|
||||
data = sub_bytes(data)
|
||||
data = shift_rows(data)
|
||||
if i != rounds:
|
||||
data = mix_columns(data)
|
||||
data = list(iter_mix_columns(data, MIX_COLUMN_MATRIX))
|
||||
data = xor(data, expanded_key[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES])
|
||||
|
||||
return data
|
||||
@@ -197,7 +197,7 @@ def aes_decrypt(data, expanded_key):
|
||||
for i in range(rounds, 0, -1):
|
||||
data = xor(data, expanded_key[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES])
|
||||
if i != rounds:
|
||||
data = mix_columns_inv(data)
|
||||
data = list(iter_mix_columns(data, MIX_COLUMN_MATRIX_INV))
|
||||
data = shift_rows_inv(data)
|
||||
data = sub_bytes_inv(data)
|
||||
data = xor(data, expanded_key[:BLOCK_SIZE_BYTES])
|
||||
@@ -375,49 +375,23 @@ def xor(data1, data2):
|
||||
return [x ^ y for x, y in zip(data1, data2)]
|
||||
|
||||
|
||||
def rijndael_mul(a, b):
|
||||
if a == 0 or b == 0:
|
||||
return 0
|
||||
return RIJNDAEL_EXP_TABLE[(RIJNDAEL_LOG_TABLE[a] + RIJNDAEL_LOG_TABLE[b]) % 0xFF]
|
||||
|
||||
|
||||
def mix_column(data, matrix):
|
||||
data_mixed = []
|
||||
for row in range(4):
|
||||
mixed = 0
|
||||
for column in range(4):
|
||||
# xor is (+) and (-)
|
||||
mixed ^= rijndael_mul(data[column], matrix[row][column])
|
||||
data_mixed.append(mixed)
|
||||
return data_mixed
|
||||
|
||||
|
||||
def mix_columns(data, matrix=MIX_COLUMN_MATRIX):
|
||||
data_mixed = []
|
||||
for i in range(4):
|
||||
column = data[i * 4: (i + 1) * 4]
|
||||
data_mixed += mix_column(column, matrix)
|
||||
return data_mixed
|
||||
|
||||
|
||||
def mix_columns_inv(data):
|
||||
return mix_columns(data, MIX_COLUMN_MATRIX_INV)
|
||||
def iter_mix_columns(data, matrix):
|
||||
for i in (0, 4, 8, 12):
|
||||
for row in matrix:
|
||||
mixed = 0
|
||||
for j in range(4):
|
||||
# xor is (+) and (-)
|
||||
mixed ^= (0 if data[i:i + 4][j] == 0 or row[j] == 0 else
|
||||
RIJNDAEL_EXP_TABLE[(RIJNDAEL_LOG_TABLE[data[i + j]] + RIJNDAEL_LOG_TABLE[row[j]]) % 0xFF])
|
||||
yield mixed
|
||||
|
||||
|
||||
def shift_rows(data):
|
||||
data_shifted = []
|
||||
for column in range(4):
|
||||
for row in range(4):
|
||||
data_shifted.append(data[((column + row) & 0b11) * 4 + row])
|
||||
return data_shifted
|
||||
return [data[((column + row) & 0b11) * 4 + row] for column in range(4) for row in range(4)]
|
||||
|
||||
|
||||
def shift_rows_inv(data):
|
||||
data_shifted = []
|
||||
for column in range(4):
|
||||
for row in range(4):
|
||||
data_shifted.append(data[((column - row) & 0b11) * 4 + row])
|
||||
return data_shifted
|
||||
return [data[((column - row) & 0b11) * 4 + row] for column in range(4) for row in range(4)]
|
||||
|
||||
|
||||
def shift_block(data):
|
||||
|
||||
@@ -50,6 +50,7 @@ class Cache(object):
|
||||
except OSError as ose:
|
||||
if ose.errno != errno.EEXIST:
|
||||
raise
|
||||
self._ydl.write_debug(f'Saving {section}.{key} to cache')
|
||||
write_json_file(data, fn)
|
||||
except Exception:
|
||||
tb = traceback.format_exc()
|
||||
@@ -66,6 +67,7 @@ class Cache(object):
|
||||
try:
|
||||
try:
|
||||
with io.open(cache_fn, 'r', encoding='utf-8') as cachef:
|
||||
self._ydl.write_debug(f'Loading {section}.{key} from cache')
|
||||
return json.load(cachef)
|
||||
except ValueError:
|
||||
try:
|
||||
|
||||
@@ -33,6 +33,8 @@ class compat_HTMLParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# compat_ctypes_WINFUNCTYPE = ctypes.WINFUNCTYPE
|
||||
# will not work since ctypes.WINFUNCTYPE does not exist in UNIX machines
|
||||
def compat_ctypes_WINFUNCTYPE(*args, **kwargs):
|
||||
return ctypes.WINFUNCTYPE(*args, **kwargs)
|
||||
|
||||
@@ -157,6 +159,12 @@ except ImportError:
|
||||
compat_pycrypto_AES = None
|
||||
|
||||
|
||||
def windows_enable_vt_mode(): # TODO: Do this the proper way https://bugs.python.org/issue30075
|
||||
if compat_os_name != 'nt':
|
||||
return
|
||||
os.system('')
|
||||
|
||||
|
||||
# Deprecated
|
||||
|
||||
compat_basestring = str
|
||||
@@ -279,5 +287,6 @@ __all__ = [
|
||||
'compat_xml_parse_error',
|
||||
'compat_xpath',
|
||||
'compat_zip',
|
||||
'windows_enable_vt_mode',
|
||||
'workaround_optparse_bug9161',
|
||||
]
|
||||
|
||||
+12
-7
@@ -353,7 +353,7 @@ class LinuxChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||
class MacChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||
def __init__(self, browser_keyring_name, logger):
|
||||
self._logger = logger
|
||||
password = _get_mac_keyring_password(browser_keyring_name)
|
||||
password = _get_mac_keyring_password(browser_keyring_name, logger)
|
||||
self._v10_key = None if password is None else self.derive_key(password)
|
||||
|
||||
@staticmethod
|
||||
@@ -546,7 +546,7 @@ def _parse_safari_cookies_record(data, jar, logger):
|
||||
p.skip_to(value_offset)
|
||||
value = p.read_cstring()
|
||||
except UnicodeDecodeError:
|
||||
logger.warning('failed to parse cookie because UTF-8 decoding failed', only_once=True)
|
||||
logger.warning('failed to parse Safari cookie because UTF-8 decoding failed', only_once=True)
|
||||
return record_size
|
||||
|
||||
p.skip_to(record_size, 'space at the end of the record')
|
||||
@@ -592,11 +592,13 @@ def _get_linux_keyring_password(browser_keyring_name):
|
||||
return password.encode('utf-8')
|
||||
|
||||
|
||||
def _get_mac_keyring_password(browser_keyring_name):
|
||||
def _get_mac_keyring_password(browser_keyring_name, logger):
|
||||
if KEYRING_AVAILABLE:
|
||||
logger.debug('using keyring to obtain password')
|
||||
password = keyring.get_password('{} Safe Storage'.format(browser_keyring_name), browser_keyring_name)
|
||||
return password.encode('utf-8')
|
||||
else:
|
||||
logger.debug('using find-generic-password to obtain password')
|
||||
proc = subprocess.Popen(['security', 'find-generic-password',
|
||||
'-w', # write password to stdout
|
||||
'-a', browser_keyring_name, # match 'account'
|
||||
@@ -605,8 +607,11 @@ def _get_mac_keyring_password(browser_keyring_name):
|
||||
stderr=subprocess.DEVNULL)
|
||||
try:
|
||||
stdout, stderr = process_communicate_or_kill(proc)
|
||||
if stdout[-1:] == b'\n':
|
||||
stdout = stdout[:-1]
|
||||
return stdout
|
||||
except BaseException:
|
||||
except BaseException as e:
|
||||
logger.warning(f'exception running find-generic-password: {type(e).__name__}({e})')
|
||||
return None
|
||||
|
||||
|
||||
@@ -640,7 +645,7 @@ def _decrypt_aes_cbc(ciphertext, key, logger, initialization_vector=b' ' * 16):
|
||||
try:
|
||||
return plaintext[:-padding_length].decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
logger.warning('failed to decrypt cookie because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
|
||||
logger.warning('failed to decrypt cookie (AES-CBC) because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
|
||||
return None
|
||||
|
||||
|
||||
@@ -648,13 +653,13 @@ def _decrypt_aes_gcm(ciphertext, key, nonce, authentication_tag, logger):
|
||||
try:
|
||||
plaintext = aes_gcm_decrypt_and_verify_bytes(ciphertext, key, authentication_tag, nonce)
|
||||
except ValueError:
|
||||
logger.warning('failed to decrypt cookie because the MAC check failed. Possibly the key is wrong?', only_once=True)
|
||||
logger.warning('failed to decrypt cookie (AES-GCM) because the MAC check failed. Possibly the key is wrong?', only_once=True)
|
||||
return None
|
||||
|
||||
try:
|
||||
return plaintext.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
logger.warning('failed to decrypt cookie because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
|
||||
logger.warning('failed to decrypt cookie (AES-GCM) because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
+38
-46
@@ -7,7 +7,6 @@ import sys
|
||||
import time
|
||||
import random
|
||||
|
||||
from ..compat import compat_os_name
|
||||
from ..utils import (
|
||||
decodeArgument,
|
||||
encodeFilename,
|
||||
@@ -17,6 +16,7 @@ from ..utils import (
|
||||
timeconvert,
|
||||
)
|
||||
from ..minicurses import (
|
||||
MultilineLogger,
|
||||
MultilinePrinter,
|
||||
QuietMultilinePrinter,
|
||||
BreaklineStatusPrinter
|
||||
@@ -44,8 +44,6 @@ class FileDownloader(object):
|
||||
noresizebuffer: Do not automatically resize the download buffer.
|
||||
continuedl: Try to continue downloads if possible.
|
||||
noprogress: Do not print the progress bar.
|
||||
logtostderr: Log messages to stderr instead of stdout.
|
||||
consoletitle: Display progress in console window's titlebar.
|
||||
nopart: Do not use temporary .part files.
|
||||
updatetime: Use the Last-modified header to set output file timestamps.
|
||||
test: Download only first bytes to test the downloader.
|
||||
@@ -61,6 +59,7 @@ class FileDownloader(object):
|
||||
http_chunk_size: Size of a chunk for chunk-based HTTP downloading. May be
|
||||
useful for bypassing bandwidth throttling imposed by
|
||||
a webserver (experimental)
|
||||
progress_template: See YoutubeDL.py
|
||||
|
||||
Subclasses of this one must re-define the real_download method.
|
||||
"""
|
||||
@@ -73,7 +72,7 @@ class FileDownloader(object):
|
||||
self.ydl = ydl
|
||||
self._progress_hooks = []
|
||||
self.params = params
|
||||
self._multiline = None
|
||||
self._prepare_multiline_status()
|
||||
self.add_progress_hook(self.report_progress)
|
||||
|
||||
@staticmethod
|
||||
@@ -242,55 +241,46 @@ class FileDownloader(object):
|
||||
"""Report destination filename."""
|
||||
self.to_screen('[download] Destination: ' + filename)
|
||||
|
||||
def _prepare_multiline_status(self, lines):
|
||||
if self.params.get('quiet'):
|
||||
def _prepare_multiline_status(self, lines=1):
|
||||
if self.params.get('noprogress'):
|
||||
self._multiline = QuietMultilinePrinter()
|
||||
elif self.params.get('progress_with_newline', False):
|
||||
elif self.ydl.params.get('logger'):
|
||||
self._multiline = MultilineLogger(self.ydl.params['logger'], lines)
|
||||
elif self.params.get('progress_with_newline'):
|
||||
self._multiline = BreaklineStatusPrinter(sys.stderr, lines)
|
||||
elif self.params.get('noprogress', False):
|
||||
self._multiline = None
|
||||
else:
|
||||
self._multiline = MultilinePrinter(sys.stderr, lines)
|
||||
self._multiline = MultilinePrinter(sys.stderr, lines, not self.params.get('quiet'))
|
||||
|
||||
def _finish_multiline_status(self):
|
||||
if self._multiline is not None:
|
||||
self._multiline.end()
|
||||
self._multiline.end()
|
||||
|
||||
def _report_progress_status(self, msg, is_last_line=False, progress_line=None):
|
||||
fullmsg = '[download] ' + msg
|
||||
if self.params.get('progress_with_newline', False):
|
||||
self.to_screen(fullmsg)
|
||||
elif progress_line is not None and self._multiline is not None:
|
||||
self._multiline.print_at_line(fullmsg, progress_line)
|
||||
else:
|
||||
if compat_os_name == 'nt' or not sys.stderr.isatty():
|
||||
prev_len = getattr(self, '_report_progress_prev_line_length', 0)
|
||||
if prev_len > len(fullmsg):
|
||||
fullmsg += ' ' * (prev_len - len(fullmsg))
|
||||
self._report_progress_prev_line_length = len(fullmsg)
|
||||
clear_line = '\r'
|
||||
else:
|
||||
clear_line = '\r\x1b[K'
|
||||
self.to_screen(clear_line + fullmsg, skip_eol=not is_last_line)
|
||||
self.to_console_title('yt-dlp ' + msg)
|
||||
def _report_progress_status(self, s):
|
||||
progress_dict = s.copy()
|
||||
progress_dict.pop('info_dict')
|
||||
progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
|
||||
|
||||
progress_template = self.params.get('progress_template', {})
|
||||
self._multiline.print_at_line(self.ydl.evaluate_outtmpl(
|
||||
progress_template.get('download') or '[download] %(progress._default_template)s',
|
||||
progress_dict), s.get('progress_idx') or 0)
|
||||
self.to_console_title(self.ydl.evaluate_outtmpl(
|
||||
progress_template.get('download-title') or 'yt-dlp %(progress._default_template)s',
|
||||
progress_dict))
|
||||
|
||||
def report_progress(self, s):
|
||||
if s['status'] == 'finished':
|
||||
if self.params.get('noprogress', False):
|
||||
if self.params.get('noprogress'):
|
||||
self.to_screen('[download] Download completed')
|
||||
else:
|
||||
msg_template = '100%%'
|
||||
if s.get('total_bytes') is not None:
|
||||
s['_total_bytes_str'] = format_bytes(s['total_bytes'])
|
||||
msg_template += ' of %(_total_bytes_str)s'
|
||||
if s.get('elapsed') is not None:
|
||||
s['_elapsed_str'] = self.format_seconds(s['elapsed'])
|
||||
msg_template += ' in %(_elapsed_str)s'
|
||||
self._report_progress_status(
|
||||
msg_template % s, is_last_line=True, progress_line=s.get('progress_idx'))
|
||||
return
|
||||
|
||||
if self.params.get('noprogress'):
|
||||
msg_template = '100%%'
|
||||
if s.get('total_bytes') is not None:
|
||||
s['_total_bytes_str'] = format_bytes(s['total_bytes'])
|
||||
msg_template += ' of %(_total_bytes_str)s'
|
||||
if s.get('elapsed') is not None:
|
||||
s['_elapsed_str'] = self.format_seconds(s['elapsed'])
|
||||
msg_template += ' in %(_elapsed_str)s'
|
||||
s['_percent_str'] = self.format_percent(100)
|
||||
s['_default_template'] = msg_template % s
|
||||
self._report_progress_status(s)
|
||||
return
|
||||
|
||||
if s['status'] != 'downloading':
|
||||
@@ -332,8 +322,8 @@ class FileDownloader(object):
|
||||
msg_template = '%(_downloaded_bytes_str)s at %(_speed_str)s'
|
||||
else:
|
||||
msg_template = '%(_percent_str)s % at %(_speed_str)s ETA %(_eta_str)s'
|
||||
|
||||
self._report_progress_status(msg_template % s, progress_line=s.get('progress_idx'))
|
||||
s['_default_template'] = msg_template % s
|
||||
self._report_progress_status(s)
|
||||
|
||||
def report_resuming_byte(self, resume_len):
|
||||
"""Report attempt to resume at given byte."""
|
||||
@@ -405,7 +395,9 @@ class FileDownloader(object):
|
||||
'[download] Sleeping %s seconds ...' % (
|
||||
sleep_interval_sub))
|
||||
time.sleep(sleep_interval_sub)
|
||||
return self.real_download(filename, info_dict), True
|
||||
ret = self.real_download(filename, info_dict)
|
||||
self._finish_multiline_status()
|
||||
return ret, True
|
||||
|
||||
def real_download(self, filename, info_dict):
|
||||
"""Real download process. Redefine in subclasses."""
|
||||
|
||||
@@ -355,7 +355,8 @@ class FragmentFD(FileDownloader):
|
||||
# not what it decrypts to.
|
||||
if self.params.get('test', False):
|
||||
return frag_content
|
||||
return aes_cbc_decrypt_bytes(frag_content, decrypt_info['KEY'], iv)
|
||||
decrypted_data = aes_cbc_decrypt_bytes(frag_content, decrypt_info['KEY'], iv)
|
||||
return decrypted_data[:-decrypted_data[-1]]
|
||||
|
||||
return decrypt_fragment
|
||||
|
||||
@@ -392,9 +393,7 @@ class FragmentFD(FileDownloader):
|
||||
result = result and job.result()
|
||||
finally:
|
||||
tpe.shutdown(wait=True)
|
||||
|
||||
self._finish_multiline_status()
|
||||
return True
|
||||
return result
|
||||
|
||||
def download_and_append_fragments(self, ctx, fragments, info_dict, *, pack_func=None, finish_func=None, tpe=None):
|
||||
fragment_retries = self.params.get('fragment_retries', 0)
|
||||
|
||||
@@ -9,6 +9,7 @@ from .fragment import FragmentFD
|
||||
from .external import FFmpegFD
|
||||
|
||||
from ..compat import (
|
||||
compat_pycrypto_AES,
|
||||
compat_urlparse,
|
||||
)
|
||||
from ..utils import (
|
||||
@@ -68,14 +69,20 @@ class HlsFD(FragmentFD):
|
||||
man_url = urlh.geturl()
|
||||
s = urlh.read().decode('utf-8', 'ignore')
|
||||
|
||||
if not self.can_download(s, info_dict, self.params.get('allow_unplayable_formats')):
|
||||
if info_dict.get('extra_param_to_segment_url') or info_dict.get('_decryption_key_url'):
|
||||
self.report_error('pycryptodome not found. Please install')
|
||||
return False
|
||||
can_download, message = self.can_download(s, info_dict, self.params.get('allow_unplayable_formats')), None
|
||||
if can_download and not compat_pycrypto_AES and '#EXT-X-KEY:METHOD=AES-128' in s:
|
||||
if FFmpegFD.available():
|
||||
can_download, message = False, 'The stream has AES-128 encryption and pycryptodomex is not available'
|
||||
else:
|
||||
message = ('The stream has AES-128 encryption and neither ffmpeg nor pycryptodomex are available; '
|
||||
'Decryption will be performed natively, but will be extremely slow')
|
||||
if not can_download:
|
||||
message = message or 'Unsupported features have been detected'
|
||||
fd = FFmpegFD(self.ydl, self.params)
|
||||
self.report_warning(
|
||||
'%s detected unsupported features; extraction will be delegated to %s' % (self.FD_NAME, fd.get_basename()))
|
||||
self.report_warning(f'{message}; extraction will be delegated to {fd.get_basename()}')
|
||||
return fd.real_download(filename, info_dict)
|
||||
elif message:
|
||||
self.report_warning(message)
|
||||
|
||||
is_webvtt = info_dict['ext'] == 'vtt'
|
||||
if is_webvtt:
|
||||
@@ -232,7 +239,6 @@ class HlsFD(FragmentFD):
|
||||
elif line.startswith('#EXT-X-DISCONTINUITY'):
|
||||
discontinuity_count += 1
|
||||
i += 1
|
||||
media_sequence += 1
|
||||
|
||||
# We only download the first fragment during the test
|
||||
if self.params.get('test', False):
|
||||
|
||||
@@ -48,8 +48,9 @@ class HttpFD(FileDownloader):
|
||||
|
||||
is_test = self.params.get('test', False)
|
||||
chunk_size = self._TEST_FILE_SIZE if is_test else (
|
||||
info_dict.get('downloader_options', {}).get('http_chunk_size')
|
||||
or self.params.get('http_chunk_size') or 0)
|
||||
self.params.get('http_chunk_size')
|
||||
or info_dict.get('downloader_options', {}).get('http_chunk_size')
|
||||
or 0)
|
||||
|
||||
ctx.open_mode = 'wb'
|
||||
ctx.resume_len = 0
|
||||
|
||||
@@ -6,7 +6,7 @@ try:
|
||||
from .lazy_extractors import *
|
||||
from .lazy_extractors import _ALL_CLASSES
|
||||
_LAZY_LOADER = True
|
||||
_PLUGIN_CLASSES = []
|
||||
_PLUGIN_CLASSES = {}
|
||||
except ImportError:
|
||||
_LAZY_LOADER = False
|
||||
|
||||
@@ -20,7 +20,7 @@ if not _LAZY_LOADER:
|
||||
_ALL_CLASSES.append(GenericIE)
|
||||
|
||||
_PLUGIN_CLASSES = load_plugins('extractor', 'IE', globals())
|
||||
_ALL_CLASSES = _PLUGIN_CLASSES + _ALL_CLASSES
|
||||
_ALL_CLASSES = list(_PLUGIN_CLASSES.values()) + _ALL_CLASSES
|
||||
|
||||
|
||||
def gen_extractor_classes():
|
||||
|
||||
@@ -37,6 +37,11 @@ MSO_INFO = {
|
||||
'username_field': 'email',
|
||||
'password_field': 'loginpassword',
|
||||
},
|
||||
'RCN': {
|
||||
'name': 'RCN',
|
||||
'username_field': 'UserName',
|
||||
'password_field': 'UserPassword',
|
||||
},
|
||||
'Rogers': {
|
||||
'name': 'Rogers',
|
||||
'username_field': 'UserName',
|
||||
|
||||
@@ -588,8 +588,8 @@ class BBCIE(BBCCoUkIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?bbc\.(?:com|co\.uk)/(?:[^/]+/)+(?P<id>[^/#?]+)'
|
||||
|
||||
_MEDIA_SETS = [
|
||||
'mobile-tablet-main',
|
||||
'pc',
|
||||
'mobile-tablet-main',
|
||||
]
|
||||
|
||||
_TESTS = [{
|
||||
|
||||
@@ -22,6 +22,7 @@ from ..utils import (
|
||||
parse_iso8601,
|
||||
try_get,
|
||||
smuggle_url,
|
||||
srt_subtitles_timecode,
|
||||
str_or_none,
|
||||
str_to_int,
|
||||
strip_jsonp,
|
||||
@@ -623,7 +624,7 @@ class BiliBiliSearchIE(SearchInfoExtractor):
|
||||
while True:
|
||||
pageNumber += 1
|
||||
# FIXME
|
||||
api_url = "https://api.bilibili.com/x/web-interface/search/type?context=&page=%s&order=pubdate&keyword=%s&duration=0&tids_2=&__refresh__=true&search_type=video&tids=0&highlight=1" % (pageNumber, query)
|
||||
api_url = 'https://api.bilibili.com/x/web-interface/search/type?context=&page=%s&order=pubdate&keyword=%s&duration=0&tids_2=&__refresh__=true&search_type=video&tids=0&highlight=1' % (pageNumber, query)
|
||||
json_str = self._download_webpage(
|
||||
api_url, "None", query={"Search_key": query},
|
||||
note='Extracting results from page %s' % pageNumber)
|
||||
@@ -783,6 +784,12 @@ class BiliIntlBaseIE(InfoExtractor):
|
||||
def _call_api(self, type, endpoint, id):
|
||||
return self._download_json(self._API_URL.format(type, endpoint), id)['data']
|
||||
|
||||
def json2srt(self, json):
|
||||
data = '\n\n'.join(
|
||||
f'{i + 1}\n{srt_subtitles_timecode(line["from"])} --> {srt_subtitles_timecode(line["to"])}\n{line["content"]}'
|
||||
for i, line in enumerate(json['body']))
|
||||
return data
|
||||
|
||||
def _get_subtitles(self, type, ep_id):
|
||||
sub_json = self._call_api(type, f'/m/subtitle?ep_id={ep_id}&platform=web', ep_id)
|
||||
subtitles = {}
|
||||
@@ -790,8 +797,12 @@ class BiliIntlBaseIE(InfoExtractor):
|
||||
sub_url = sub.get('url')
|
||||
if not sub_url:
|
||||
continue
|
||||
sub_data = self._download_json(sub_url, ep_id, fatal=False)
|
||||
if not sub_data:
|
||||
continue
|
||||
subtitles.setdefault(sub.get('key', 'en'), []).append({
|
||||
'url': sub_url,
|
||||
'ext': 'srt',
|
||||
'data': self.json2srt(sub_data)
|
||||
})
|
||||
return subtitles
|
||||
|
||||
|
||||
@@ -202,7 +202,6 @@ class CBCGemIE(InfoExtractor):
|
||||
IE_NAME = 'gem.cbc.ca'
|
||||
_VALID_URL = r'https?://gem\.cbc\.ca/media/(?P<id>[0-9a-z-]+/s[0-9]+[a-z][0-9]+)'
|
||||
_TESTS = [{
|
||||
# geo-restricted to Canada, bypassable
|
||||
# This is a normal, public, TV show video
|
||||
'url': 'https://gem.cbc.ca/media/schitts-creek/s06e01',
|
||||
'md5': '93dbb31c74a8e45b378cf13bd3f6f11e',
|
||||
@@ -224,7 +223,6 @@ class CBCGemIE(InfoExtractor):
|
||||
'params': {'format': 'bv'},
|
||||
'skip': 'Geo-restricted to Canada',
|
||||
}, {
|
||||
# geo-restricted to Canada, bypassable
|
||||
# This video requires an account in the browser, but works fine in yt-dlp
|
||||
'url': 'https://gem.cbc.ca/media/schitts-creek/s01e01',
|
||||
'md5': '297a9600f554f2258aed01514226a697',
|
||||
@@ -313,7 +311,6 @@ class CBCGemPlaylistIE(InfoExtractor):
|
||||
IE_NAME = 'gem.cbc.ca:playlist'
|
||||
_VALID_URL = r'https?://gem\.cbc\.ca/media/(?P<id>(?P<show>[0-9a-z-]+)/s(?P<season>[0-9]+))/?(?:[?#]|$)'
|
||||
_TESTS = [{
|
||||
# geo-restricted to Canada, bypassable
|
||||
# TV show playlist, all public videos
|
||||
'url': 'https://gem.cbc.ca/media/schitts-creek/s06',
|
||||
'playlist_count': 16,
|
||||
@@ -322,7 +319,6 @@ class CBCGemPlaylistIE(InfoExtractor):
|
||||
'title': 'Season 6',
|
||||
'description': 'md5:6a92104a56cbeb5818cc47884d4326a2',
|
||||
},
|
||||
'skip': 'Geo-restricted to Canada',
|
||||
}]
|
||||
_API_BASE = 'https://services.radio-canada.ca/ott/cbc-api/v2/shows/'
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
int_or_none,
|
||||
try_get,
|
||||
unified_timestamp,
|
||||
)
|
||||
|
||||
|
||||
class CiscoWebexIE(InfoExtractor):
|
||||
IE_NAME = 'ciscowebex'
|
||||
IE_DESC = 'Cisco Webex'
|
||||
_VALID_URL = r'''(?x)
|
||||
(?P<url>https?://(?P<subdomain>[^/#?]*)\.webex\.com/(?:
|
||||
(?P<siteurl_1>[^/#?]*)/(?:ldr|lsr).php\?(?:[^#]*&)*RCID=(?P<rcid>[0-9a-f]{32})|
|
||||
(?:recordingservice|webappng)/sites/(?P<siteurl_2>[^/#?]*)/recording/(?:playback/|play/)?(?P<id>[0-9a-f]{32})
|
||||
))'''
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://demosubdomain.webex.com/demositeurl/ldr.php?RCID=e58e803bc0f766bb5f6376d2e86adb5b',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'http://demosubdomain.webex.com/demositeurl/lsr.php?RCID=bc04b4a7b5ea2cc3a493d5ae6aaff5d7',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://demosubdomain.webex.com/recordingservice/sites/demositeurl/recording/88e7a42f7b19f5b423c54754aecc2ce9/playback',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = self._match_valid_url(url)
|
||||
rcid = mobj.group('rcid')
|
||||
if rcid:
|
||||
webpage = self._download_webpage(url, None, note='Getting video ID')
|
||||
url = self._search_regex(self._VALID_URL, webpage, 'redirection url', group='url')
|
||||
url = self._request_webpage(url, None, note='Resolving final URL').geturl()
|
||||
mobj = self._match_valid_url(url)
|
||||
subdomain = mobj.group('subdomain')
|
||||
siteurl = mobj.group('siteurl_1') or mobj.group('siteurl_2')
|
||||
video_id = mobj.group('id')
|
||||
|
||||
stream = self._download_json(
|
||||
'https://%s.webex.com/webappng/api/v1/recordings/%s/stream' % (subdomain, video_id),
|
||||
video_id, fatal=False, query={'siteurl': siteurl})
|
||||
if not stream:
|
||||
self.raise_login_required(method='cookies')
|
||||
|
||||
video_id = stream.get('recordUUID') or video_id
|
||||
|
||||
formats = [{
|
||||
'format_id': 'video',
|
||||
'url': stream['fallbackPlaySrc'],
|
||||
'ext': 'mp4',
|
||||
'vcodec': 'avc1.640028',
|
||||
'acodec': 'mp4a.40.2',
|
||||
}]
|
||||
if stream.get('preventDownload') is False:
|
||||
mp4url = try_get(stream, lambda x: x['downloadRecordingInfo']['downloadInfo']['mp4URL'])
|
||||
if mp4url:
|
||||
formats.append({
|
||||
'format_id': 'video',
|
||||
'url': mp4url,
|
||||
'ext': 'mp4',
|
||||
'vcodec': 'avc1.640028',
|
||||
'acodec': 'mp4a.40.2',
|
||||
})
|
||||
audiourl = try_get(stream, lambda x: x['downloadRecordingInfo']['downloadInfo']['audioURL'])
|
||||
if audiourl:
|
||||
formats.append({
|
||||
'format_id': 'audio',
|
||||
'url': audiourl,
|
||||
'ext': 'mp3',
|
||||
'vcodec': 'none',
|
||||
'acodec': 'mp3',
|
||||
})
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': stream['recordName'],
|
||||
'description': stream.get('description'),
|
||||
'uploader': stream.get('ownerDisplayName'),
|
||||
'uploader_id': stream.get('ownerUserName') or stream.get('ownerId'), # mail or id
|
||||
'timestamp': unified_timestamp(stream.get('createTime')),
|
||||
'duration': int_or_none(stream.get('duration'), 1000),
|
||||
'webpage_url': 'https://%s.webex.com/recordingservice/sites/%s/recording/playback/%s' % (subdomain, siteurl, video_id),
|
||||
'formats': formats,
|
||||
}
|
||||
+61
-34
@@ -406,6 +406,10 @@ class InfoExtractor(object):
|
||||
_real_extract() methods and define a _VALID_URL regexp.
|
||||
Probably, they should also be added to the list of extractors.
|
||||
|
||||
Subclasses may also override suitable() if necessary, but ensure the function
|
||||
signature is preserved and that this function imports everything it needs
|
||||
(except other extractors), so that lazy_extractors works correctly
|
||||
|
||||
_GEO_BYPASS attribute may be set to False in order to disable
|
||||
geo restriction bypass mechanisms for a particular extractor.
|
||||
Though it won't disable explicit geo restriction bypass based on
|
||||
@@ -421,7 +425,7 @@ class InfoExtractor(object):
|
||||
will be used by geo restriction bypass mechanism similarly
|
||||
to _GEO_COUNTRIES.
|
||||
|
||||
Finally, the _WORKING attribute should be set to False for broken IEs
|
||||
The _WORKING attribute should be set to False for broken IEs
|
||||
in order to warn the users and skip the tests.
|
||||
"""
|
||||
|
||||
@@ -1130,10 +1134,7 @@ class InfoExtractor(object):
|
||||
if mobj:
|
||||
break
|
||||
|
||||
if not self.get_param('no_color') and compat_os_name != 'nt' and sys.stderr.isatty():
|
||||
_name = '\033[0;34m%s\033[0m' % name
|
||||
else:
|
||||
_name = name
|
||||
_name = self._downloader._color_text(name, 'blue')
|
||||
|
||||
if mobj:
|
||||
if group is None:
|
||||
@@ -1677,7 +1678,7 @@ class InfoExtractor(object):
|
||||
has_multiple_limits = has_limit and has_multiple_fields and not self._get_field_setting(field, 'same_limit')
|
||||
|
||||
fields = self._get_field_setting(field, 'field') if has_multiple_fields else (field,)
|
||||
limits = limit_text.split(":") if has_multiple_limits else (limit_text,) if has_limit else tuple()
|
||||
limits = limit_text.split(':') if has_multiple_limits else (limit_text,) if has_limit else tuple()
|
||||
limit_count = len(limits)
|
||||
for (i, f) in enumerate(fields):
|
||||
add_item(f, reverse, closest,
|
||||
@@ -1761,9 +1762,9 @@ class InfoExtractor(object):
|
||||
if format.get('vbr') is not None and format.get('abr') is not None:
|
||||
format['tbr'] = format.get('vbr', 0) + format.get('abr', 0)
|
||||
else:
|
||||
if format.get('vcodec') != "none" and format.get('vbr') is None:
|
||||
if format.get('vcodec') != 'none' and format.get('vbr') is None:
|
||||
format['vbr'] = format.get('tbr') - format.get('abr', 0)
|
||||
if format.get('acodec') != "none" and format.get('abr') is None:
|
||||
if format.get('acodec') != 'none' and format.get('abr') is None:
|
||||
format['abr'] = format.get('tbr') - format.get('vbr', 0)
|
||||
|
||||
return tuple(self._calculate_field_preference(format, field) for field in self._order)
|
||||
@@ -1965,13 +1966,16 @@ class InfoExtractor(object):
|
||||
'format_note': 'Quality selection URL',
|
||||
}
|
||||
|
||||
def _report_ignoring_subs(self, name):
|
||||
self.report_warning(bug_reports_message(
|
||||
f'Ignoring subtitle tracks found in the {name} manifest; '
|
||||
'if any subtitle tracks are missing,'
|
||||
), only_once=True)
|
||||
|
||||
def _extract_m3u8_formats(self, *args, **kwargs):
|
||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(*args, **kwargs)
|
||||
if subs:
|
||||
self.report_warning(bug_reports_message(
|
||||
"Ignoring subtitle tracks found in the HLS manifest; "
|
||||
"if any subtitle tracks are missing,"
|
||||
), only_once=True)
|
||||
self._report_ignoring_subs('HLS')
|
||||
return fmts
|
||||
|
||||
def _extract_m3u8_formats_and_subtitles(
|
||||
@@ -2219,6 +2223,25 @@ class InfoExtractor(object):
|
||||
last_stream_inf = {}
|
||||
return formats, subtitles
|
||||
|
||||
def _extract_m3u8_vod_duration(
|
||||
self, m3u8_vod_url, video_id, note=None, errnote=None, data=None, headers={}, query={}):
|
||||
|
||||
m3u8_vod = self._download_webpage(
|
||||
m3u8_vod_url, video_id,
|
||||
note='Downloading m3u8 VOD manifest' if note is None else note,
|
||||
errnote='Failed to download VOD manifest' if errnote is None else errnote,
|
||||
fatal=False, data=data, headers=headers, query=query)
|
||||
|
||||
return self._parse_m3u8_vod_duration(m3u8_vod or '', video_id)
|
||||
|
||||
def _parse_m3u8_vod_duration(self, m3u8_vod, video_id):
|
||||
if '#EXT-X-PLAYLIST-TYPE:VOD' not in m3u8_vod:
|
||||
return None
|
||||
|
||||
return int(sum(
|
||||
float(line[len('#EXTINF:'):].split(',')[0])
|
||||
for line in m3u8_vod.splitlines() if line.startswith('#EXTINF:'))) or None
|
||||
|
||||
@staticmethod
|
||||
def _xpath_ns(path, namespace=None):
|
||||
if not namespace:
|
||||
@@ -2250,10 +2273,7 @@ class InfoExtractor(object):
|
||||
def _extract_smil_formats(self, *args, **kwargs):
|
||||
fmts, subs = self._extract_smil_formats_and_subtitles(*args, **kwargs)
|
||||
if subs:
|
||||
self.report_warning(bug_reports_message(
|
||||
"Ignoring subtitle tracks found in the SMIL manifest; "
|
||||
"if any subtitle tracks are missing,"
|
||||
), only_once=True)
|
||||
self._report_ignoring_subs('SMIL')
|
||||
return fmts
|
||||
|
||||
def _extract_smil_info(self, smil_url, video_id, fatal=True, f4m_params=None):
|
||||
@@ -2323,14 +2343,15 @@ class InfoExtractor(object):
|
||||
rtmp_count = 0
|
||||
http_count = 0
|
||||
m3u8_count = 0
|
||||
imgs_count = 0
|
||||
|
||||
srcs = []
|
||||
srcs = set()
|
||||
media = smil.findall(self._xpath_ns('.//video', namespace)) + smil.findall(self._xpath_ns('.//audio', namespace))
|
||||
for medium in media:
|
||||
src = medium.get('src')
|
||||
if not src or src in srcs:
|
||||
continue
|
||||
srcs.append(src)
|
||||
srcs.add(src)
|
||||
|
||||
bitrate = float_or_none(medium.get('system-bitrate') or medium.get('systemBitrate'), 1000)
|
||||
filesize = int_or_none(medium.get('size') or medium.get('fileSize'))
|
||||
@@ -2404,6 +2425,24 @@ class InfoExtractor(object):
|
||||
'height': height,
|
||||
})
|
||||
|
||||
for medium in smil.findall(self._xpath_ns('.//imagestream', namespace)):
|
||||
src = medium.get('src')
|
||||
if not src or src in srcs:
|
||||
continue
|
||||
srcs.add(src)
|
||||
|
||||
imgs_count += 1
|
||||
formats.append({
|
||||
'format_id': 'imagestream-%d' % (imgs_count),
|
||||
'url': src,
|
||||
'ext': mimetype2ext(medium.get('type')),
|
||||
'acodec': 'none',
|
||||
'vcodec': 'none',
|
||||
'width': int_or_none(medium.get('width')),
|
||||
'height': int_or_none(medium.get('height')),
|
||||
'format_note': 'SMIL storyboards',
|
||||
})
|
||||
|
||||
return formats
|
||||
|
||||
def _parse_smil_subtitles(self, smil, namespace=None, subtitles_lang='en'):
|
||||
@@ -2476,10 +2515,7 @@ class InfoExtractor(object):
|
||||
def _extract_mpd_formats(self, *args, **kwargs):
|
||||
fmts, subs = self._extract_mpd_formats_and_subtitles(*args, **kwargs)
|
||||
if subs:
|
||||
self.report_warning(bug_reports_message(
|
||||
"Ignoring subtitle tracks found in the DASH manifest; "
|
||||
"if any subtitle tracks are missing,"
|
||||
), only_once=True)
|
||||
self._report_ignoring_subs('DASH')
|
||||
return fmts
|
||||
|
||||
def _extract_mpd_formats_and_subtitles(
|
||||
@@ -2503,10 +2539,7 @@ class InfoExtractor(object):
|
||||
def _parse_mpd_formats(self, *args, **kwargs):
|
||||
fmts, subs = self._parse_mpd_formats_and_subtitles(*args, **kwargs)
|
||||
if subs:
|
||||
self.report_warning(bug_reports_message(
|
||||
"Ignoring subtitle tracks found in the DASH manifest; "
|
||||
"if any subtitle tracks are missing,"
|
||||
), only_once=True)
|
||||
self._report_ignoring_subs('DASH')
|
||||
return fmts
|
||||
|
||||
def _parse_mpd_formats_and_subtitles(
|
||||
@@ -2834,10 +2867,7 @@ class InfoExtractor(object):
|
||||
def _extract_ism_formats(self, *args, **kwargs):
|
||||
fmts, subs = self._extract_ism_formats_and_subtitles(*args, **kwargs)
|
||||
if subs:
|
||||
self.report_warning(bug_reports_message(
|
||||
"Ignoring subtitle tracks found in the ISM manifest; "
|
||||
"if any subtitle tracks are missing,"
|
||||
))
|
||||
self._report_ignoring_subs('ISM')
|
||||
return fmts
|
||||
|
||||
def _extract_ism_formats_and_subtitles(self, ism_url, video_id, ism_id=None, note=None, errnote=None, fatal=True, data=None, headers={}, query={}):
|
||||
@@ -3097,10 +3127,7 @@ class InfoExtractor(object):
|
||||
def _extract_akamai_formats(self, *args, **kwargs):
|
||||
fmts, subs = self._extract_akamai_formats_and_subtitles(*args, **kwargs)
|
||||
if subs:
|
||||
self.report_warning(bug_reports_message(
|
||||
"Ignoring subtitle tracks found in the manifests; "
|
||||
"if any subtitle tracks are missing,"
|
||||
))
|
||||
self._report_ignoring_subs('akamai')
|
||||
return fmts
|
||||
|
||||
def _extract_akamai_formats_and_subtitles(self, manifest_url, video_id, hosts={}):
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
# coding: utf-8
|
||||
|
||||
from ..utils import (
|
||||
int_or_none,
|
||||
traverse_obj,
|
||||
url_or_none,
|
||||
)
|
||||
from .common import (
|
||||
InfoExtractor,
|
||||
compat_urllib_parse_unquote,
|
||||
)
|
||||
|
||||
|
||||
class DouyinIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?douyin\.com/video/(?P<id>[0-9]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.douyin.com/video/6961737553342991651',
|
||||
'md5': '10523312c8b8100f353620ac9dc8f067',
|
||||
'info_dict': {
|
||||
'id': '6961737553342991651',
|
||||
'ext': 'mp4',
|
||||
'title': '#杨超越 小小水手带你去远航❤️',
|
||||
'uploader': '杨超越',
|
||||
'upload_date': '20210513',
|
||||
'timestamp': 1620905839,
|
||||
'uploader_id': '110403406559',
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'repost_count': int,
|
||||
'comment_count': int,
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.douyin.com/video/6982497745948921092',
|
||||
'md5': 'd78408c984b9b5102904cf6b6bc2d712',
|
||||
'info_dict': {
|
||||
'id': '6982497745948921092',
|
||||
'ext': 'mp4',
|
||||
'title': '这个夏日和小羊@杨超越 一起遇见白色幻想',
|
||||
'uploader': '杨超越工作室',
|
||||
'upload_date': '20210708',
|
||||
'timestamp': 1625739481,
|
||||
'uploader_id': '408654318141572',
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'repost_count': int,
|
||||
'comment_count': int,
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.douyin.com/video/6953975910773099811',
|
||||
'md5': '72e882e24f75064c218b76c8b713c185',
|
||||
'info_dict': {
|
||||
'id': '6953975910773099811',
|
||||
'ext': 'mp4',
|
||||
'title': '#一起看海 出现在你的夏日里',
|
||||
'uploader': '杨超越',
|
||||
'upload_date': '20210422',
|
||||
'timestamp': 1619098692,
|
||||
'uploader_id': '110403406559',
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'repost_count': int,
|
||||
'comment_count': int,
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.douyin.com/video/6950251282489675042',
|
||||
'md5': 'b4db86aec367ef810ddd38b1737d2fed',
|
||||
'info_dict': {
|
||||
'id': '6950251282489675042',
|
||||
'ext': 'mp4',
|
||||
'title': '哈哈哈,成功了哈哈哈哈哈哈',
|
||||
'uploader': '杨超越',
|
||||
'upload_date': '20210412',
|
||||
'timestamp': 1618231483,
|
||||
'uploader_id': '110403406559',
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'repost_count': int,
|
||||
'comment_count': int,
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.douyin.com/video/6963263655114722595',
|
||||
'md5': '1abe1c477d05ee62efb40bf2329957cf',
|
||||
'info_dict': {
|
||||
'id': '6963263655114722595',
|
||||
'ext': 'mp4',
|
||||
'title': '#哪个爱豆的105度最甜 换个角度看看我哈哈',
|
||||
'uploader': '杨超越',
|
||||
'upload_date': '20210517',
|
||||
'timestamp': 1621261163,
|
||||
'uploader_id': '110403406559',
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'repost_count': int,
|
||||
'comment_count': int,
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
render_data = self._parse_json(
|
||||
self._search_regex(
|
||||
r'<script [^>]*\bid=[\'"]RENDER_DATA[\'"][^>]*>(%7B.+%7D)</script>',
|
||||
webpage, 'render data'),
|
||||
video_id, transform_source=compat_urllib_parse_unquote)
|
||||
details = traverse_obj(render_data, (..., 'aweme', 'detail'), get_all=False)
|
||||
|
||||
thumbnails = [{'url': self._proto_relative_url(url)} for url in traverse_obj(
|
||||
details, ('video', ('cover', 'dynamicCover', 'originCover')), expected_type=url_or_none, default=[])]
|
||||
|
||||
common = {
|
||||
'width': traverse_obj(details, ('video', 'width'), expected_type=int),
|
||||
'height': traverse_obj(details, ('video', 'height'), expected_type=int),
|
||||
'ext': 'mp4',
|
||||
}
|
||||
formats = [{**common, 'url': self._proto_relative_url(url)} for url in traverse_obj(
|
||||
details, ('video', 'playAddr', ..., 'src'), expected_type=url_or_none, default=[]) if url]
|
||||
self._remove_duplicate_formats(formats)
|
||||
|
||||
download_url = traverse_obj(details, ('download', 'url'), expected_type=url_or_none)
|
||||
if download_url:
|
||||
formats.append({
|
||||
**common,
|
||||
'format_id': 'download',
|
||||
'url': self._proto_relative_url(download_url),
|
||||
'quality': 1,
|
||||
})
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': details.get('desc') or self._html_search_meta('title', webpage),
|
||||
'formats': formats,
|
||||
'thumbnails': thumbnails,
|
||||
'uploader': traverse_obj(details, ('authorInfo', 'nickname'), expected_type=str),
|
||||
'uploader_id': traverse_obj(details, ('authorInfo', 'uid'), expected_type=str),
|
||||
'uploader_url': 'https://www.douyin.com/user/%s' % traverse_obj(
|
||||
details, ('authorInfo', 'secUid'), expected_type=str),
|
||||
'timestamp': int_or_none(details.get('createTime')),
|
||||
'duration': traverse_obj(details, ('video', 'duration'), expected_type=int),
|
||||
'view_count': traverse_obj(details, ('stats', 'playCount'), expected_type=int),
|
||||
'like_count': traverse_obj(details, ('stats', 'diggCount'), expected_type=int),
|
||||
'repost_count': traverse_obj(details, ('stats', 'shareCount'), expected_type=int),
|
||||
'comment_count': traverse_obj(details, ('stats', 'commentCount'), expected_type=int),
|
||||
}
|
||||
@@ -258,6 +258,7 @@ from .ciscolive import (
|
||||
CiscoLiveSessionIE,
|
||||
CiscoLiveSearchIE,
|
||||
)
|
||||
from .ciscowebex import CiscoWebexIE
|
||||
from .cjsw import CJSWIE
|
||||
from .cliphunter import CliphunterIE
|
||||
from .clippit import ClippitIE
|
||||
@@ -340,7 +341,6 @@ from .discoveryplusindia import (
|
||||
DiscoveryPlusIndiaShowIE,
|
||||
)
|
||||
from .dotsub import DotsubIE
|
||||
from .douyin import DouyinIE
|
||||
from .douyutv import (
|
||||
DouyuShowIE,
|
||||
DouyuTVIE,
|
||||
@@ -507,6 +507,7 @@ from .gazeta import GazetaIE
|
||||
from .gdcvault import GDCVaultIE
|
||||
from .gedidigital import GediDigitalIE
|
||||
from .generic import GenericIE
|
||||
from .gettr import GettrIE
|
||||
from .gfycat import GfycatIE
|
||||
from .giantbomb import GiantBombIE
|
||||
from .giga import GigaIE
|
||||
@@ -524,6 +525,7 @@ from .googlepodcasts import (
|
||||
GooglePodcastsFeedIE,
|
||||
)
|
||||
from .googlesearch import GoogleSearchIE
|
||||
from .gopro import GoProIE
|
||||
from .goshgay import GoshgayIE
|
||||
from .gotostage import GoToStageIE
|
||||
from .gputechconf import GPUTechConfIE
|
||||
@@ -833,6 +835,7 @@ from .myvi import (
|
||||
)
|
||||
from .myvideoge import MyVideoGeIE
|
||||
from .myvidster import MyVidsterIE
|
||||
from .n1 import N1InfoIIE, N1InfoAssetIE
|
||||
from .nationalgeographic import (
|
||||
NationalGeographicVideoIE,
|
||||
NationalGeographicTVIE,
|
||||
@@ -1204,7 +1207,10 @@ from .rtve import RTVEALaCartaIE, RTVELiveIE, RTVEInfantilIE, RTVELiveIE, RTVETe
|
||||
from .rtvnh import RTVNHIE
|
||||
from .rtvs import RTVSIE
|
||||
from .ruhd import RUHDIE
|
||||
from .rumble import RumbleEmbedIE
|
||||
from .rumble import (
|
||||
RumbleEmbedIE,
|
||||
RumbleChannelIE,
|
||||
)
|
||||
from .rutube import (
|
||||
RutubeIE,
|
||||
RutubeChannelIE,
|
||||
@@ -1428,7 +1434,10 @@ from .theplatform import (
|
||||
from .thescene import TheSceneIE
|
||||
from .thestar import TheStarIE
|
||||
from .thesun import TheSunIE
|
||||
from .theta import ThetaIE
|
||||
from .theta import (
|
||||
ThetaVideoIE,
|
||||
ThetaStreamIE,
|
||||
)
|
||||
from .theweatherchannel import TheWeatherChannelIE
|
||||
from .thisamericanlife import ThisAmericanLifeIE
|
||||
from .thisav import ThisAVIE
|
||||
@@ -1437,6 +1446,7 @@ from .threeqsdn import ThreeQSDNIE
|
||||
from .tiktok import (
|
||||
TikTokIE,
|
||||
TikTokUserIE,
|
||||
DouyinIE,
|
||||
)
|
||||
from .tinypic import TinyPicIE
|
||||
from .tmz import TMZIE
|
||||
@@ -1600,6 +1610,7 @@ from .utreon import UtreonIE
|
||||
from .varzesh3 import Varzesh3IE
|
||||
from .vbox7 import Vbox7IE
|
||||
from .veehd import VeeHDIE
|
||||
from .veo import VeoIE
|
||||
from .veoh import VeohIE
|
||||
from .vesti import VestiIE
|
||||
from .vevo import (
|
||||
@@ -1634,11 +1645,6 @@ from .vidio import (
|
||||
VidioLiveIE
|
||||
)
|
||||
from .vidlii import VidLiiIE
|
||||
from .vidme import (
|
||||
VidmeIE,
|
||||
VidmeUserIE,
|
||||
VidmeUserLikesIE,
|
||||
)
|
||||
from .vier import VierIE, VierVideosIE
|
||||
from .viewlift import (
|
||||
ViewLiftIE,
|
||||
@@ -1709,6 +1715,7 @@ from .vtm import VTMIE
|
||||
from .medialaan import MedialaanIE
|
||||
from .vube import VubeIE
|
||||
from .vuclip import VuClipIE
|
||||
from .vupload import VuploadIE
|
||||
from .vvvvid import (
|
||||
VVVVIDIE,
|
||||
VVVVIDShowIE,
|
||||
|
||||
@@ -2,25 +2,61 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_HTTPError
|
||||
from ..utils import (
|
||||
determine_ext,
|
||||
dict_get,
|
||||
int_or_none,
|
||||
js_to_json,
|
||||
orderedSet,
|
||||
qualities,
|
||||
str_or_none,
|
||||
traverse_obj,
|
||||
try_get,
|
||||
urlencode_postdata,
|
||||
ExtractorError,
|
||||
)
|
||||
|
||||
|
||||
class FunimationPageIE(InfoExtractor):
|
||||
class FunimationBaseIE(InfoExtractor):
|
||||
_NETRC_MACHINE = 'funimation'
|
||||
_REGION = None
|
||||
_TOKEN = None
|
||||
|
||||
def _get_region(self):
|
||||
region_cookie = self._get_cookies('https://www.funimation.com').get('region')
|
||||
region = region_cookie.value if region_cookie else self.get_param('geo_bypass_country')
|
||||
return region or traverse_obj(
|
||||
self._download_json(
|
||||
'https://geo-service.prd.funimationsvc.com/geo/v1/region/check', None, fatal=False,
|
||||
note='Checking geo-location', errnote='Unable to fetch geo-location information'),
|
||||
'region') or 'US'
|
||||
|
||||
def _login(self):
|
||||
username, password = self._get_login_info()
|
||||
if username is None:
|
||||
return
|
||||
try:
|
||||
data = self._download_json(
|
||||
'https://prod-api-funimationnow.dadcdigital.com/api/auth/login/',
|
||||
None, 'Logging in', data=urlencode_postdata({
|
||||
'username': username,
|
||||
'password': password,
|
||||
}))
|
||||
return data['token']
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
|
||||
error = self._parse_json(e.cause.read().decode(), None)['error']
|
||||
raise ExtractorError(error, expected=True)
|
||||
raise
|
||||
|
||||
|
||||
class FunimationPageIE(FunimationBaseIE):
|
||||
IE_NAME = 'funimation:page'
|
||||
_VALID_URL = r'(?P<origin>https?://(?:www\.)?funimation(?:\.com|now\.uk))/(?P<lang>[^/]+/)?(?P<path>shows/(?P<id>[^/]+/[^/?#&]+).*$)'
|
||||
_VALID_URL = r'https?://(?:www\.)?funimation(?:\.com|now\.uk)/(?:(?P<lang>[^/]+)/)?(?:shows|v)/(?P<show>[^/]+)/(?P<episode>[^/?#&]+)'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://www.funimation.com/shows/attack-on-titan-junior-high/broadcast-dub-preview/',
|
||||
@@ -45,38 +81,34 @@ class FunimationPageIE(InfoExtractor):
|
||||
}, {
|
||||
'url': 'https://www.funimationnow.uk/shows/puzzle-dragons-x/drop-impact/simulcast/',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://www.funimation.com/v/a-certain-scientific-railgun/super-powered-level-5',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_initialize(self):
|
||||
if not self._REGION:
|
||||
FunimationBaseIE._REGION = self._get_region()
|
||||
if not self._TOKEN:
|
||||
FunimationBaseIE._TOKEN = self._login()
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = self._match_valid_url(url)
|
||||
display_id = mobj.group('id').replace('/', '_')
|
||||
if not mobj.group('lang'):
|
||||
url = '%s/en/%s' % (mobj.group('origin'), mobj.group('path'))
|
||||
locale, show, episode = self._match_valid_url(url).group('lang', 'show', 'episode')
|
||||
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
title_data = self._parse_json(self._search_regex(
|
||||
r'TITLE_DATA\s*=\s*({[^}]+})',
|
||||
webpage, 'title data', default=''),
|
||||
display_id, js_to_json, fatal=False) or {}
|
||||
video_id = traverse_obj(self._download_json(
|
||||
f'https://title-api.prd.funimationsvc.com/v1/shows/{show}/episodes/{episode}',
|
||||
f'{show}_{episode}', query={
|
||||
'deviceType': 'web',
|
||||
'region': self._REGION,
|
||||
'locale': locale or 'en'
|
||||
}), ('videoList', ..., 'id'), get_all=False)
|
||||
|
||||
video_id = (
|
||||
title_data.get('id')
|
||||
or self._search_regex(
|
||||
(r"KANE_customdimensions.videoID\s*=\s*'(\d+)';", r'<iframe[^>]+src="/player/(\d+)'),
|
||||
webpage, 'video_id', default=None)
|
||||
or self._search_regex(
|
||||
r'/player/(\d+)',
|
||||
self._html_search_meta(['al:web:url', 'og:video:url', 'og:video:secure_url'], webpage, fatal=True),
|
||||
'video id'))
|
||||
return self.url_result(f'https://www.funimation.com/player/{video_id}', FunimationIE.ie_key(), video_id)
|
||||
|
||||
|
||||
class FunimationIE(InfoExtractor):
|
||||
class FunimationIE(FunimationBaseIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?funimation\.com/player/(?P<id>\d+)'
|
||||
|
||||
_NETRC_MACHINE = 'funimation'
|
||||
_TOKEN = None
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://www.funimation.com/player/210051',
|
||||
'info_dict': {
|
||||
@@ -92,7 +124,7 @@ class FunimationIE(InfoExtractor):
|
||||
'season_number': 99,
|
||||
'series': 'Attack on Titan: Junior High',
|
||||
'description': '',
|
||||
'duration': 154,
|
||||
'duration': 155,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': 'm3u8',
|
||||
@@ -113,7 +145,7 @@ class FunimationIE(InfoExtractor):
|
||||
'season_number': 99,
|
||||
'series': 'Attack on Titan: Junior High',
|
||||
'description': '',
|
||||
'duration': 154,
|
||||
'duration': 155,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': 'm3u8',
|
||||
@@ -121,26 +153,9 @@ class FunimationIE(InfoExtractor):
|
||||
},
|
||||
}]
|
||||
|
||||
def _login(self):
|
||||
username, password = self._get_login_info()
|
||||
if username is None:
|
||||
return
|
||||
try:
|
||||
data = self._download_json(
|
||||
'https://prod-api-funimationnow.dadcdigital.com/api/auth/login/',
|
||||
None, 'Logging in', data=urlencode_postdata({
|
||||
'username': username,
|
||||
'password': password,
|
||||
}))
|
||||
self._TOKEN = data['token']
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
|
||||
error = self._parse_json(e.cause.read().decode(), None)['error']
|
||||
raise ExtractorError(error, expected=True)
|
||||
raise
|
||||
|
||||
def _real_initialize(self):
|
||||
self._login()
|
||||
if not self._TOKEN:
|
||||
FunimationBaseIE._TOKEN = self._login()
|
||||
|
||||
@staticmethod
|
||||
def _get_experiences(episode):
|
||||
@@ -180,6 +195,8 @@ class FunimationIE(InfoExtractor):
|
||||
|
||||
formats, subtitles, thumbnails, duration = [], {}, [], 0
|
||||
requested_languages, requested_versions = self._configuration_arg('language'), self._configuration_arg('version')
|
||||
language_preference = qualities((requested_languages or [''])[::-1])
|
||||
source_preference = qualities((requested_versions or ['uncut', 'simulcast'])[::-1])
|
||||
only_initial_experience = 'seperate-video-versions' in self.get_param('compat_opts', [])
|
||||
|
||||
for lang, version, fmt in self._get_experiences(episode):
|
||||
@@ -227,10 +244,15 @@ class FunimationIE(InfoExtractor):
|
||||
})
|
||||
for f in current_formats:
|
||||
# TODO: Convert language to code
|
||||
f.update({'language': lang, 'format_note': version})
|
||||
f.update({
|
||||
'language': lang,
|
||||
'format_note': version,
|
||||
'source_preference': source_preference(version.lower()),
|
||||
'language_preference': language_preference(lang.lower()),
|
||||
})
|
||||
formats.extend(current_formats)
|
||||
self._remove_duplicate_formats(formats)
|
||||
self._sort_formats(formats)
|
||||
self._sort_formats(formats, ('lang', 'source'))
|
||||
|
||||
return {
|
||||
'id': initial_experience_id if only_initial_experience else episode_id,
|
||||
@@ -275,7 +297,7 @@ class FunimationIE(InfoExtractor):
|
||||
return subtitles
|
||||
|
||||
|
||||
class FunimationShowIE(FunimationIE):
|
||||
class FunimationShowIE(FunimationBaseIE):
|
||||
IE_NAME = 'funimation:show'
|
||||
_VALID_URL = r'(?P<url>https?://(?:www\.)?funimation(?:\.com|now\.uk)/(?P<locale>[^/]+)?/?shows/(?P<id>[^/?#&]+))/?(?:[?#]|$)'
|
||||
|
||||
@@ -303,31 +325,28 @@ class FunimationShowIE(FunimationIE):
|
||||
}]
|
||||
|
||||
def _real_initialize(self):
|
||||
region = self._get_cookies('https://www.funimation.com').get('region')
|
||||
self._region = region.value if region else try_get(
|
||||
self._download_json(
|
||||
'https://geo-service.prd.funimationsvc.com/geo/v1/region/check', None, fatal=False,
|
||||
note='Checking geo-location', errnote='Unable to fetch geo-location information'),
|
||||
lambda x: x['region']) or 'US'
|
||||
if not self._REGION:
|
||||
FunimationBaseIE._REGION = self._get_region()
|
||||
|
||||
def _real_extract(self, url):
|
||||
base_url, locale, display_id = self._match_valid_url(url).groups()
|
||||
|
||||
show_info = self._download_json(
|
||||
'https://title-api.prd.funimationsvc.com/v2/shows/%s?region=%s&deviceType=web&locale=%s'
|
||||
% (display_id, self._region, locale or 'en'), display_id)
|
||||
items = self._download_json(
|
||||
% (display_id, self._REGION, locale or 'en'), display_id)
|
||||
items_info = self._download_json(
|
||||
'https://prod-api-funimationnow.dadcdigital.com/api/funimation/episodes/?limit=99999&title_id=%s'
|
||||
% show_info.get('id'), display_id).get('items')
|
||||
vod_items = map(lambda k: dict_get(k, ('mostRecentSvod', 'mostRecentAvod')).get('item'), items)
|
||||
% show_info.get('id'), display_id)
|
||||
|
||||
vod_items = traverse_obj(items_info, ('items', ..., re.compile('(?i)mostRecent[AS]vod').match, 'item'))
|
||||
|
||||
return {
|
||||
'_type': 'playlist',
|
||||
'id': show_info['id'],
|
||||
'title': show_info['name'],
|
||||
'entries': [
|
||||
'entries': orderedSet(
|
||||
self.url_result(
|
||||
'%s/%s' % (base_url, vod_item.get('episodeSlug')), FunimationPageIE.ie_key(),
|
||||
vod_item.get('episodeId'), vod_item.get('episodeName'))
|
||||
for vod_item in sorted(vod_items, key=lambda x: x.get('episodeOrder'))],
|
||||
for vod_item in sorted(vod_items, key=lambda x: x.get('episodeOrder', -1))),
|
||||
}
|
||||
|
||||
@@ -2755,12 +2755,6 @@ class GenericIE(InfoExtractor):
|
||||
if vhx_url:
|
||||
return self.url_result(vhx_url, VHXEmbedIE.ie_key())
|
||||
|
||||
vid_me_embed_url = self._search_regex(
|
||||
r'src=[\'"](https?://vid\.me/[^\'"]+)[\'"]',
|
||||
webpage, 'vid.me embed', default=None)
|
||||
if vid_me_embed_url is not None:
|
||||
return self.url_result(vid_me_embed_url, 'Vidme')
|
||||
|
||||
# Invidious Instances
|
||||
# https://github.com/yt-dlp/yt-dlp/issues/195
|
||||
# https://github.com/iv-org/invidious/pull/1730
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
dict_get,
|
||||
float_or_none,
|
||||
int_or_none,
|
||||
remove_end,
|
||||
str_or_none,
|
||||
try_get,
|
||||
url_or_none,
|
||||
urljoin,
|
||||
)
|
||||
|
||||
|
||||
class GettrIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(www\.)?gettr\.com/post/(?P<id>[a-z0-9]+)'
|
||||
_MEDIA_BASE_URL = 'https://media.gettr.com/'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://www.gettr.com/post/pcf6uv838f',
|
||||
'info_dict': {
|
||||
'id': 'pcf6uv838f',
|
||||
'title': 'md5:9086a646bbd06c41c4fe8e52b3c93454',
|
||||
'description': 'md5:be0577f1e4caadc06de4a002da2bf287',
|
||||
'ext': 'mp4',
|
||||
'uploader': 'EpochTV',
|
||||
'uploader_id': 'epochtv',
|
||||
'thumbnail': r're:^https?://.+/out\.jpg',
|
||||
'timestamp': 1632782451058,
|
||||
'duration': 58.5585,
|
||||
}
|
||||
}, {
|
||||
'url': 'https://gettr.com/post/p4iahp',
|
||||
'info_dict': {
|
||||
'id': 'p4iahp',
|
||||
'title': 'md5:b03c07883db6fbc1aab88877a6c3b149',
|
||||
'description': 'md5:741b7419d991c403196ed2ea7749a39d',
|
||||
'ext': 'mp4',
|
||||
'uploader': 'Neues Forum Freiheit',
|
||||
'uploader_id': 'nf_freiheit',
|
||||
'thumbnail': r're:^https?://.+/out\.jpg',
|
||||
'timestamp': 1626594455017,
|
||||
'duration': 23,
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
post_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, post_id)
|
||||
|
||||
api_data = self._download_json(
|
||||
'https://api.gettr.com/u/post/%s?incl="poststats|userinfo"' % post_id, post_id)
|
||||
|
||||
post_data = try_get(api_data, lambda x: x['result']['data'])
|
||||
user_data = try_get(api_data, lambda x: x['result']['aux']['uinf'][post_data['uid']]) or {}
|
||||
|
||||
if post_data.get('nfound'):
|
||||
raise ExtractorError(post_data.get('txt'), expected=True)
|
||||
|
||||
title = description = str_or_none(
|
||||
post_data.get('txt') or self._og_search_description(webpage))
|
||||
|
||||
uploader = str_or_none(
|
||||
user_data.get('nickname')
|
||||
or remove_end(self._og_search_title(webpage), ' on GETTR'))
|
||||
if uploader:
|
||||
title = '%s - %s' % (uploader, title)
|
||||
|
||||
if not dict_get(post_data, ['vid', 'ovid']):
|
||||
raise ExtractorError('There\'s no video in this post.')
|
||||
|
||||
vid = post_data.get('vid')
|
||||
ovid = post_data.get('ovid')
|
||||
|
||||
formats = self._extract_m3u8_formats(
|
||||
urljoin(self._MEDIA_BASE_URL, vid), post_id, 'mp4',
|
||||
entry_protocol='m3u8_native', m3u8_id='hls') if vid else []
|
||||
|
||||
if ovid:
|
||||
formats.append({
|
||||
'url': urljoin(self._MEDIA_BASE_URL, ovid),
|
||||
'format_id': 'ovid',
|
||||
'ext': 'mp4',
|
||||
'width': int_or_none(post_data.get('vid_wid')),
|
||||
'height': int_or_none(post_data.get('vid_hgt')),
|
||||
'source_preference': 1,
|
||||
'quality': 1,
|
||||
})
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': post_id,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'thumbnail': url_or_none(
|
||||
urljoin(self._MEDIA_BASE_URL, post_data.get('main'))
|
||||
or self._og_search_thumbnail(webpage)),
|
||||
'timestamp': int_or_none(post_data.get('cdate')),
|
||||
'uploader_id': str_or_none(
|
||||
dict_get(user_data, ['_id', 'username'])
|
||||
or post_data.get('uid')),
|
||||
'uploader': uploader,
|
||||
'formats': formats,
|
||||
'duration': float_or_none(post_data.get('vid_dur')),
|
||||
'tags': post_data.get('htgs'),
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
int_or_none,
|
||||
remove_end,
|
||||
str_or_none,
|
||||
try_get,
|
||||
unified_timestamp,
|
||||
url_or_none,
|
||||
)
|
||||
|
||||
|
||||
class GoProIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(www\.)?gopro\.com/v/(?P<id>[A-Za-z0-9]+)'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://gopro.com/v/ZNVvED8QDzR5V',
|
||||
'info_dict': {
|
||||
'id': 'ZNVvED8QDzR5V',
|
||||
'title': 'My GoPro Adventure - 9/19/21',
|
||||
'thumbnail': r're:https?://.+',
|
||||
'ext': 'mp4',
|
||||
'timestamp': 1632072947,
|
||||
'upload_date': '20210919',
|
||||
'uploader_id': 'fireydive30018',
|
||||
'duration': 396062,
|
||||
}
|
||||
}, {
|
||||
'url': 'https://gopro.com/v/KRm6Vgp2peg4e',
|
||||
'info_dict': {
|
||||
'id': 'KRm6Vgp2peg4e',
|
||||
'title': 'じゃがいも カリカリ オーブン焼き',
|
||||
'thumbnail': r're:https?://.+',
|
||||
'ext': 'mp4',
|
||||
'timestamp': 1607231125,
|
||||
'upload_date': '20201206',
|
||||
'uploader_id': 'dc9bcb8b-47d2-47c6-afbc-4c48f9a3769e',
|
||||
'duration': 45187,
|
||||
'track': 'The Sky Machine',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://gopro.com/v/kVrK9wlJvBMwn',
|
||||
'info_dict': {
|
||||
'id': 'kVrK9wlJvBMwn',
|
||||
'title': 'DARKNESS',
|
||||
'thumbnail': r're:https?://.+',
|
||||
'ext': 'mp4',
|
||||
'timestamp': 1594183735,
|
||||
'upload_date': '20200708',
|
||||
'uploader_id': '闇夜乃皇帝',
|
||||
'duration': 313075,
|
||||
'track': 'Battery (Live)',
|
||||
'artist': 'Metallica',
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
metadata = self._parse_json(
|
||||
self._html_search_regex(r'window\.__reflectData\s*=\s*([^;]+)', webpage, 'metadata'), video_id)
|
||||
|
||||
video_info = metadata['collectionMedia'][0]
|
||||
media_data = self._download_json(
|
||||
'https://api.gopro.com/media/%s/download' % video_info['id'], video_id)
|
||||
|
||||
formats = []
|
||||
for fmt in try_get(media_data, lambda x: x['_embedded']['variations']) or []:
|
||||
format_url = url_or_none(fmt.get('url'))
|
||||
if not format_url:
|
||||
continue
|
||||
formats.append({
|
||||
'url': format_url,
|
||||
'format_id': str_or_none(fmt.get('quality')),
|
||||
'format_note': str_or_none(fmt.get('label')),
|
||||
'ext': str_or_none(fmt.get('type')),
|
||||
'width': int_or_none(fmt.get('width')),
|
||||
'height': int_or_none(fmt.get('height')),
|
||||
})
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
title = str_or_none(
|
||||
try_get(metadata, lambda x: x['collection']['title'])
|
||||
or self._html_search_meta(['og:title', 'twitter:title'], webpage)
|
||||
or remove_end(self._html_search_regex(
|
||||
r'<title[^>]*>([^<]+)</title>', webpage, 'title', fatal=False), ' | GoPro'))
|
||||
if title:
|
||||
title = title.replace('\n', ' ')
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'formats': formats,
|
||||
'thumbnail': url_or_none(
|
||||
self._html_search_meta(['og:image', 'twitter:image'], webpage)),
|
||||
'timestamp': unified_timestamp(
|
||||
try_get(metadata, lambda x: x['collection']['created_at'])),
|
||||
'uploader_id': str_or_none(
|
||||
try_get(metadata, lambda x: x['account']['nickname'])),
|
||||
'duration': int_or_none(
|
||||
video_info.get('source_duration')),
|
||||
'artist': str_or_none(
|
||||
video_info.get('music_track_artist')),
|
||||
'track': str_or_none(
|
||||
video_info.get('music_track_name')),
|
||||
}
|
||||
+52
-53
@@ -1,6 +1,4 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
@@ -14,7 +12,7 @@ from ..utils import (
|
||||
|
||||
|
||||
class HiDiveIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?hidive\.com/stream/(?P<title>[^/]+)/(?P<key>[^/?#&]+)'
|
||||
_VALID_URL = r'https?://(?:www\.)?hidive\.com/stream/(?P<id>(?P<title>[^/]+)/(?P<key>[^/?#&]+))'
|
||||
# Using X-Forwarded-For results in 403 HTTP error for HLS fragments,
|
||||
# so disabling geo bypass completely
|
||||
_GEO_BYPASS = False
|
||||
@@ -54,69 +52,70 @@ class HiDiveIE(InfoExtractor):
|
||||
self._download_webpage(
|
||||
self._LOGIN_URL, None, 'Logging in', data=urlencode_postdata(data))
|
||||
|
||||
def _call_api(self, video_id, title, key, data={}, **kwargs):
|
||||
data = {
|
||||
**data,
|
||||
'Title': title,
|
||||
'Key': key,
|
||||
'PlayerId': 'f4f895ce1ca713ba263b91caeb1daa2d08904783',
|
||||
}
|
||||
return self._download_json(
|
||||
'https://www.hidive.com/play/settings', video_id,
|
||||
data=urlencode_postdata(data), **kwargs) or {}
|
||||
|
||||
def _extract_subtitles_from_rendition(self, rendition, subtitles, parsed_urls):
|
||||
for cc_file in rendition.get('ccFiles', []):
|
||||
cc_url = url_or_none(try_get(cc_file, lambda x: x[2]))
|
||||
# name is used since we cant distinguish subs with same language code
|
||||
cc_lang = try_get(cc_file, (lambda x: x[1].replace(' ', '-').lower(), lambda x: x[0]), str)
|
||||
if cc_url not in parsed_urls and cc_lang:
|
||||
parsed_urls.add(cc_url)
|
||||
subtitles.setdefault(cc_lang, []).append({'url': cc_url})
|
||||
|
||||
def _get_subtitles(self, url, video_id, title, key, subtitles, parsed_urls):
|
||||
webpage = self._download_webpage(url, video_id, fatal=False) or ''
|
||||
for caption in set(re.findall(r'data-captions=\"([^\"]+)\"', webpage)):
|
||||
renditions = self._call_api(
|
||||
video_id, title, key, {'Captions': caption}, fatal=False,
|
||||
note=f'Downloading {caption} subtitle information').get('renditions') or {}
|
||||
for rendition_id, rendition in renditions.items():
|
||||
self._extract_subtitles_from_rendition(rendition, subtitles, parsed_urls)
|
||||
return subtitles
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = self._match_valid_url(url)
|
||||
title, key = mobj.group('title', 'key')
|
||||
video_id = '%s/%s' % (title, key)
|
||||
webpage = self._download_webpage(url, video_id, fatal=False)
|
||||
data_videos = re.findall(r'data-video=\"([^\"]+)\"\s?data-captions=\"([^\"]+)\"', webpage)
|
||||
formats = []
|
||||
subtitles = {}
|
||||
for data_video in data_videos:
|
||||
_, _, _, version, audio, _, extra = data_video[0].split('_')
|
||||
caption = data_video[1]
|
||||
video_id, title, key = self._match_valid_url(url).group('id', 'title', 'key')
|
||||
settings = self._call_api(video_id, title, key)
|
||||
|
||||
settings = self._download_json(
|
||||
'https://www.hidive.com/play/settings', video_id,
|
||||
data=urlencode_postdata({
|
||||
'Title': title,
|
||||
'Key': key,
|
||||
'PlayerId': 'f4f895ce1ca713ba263b91caeb1daa2d08904783',
|
||||
'Version': version,
|
||||
'Audio': audio,
|
||||
'Captions': caption,
|
||||
'Extra': extra,
|
||||
}))
|
||||
restriction = settings.get('restrictionReason')
|
||||
if restriction == 'RegionRestricted':
|
||||
self.raise_geo_restricted()
|
||||
if restriction and restriction != 'None':
|
||||
raise ExtractorError(
|
||||
'%s said: %s' % (self.IE_NAME, restriction), expected=True)
|
||||
|
||||
restriction = settings.get('restrictionReason')
|
||||
if restriction == 'RegionRestricted':
|
||||
self.raise_geo_restricted()
|
||||
|
||||
if restriction and restriction != 'None':
|
||||
raise ExtractorError(
|
||||
'%s said: %s' % (self.IE_NAME, restriction), expected=True)
|
||||
|
||||
for rendition_id, rendition in settings['renditions'].items():
|
||||
m3u8_url = url_or_none(try_get(rendition, lambda x: x['bitrates']['hls']))
|
||||
if not m3u8_url:
|
||||
continue
|
||||
formats, parsed_urls = [], {}, {None}
|
||||
for rendition_id, rendition in settings['renditions'].items():
|
||||
audio, version, extra = rendition_id.split('_')
|
||||
m3u8_url = url_or_none(try_get(rendition, lambda x: x['bitrates']['hls']))
|
||||
if m3u8_url not in parsed_urls:
|
||||
parsed_urls.add(m3u8_url)
|
||||
frmt = self._extract_m3u8_formats(
|
||||
m3u8_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||
m3u8_id='%s-%s-%s-%s' % (version, audio, extra, caption), fatal=False)
|
||||
m3u8_url, video_id, 'mp4', entry_protocol='m3u8_native', m3u8_id=rendition_id, fatal=False)
|
||||
for f in frmt:
|
||||
f['language'] = audio
|
||||
f['format_note'] = f'{version}, {extra}'
|
||||
formats.extend(frmt)
|
||||
|
||||
for cc_file in rendition.get('ccFiles', []):
|
||||
cc_url = url_or_none(try_get(cc_file, lambda x: x[2]))
|
||||
# name is used since we cant distinguish subs with same language code
|
||||
cc_lang = try_get(cc_file, (lambda x: x[1].replace(' ', '-').lower(), lambda x: x[0]), str)
|
||||
if cc_url and cc_lang:
|
||||
subtitles.setdefault(cc_lang, []).append({'url': cc_url})
|
||||
self._sort_formats(formats)
|
||||
|
||||
season_number = int_or_none(self._search_regex(
|
||||
r's(\d+)', key, 'season number', default=None))
|
||||
episode_number = int_or_none(self._search_regex(
|
||||
r'e(\d+)', key, 'episode number', default=None))
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': video_id,
|
||||
'subtitles': subtitles,
|
||||
'subtitles': self.extract_subtitles(url, video_id, title, key, parsed_urls),
|
||||
'formats': formats,
|
||||
'series': title,
|
||||
'season_number': season_number,
|
||||
'episode_number': episode_number,
|
||||
'season_number': int_or_none(
|
||||
self._search_regex(r's(\d+)', key, 'season number', default=None)),
|
||||
'episode_number': int_or_none(
|
||||
self._search_regex(r'e(\d+)', key, 'episode number', default=None)),
|
||||
'http_headers': {'Referer': url}
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ class HotStarPlaylistIE(HotStarBaseIE):
|
||||
|
||||
class HotStarSeriesIE(HotStarBaseIE):
|
||||
IE_NAME = 'hotstar:series'
|
||||
_VALID_URL = r'(?:https?://)(?:www\.)?hotstar\.com(?:/in)?/tv/[^/]+/(?P<id>\d+)'
|
||||
_VALID_URL = r'(?P<url>(?:https?://)(?:www\.)?hotstar\.com(?:/in)?/tv/[^/]+/(?P<id>\d+))'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.hotstar.com/in/tv/radhakrishn/1260000646',
|
||||
'info_dict': {
|
||||
@@ -312,7 +312,7 @@ class HotStarSeriesIE(HotStarBaseIE):
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
series_id = self._match_id(url)
|
||||
url, series_id = self._match_valid_url(url).groups()
|
||||
headers = {
|
||||
'x-country-code': 'IN',
|
||||
'x-platform-code': 'PCTV',
|
||||
@@ -324,7 +324,7 @@ class HotStarSeriesIE(HotStarBaseIE):
|
||||
video_id=series_id, headers=headers)
|
||||
entries = [
|
||||
self.url_result(
|
||||
'hotstar:episode:%d' % video['contentId'],
|
||||
'%s/ignoreme/%d' % (url, video['contentId']),
|
||||
ie=HotStarIE.ie_key(), video_id=video['contentId'])
|
||||
for video in item_json['body']['results']['items']
|
||||
if video.get('contentId')]
|
||||
|
||||
@@ -141,7 +141,7 @@ class IviIE(InfoExtractor):
|
||||
elif site == 353:
|
||||
continue
|
||||
elif not pycryptodome_found:
|
||||
raise ExtractorError('pycryptodome not found. Please install', expected=True)
|
||||
raise ExtractorError('pycryptodomex not found. Please install', expected=True)
|
||||
elif message:
|
||||
extractor_msg += ': ' + message
|
||||
raise ExtractorError(extractor_msg % video_id, expected=True)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from itertools import zip_longest
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
@@ -8,6 +9,8 @@ from ..utils import (
|
||||
ExtractorError,
|
||||
float_or_none,
|
||||
int_or_none,
|
||||
srt_subtitles_timecode,
|
||||
try_get,
|
||||
urlencode_postdata,
|
||||
urljoin,
|
||||
)
|
||||
@@ -86,6 +89,16 @@ class LinkedInLearningIE(LinkedInLearningBaseIE):
|
||||
},
|
||||
}
|
||||
|
||||
def json2srt(self, transcript_lines, duration=None):
|
||||
srt_data = ''
|
||||
for line, (line_dict, next_dict) in enumerate(zip_longest(transcript_lines, transcript_lines[1:])):
|
||||
start_time, caption = line_dict['transcriptStartAt'] / 1000, line_dict['caption']
|
||||
end_time = next_dict['transcriptStartAt'] / 1000 if next_dict else duration or start_time + 1
|
||||
srt_data += '%d\n%s --> %s\n%s\n\n' % (line + 1, srt_subtitles_timecode(start_time),
|
||||
srt_subtitles_timecode(end_time),
|
||||
caption)
|
||||
return srt_data
|
||||
|
||||
def _real_extract(self, url):
|
||||
course_slug, video_slug = self._match_valid_url(url).groups()
|
||||
|
||||
@@ -101,6 +114,7 @@ class LinkedInLearningIE(LinkedInLearningBaseIE):
|
||||
formats.append({
|
||||
'format_id': 'progressive-%dp' % height,
|
||||
'url': progressive_url,
|
||||
'ext': 'mp4',
|
||||
'height': height,
|
||||
'width': width,
|
||||
'source_preference': 1,
|
||||
@@ -128,6 +142,14 @@ class LinkedInLearningIE(LinkedInLearningBaseIE):
|
||||
# However, unless someone can confirm this, the old
|
||||
# behaviour is being kept as-is
|
||||
self._sort_formats(formats, ('res', 'source_preference'))
|
||||
subtitles = {}
|
||||
duration = int_or_none(video_data.get('durationInSeconds'))
|
||||
transcript_lines = try_get(video_data, lambda x: x['transcript']['lines'], expected_type=list)
|
||||
if transcript_lines:
|
||||
subtitles['en'] = [{
|
||||
'ext': 'srt',
|
||||
'data': self.json2srt(transcript_lines, duration)
|
||||
}]
|
||||
|
||||
return {
|
||||
'id': self._get_video_id(video_data, course_slug, video_slug),
|
||||
@@ -135,7 +157,8 @@ class LinkedInLearningIE(LinkedInLearningBaseIE):
|
||||
'formats': formats,
|
||||
'thumbnail': video_data.get('defaultThumbnail'),
|
||||
'timestamp': float_or_none(video_data.get('publishedOn'), 1000),
|
||||
'duration': int_or_none(video_data.get('durationInSeconds')),
|
||||
'duration': duration,
|
||||
'subtitles': subtitles,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from .common import InfoExtractor
|
||||
|
||||
|
||||
class MediaiteIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?mediaite.com/(?:tv|sports|politics|podcasts|opinion)/[\w-]+/'
|
||||
_VALID_URL = r'https?://(?:www\.)?mediaite.com(?!/category)(?:/[\w-]+){2}'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.mediaite.com/sports/bill-burr-roasts-nfl-for-promoting-black-lives-matter-while-scheduling-more-games-after-all-the-sht-they-know-about-cte/',
|
||||
'info_dict': {
|
||||
@@ -71,6 +71,19 @@ class MediaiteIE(InfoExtractor):
|
||||
'upload_date': '20210913',
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, {
|
||||
'url': 'https://www.mediaite.com/news/watch-cnbcs-jim-cramer-says-nobody-wants-to-die-getting-infected-by-unvaccinated-coworker-even-for-22-an-hour/',
|
||||
'info_dict': {
|
||||
'id': 'nwpt1elX',
|
||||
'ext': 'mp4',
|
||||
'title': "CNBC's Jim Cramer Says Nobody Wants to Die Getting Infected by Unvaccinated Coworker 'Even for $22 an Hour'.mp4",
|
||||
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
|
||||
'thumbnail': 'https://cdn.jwplayer.com/v2/media/nwpt1elX/poster.jpg?width=720',
|
||||
'duration': 60,
|
||||
'timestamp': 1633014214,
|
||||
'upload_date': '20210930',
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
|
||||
@@ -37,7 +37,7 @@ class MinotoIE(InfoExtractor):
|
||||
'filesize': int_or_none(fmt.get('filesize')),
|
||||
'width': int_or_none(fmt.get('width')),
|
||||
'height': int_or_none(fmt.get('height')),
|
||||
'codecs': parse_codecs(fmt.get('codecs')),
|
||||
**parse_codecs(fmt.get('codecs')),
|
||||
})
|
||||
self._sort_formats(formats)
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .youtube import YoutubeIE
|
||||
from .reddit import RedditRIE
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
unified_timestamp,
|
||||
extract_attributes,
|
||||
)
|
||||
|
||||
|
||||
class N1InfoAssetIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://best-vod\.umn\.cdn\.united\.cloud/stream\?asset=(?P<id>[^&]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://best-vod.umn.cdn.united.cloud/stream?asset=ljsottomazilirija3060921-n1info-si-worldwide&stream=hp1400&t=0&player=m3u8v&sp=n1info&u=n1info&p=n1Sh4redSecre7iNf0',
|
||||
'md5': '28b08b32aeaff2b8562736ccd5a66fe7',
|
||||
'info_dict': {
|
||||
'id': 'ljsottomazilirija3060921-n1info-si-worldwide',
|
||||
'ext': 'mp4',
|
||||
'title': 'ljsottomazilirija3060921-n1info-si-worldwide',
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
formats = self._extract_m3u8_formats(
|
||||
url, video_id, 'mp4', entry_protocol='m3u8_native', m3u8_id='hls', fatal=False)
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': video_id,
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
|
||||
class N1InfoIIE(InfoExtractor):
|
||||
IE_NAME = 'N1Info:article'
|
||||
_VALID_URL = r'https?://(?:(?:ba|rs|hr)\.)?n1info\.(?:com|si)/(?:[^/]+/){1,2}(?P<id>[^/]+)'
|
||||
_TESTS = [{
|
||||
# Youtube embedded
|
||||
'url': 'https://rs.n1info.com/sport-klub/tenis/kako-je-djokovic-propustio-istorijsku-priliku-video/',
|
||||
'md5': '01ddb6646d0fd9c4c7d990aa77fe1c5a',
|
||||
'info_dict': {
|
||||
'id': 'L5Hd4hQVUpk',
|
||||
'ext': 'mp4',
|
||||
'upload_date': '20210913',
|
||||
'title': 'Ozmo i USO21, ep. 13: Novak Đoković – Danil Medvedev | Ključevi Poraza, Budućnost | SPORT KLUB TENIS',
|
||||
'description': 'md5:467f330af1effedd2e290f10dc31bb8e',
|
||||
'uploader': 'Sport Klub',
|
||||
'uploader_id': 'sportklub',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://rs.n1info.com/vesti/djilas-los-plan-za-metro-nece-resiti-nijedan-saobracajni-problem/',
|
||||
'info_dict': {
|
||||
'id': 'bgmetrosot2409zta20210924174316682-n1info-rs-worldwide',
|
||||
'ext': 'mp4',
|
||||
'title': 'Đilas: Predlog izgradnje metroa besmislen; SNS odbacuje navode',
|
||||
'upload_date': '20210924',
|
||||
'timestamp': 1632481347,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
'url': 'https://n1info.si/novice/slovenija/zadnji-dnevi-na-kopaliscu-ilirija-ilirija-ni-umrla-ubili-so-jo/',
|
||||
'info_dict': {
|
||||
'id': 'ljsottomazilirija3060921-n1info-si-worldwide',
|
||||
'ext': 'mp4',
|
||||
'title': 'Zadnji dnevi na kopališču Ilirija: “Ilirija ni umrla, ubili so jo”',
|
||||
'timestamp': 1632567630,
|
||||
'upload_date': '20210925',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
# Reddit embedded
|
||||
'url': 'https://ba.n1info.com/lifestyle/vucic-bolji-od-tita-ako-izgubi-ja-cu-da-crknem-jugoslavija-je-gotova/',
|
||||
'info_dict': {
|
||||
'id': '2wmfee9eycp71',
|
||||
'ext': 'mp4',
|
||||
'title': '"Ako Vučić izgubi izbore, ja ću da crknem, Jugoslavija je gotova"',
|
||||
'upload_date': '20210924',
|
||||
'timestamp': 1632448649.0,
|
||||
'uploader': 'YouLotWhatDontStop',
|
||||
},
|
||||
'params': {
|
||||
'format': 'bestvideo',
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
'url': 'https://hr.n1info.com/vijesti/pravobraniteljica-o-ubojstvu-u-zagrebu-radi-se-o-doista-nezapamcenoj-situaciji/',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
title = self._html_search_regex(r'<h1[^>]+>(.+?)</h1>', webpage, 'title')
|
||||
timestamp = unified_timestamp(self._html_search_meta('article:published_time', webpage))
|
||||
|
||||
videos = re.findall(r'(?m)(<video[^>]+>)', webpage)
|
||||
entries = []
|
||||
for video in videos:
|
||||
video_data = extract_attributes(video)
|
||||
entries.append({
|
||||
'_type': 'url_transparent',
|
||||
'url': video_data.get('data-url'),
|
||||
'id': video_data.get('id'),
|
||||
'title': title,
|
||||
'thumbnail': video_data.get('data-thumbnail'),
|
||||
'timestamp': timestamp,
|
||||
'ie_key': N1InfoAssetIE.ie_key()})
|
||||
|
||||
embedded_videos = re.findall(r'(<iframe[^>]+>)', webpage)
|
||||
for embedded_video in embedded_videos:
|
||||
video_data = extract_attributes(embedded_video)
|
||||
url = video_data.get('src')
|
||||
if url.startswith('https://www.youtube.com'):
|
||||
entries.append(self.url_result(url, ie=YoutubeIE.ie_key()))
|
||||
elif url.startswith('https://www.redditmedia.com'):
|
||||
entries.append(self.url_result(url, ie=RedditRIE.ie_key()))
|
||||
|
||||
return {
|
||||
'_type': 'playlist',
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'timestamp': timestamp,
|
||||
'entries': entries,
|
||||
}
|
||||
@@ -42,6 +42,7 @@ class NewgroundsIE(InfoExtractor):
|
||||
'timestamp': 955064100,
|
||||
'upload_date': '20000406',
|
||||
'description': 'Scrotum plays "catch."',
|
||||
'age_limit': 17,
|
||||
},
|
||||
}, {
|
||||
# source format unavailable, additional mp4 formats
|
||||
@@ -54,6 +55,7 @@ class NewgroundsIE(InfoExtractor):
|
||||
'timestamp': 1487965140,
|
||||
'upload_date': '20170224',
|
||||
'description': 'ZTV News Episode 8 (February 2017)',
|
||||
'age_limit': 17,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -69,6 +71,7 @@ class NewgroundsIE(InfoExtractor):
|
||||
'timestamp': 1140663240,
|
||||
'upload_date': '20060223',
|
||||
'description': 'Metal Gear is awesome is so is this movie.',
|
||||
'age_limit': 13,
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.newgrounds.com/portal/view/297383/format/flash',
|
||||
@@ -81,8 +84,15 @@ class NewgroundsIE(InfoExtractor):
|
||||
'uploader': 'Egoraptor',
|
||||
'upload_date': '20060223',
|
||||
'timestamp': 1140663240,
|
||||
'age_limit': 13,
|
||||
}
|
||||
}]
|
||||
_AGE_LIMIT = {
|
||||
'e': 0,
|
||||
't': 13,
|
||||
'm': 17,
|
||||
'a': 18,
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
media_id = self._match_id(url)
|
||||
@@ -127,16 +137,20 @@ class NewgroundsIE(InfoExtractor):
|
||||
r'(?:Author|Writer)\s*<a[^>]+>([^<]+)'), webpage, 'uploader',
|
||||
fatal=False)
|
||||
|
||||
age_limit = self._html_search_regex(
|
||||
r'<h2\s*class=["\']rated-([^"\'])["\'][^>]+>', webpage, 'age_limit', default='e')
|
||||
age_limit = self._AGE_LIMIT.get(age_limit)
|
||||
|
||||
timestamp = unified_timestamp(self._html_search_regex(
|
||||
(r'<dt>\s*Uploaded\s*</dt>\s*<dd>([^<]+</dd>\s*<dd>[^<]+)',
|
||||
r'<dt>\s*Uploaded\s*</dt>\s*<dd>([^<]+)'), webpage, 'timestamp',
|
||||
default=None))
|
||||
duration = parse_duration(self._html_search_regex(
|
||||
r'"duration"\s*:\s*["\']?([\d]+)["\']?,', webpage,
|
||||
r'"duration"\s*:\s*["\']?(\d+)["\']?', webpage,
|
||||
'duration', default=None))
|
||||
|
||||
view_count = parse_count(self._html_search_regex(
|
||||
r'(?s)<dt>\s*Views\s*</dt>\s*<dd>([\d\.,]+)</dd>', webpage,
|
||||
r'(?s)<dt>\s*(?:Views|Listens)\s*</dt>\s*<dd>([\d\.,]+)</dd>', webpage,
|
||||
'view count', default=None))
|
||||
|
||||
filesize = int_or_none(self._html_search_regex(
|
||||
@@ -164,6 +178,7 @@ class NewgroundsIE(InfoExtractor):
|
||||
'formats': formats,
|
||||
'thumbnail': self._og_search_thumbnail(webpage),
|
||||
'description': self._og_search_description(webpage),
|
||||
'age_limit': age_limit,
|
||||
'view_count': view_count,
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ class PalcoMP3ArtistIE(PalcoMP3BaseIE):
|
||||
}
|
||||
name'''
|
||||
|
||||
@ classmethod
|
||||
@classmethod
|
||||
def suitable(cls, url):
|
||||
return False if PalcoMP3IE._match_valid_url(url) else super(PalcoMP3ArtistIE, cls).suitable(url)
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
unified_timestamp,
|
||||
try_get,
|
||||
)
|
||||
|
||||
|
||||
class ParliamentLiveUKIE(InfoExtractor):
|
||||
@@ -11,12 +19,14 @@ class ParliamentLiveUKIE(InfoExtractor):
|
||||
_TESTS = [{
|
||||
'url': 'http://parliamentlive.tv/Event/Index/c1e9d44d-fd6c-4263-b50f-97ed26cc998b',
|
||||
'info_dict': {
|
||||
'id': '1_af9nv9ym',
|
||||
'id': 'c1e9d44d-fd6c-4263-b50f-97ed26cc998b',
|
||||
'ext': 'mp4',
|
||||
'title': 'Home Affairs Committee',
|
||||
'uploader_id': 'FFMPEG-01',
|
||||
'timestamp': 1422696664,
|
||||
'upload_date': '20150131',
|
||||
'timestamp': 1395153872,
|
||||
'upload_date': '20140318',
|
||||
},
|
||||
'params': {
|
||||
'format': 'bestvideo',
|
||||
},
|
||||
}, {
|
||||
'url': 'http://parliamentlive.tv/event/index/3f24936f-130f-40bf-9a5d-b3d6479da6a4',
|
||||
@@ -25,19 +35,49 @@ class ParliamentLiveUKIE(InfoExtractor):
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(
|
||||
'http://vodplayer.parliamentlive.tv/?mid=' + video_id, video_id)
|
||||
widget_config = self._parse_json(self._search_regex(
|
||||
r'(?s)kWidgetConfig\s*=\s*({.+});',
|
||||
webpage, 'kaltura widget config'), video_id)
|
||||
kaltura_url = 'kaltura:%s:%s' % (
|
||||
widget_config['wid'][1:], widget_config['entry_id'])
|
||||
event_title = self._download_json(
|
||||
'http://parliamentlive.tv/Event/GetShareVideo/' + video_id, video_id)['event']['title']
|
||||
video_info = self._download_json(f'https://www.parliamentlive.tv/Event/GetShareVideo/{video_id}', video_id)
|
||||
_DEVICE_ID = str(uuid.uuid4())
|
||||
auth = 'Bearer ' + self._download_json(
|
||||
'https://exposure.api.redbee.live/v2/customer/UKParliament/businessunit/ParliamentLive/auth/anonymous',
|
||||
video_id, headers={
|
||||
'Origin': 'https://videoplayback.parliamentlive.tv',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
}, data=json.dumps({
|
||||
'deviceId': _DEVICE_ID,
|
||||
'device': {
|
||||
'deviceId': _DEVICE_ID,
|
||||
'width': 653,
|
||||
'height': 368,
|
||||
'type': 'WEB',
|
||||
'name': ' Mozilla Firefox 91'
|
||||
}
|
||||
}).encode('utf-8'))['sessionToken']
|
||||
|
||||
video_urls = self._download_json(
|
||||
f'https://exposure.api.redbee.live/v2/customer/UKParliament/businessunit/ParliamentLive/entitlement/{video_id}/play',
|
||||
video_id, headers={'Authorization': auth, 'Accept': 'application/json, text/plain, */*'})['formats']
|
||||
|
||||
formats = []
|
||||
for format in video_urls:
|
||||
if not format.get('mediaLocator'):
|
||||
continue
|
||||
if format.get('format') == 'DASH':
|
||||
formats.extend(self._extract_mpd_formats(
|
||||
format['mediaLocator'], video_id, mpd_id='dash', fatal=False))
|
||||
elif format.get('format') == 'SMOOTHSTREAMING':
|
||||
formats.extend(self._extract_ism_formats(
|
||||
format['mediaLocator'], video_id, ism_id='ism', fatal=False))
|
||||
elif format.get('format') == 'HLS':
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
format['mediaLocator'], video_id, m3u8_id='hls', fatal=False))
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'_type': 'url_transparent',
|
||||
'title': event_title,
|
||||
'description': '',
|
||||
'url': kaltura_url,
|
||||
'ie_key': 'Kaltura',
|
||||
'id': video_id,
|
||||
'formats': formats,
|
||||
'title': video_info['event']['title'],
|
||||
'timestamp': unified_timestamp(try_get(video_info, lambda x: x['event']['publishedStartTime'])),
|
||||
'thumbnail': video_info.get('thumbnailUrl'),
|
||||
}
|
||||
|
||||
@@ -15,12 +15,13 @@ from ..utils import (
|
||||
int_or_none,
|
||||
strip_or_none,
|
||||
unified_timestamp,
|
||||
unescapeHTML,
|
||||
)
|
||||
|
||||
|
||||
class PolskieRadioIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?polskieradio\.pl/\d+/\d+/Artykul/(?P<id>[0-9]+)'
|
||||
_TESTS = [{
|
||||
_TESTS = [{ # Old-style single broadcast.
|
||||
'url': 'http://www.polskieradio.pl/7/5102/Artykul/1587943,Prof-Andrzej-Nowak-o-historii-nie-da-sie-myslec-beznamietnie',
|
||||
'info_dict': {
|
||||
'id': '1587943',
|
||||
@@ -39,14 +40,41 @@ class PolskieRadioIE(InfoExtractor):
|
||||
'thumbnail': r're:^https?://static\.prsa\.pl/images/.*\.jpg$'
|
||||
},
|
||||
}],
|
||||
}, {
|
||||
'url': 'http://www.polskieradio.pl/265/5217/Artykul/1635803,Euro-2016-nie-ma-miejsca-na-blad-Polacy-graja-ze-Szwajcaria-o-cwiercfinal',
|
||||
}, { # New-style single broadcast.
|
||||
'url': 'https://www.polskieradio.pl/8/2382/Artykul/2534482,Zagarysci-Poezja-jak-spoiwo',
|
||||
'info_dict': {
|
||||
'id': '1635803',
|
||||
'title': 'Euro 2016: nie ma miejsca na błąd. Polacy grają ze Szwajcarią o ćwierćfinał',
|
||||
'description': 'md5:01cb7d0cad58664095d72b51a1ebada2',
|
||||
'id': '2534482',
|
||||
'title': 'Żagaryści. Poezja jak spoiwo',
|
||||
'description': 'md5:f18d95d5dcba747a09b635e21a4c0695',
|
||||
},
|
||||
'playlist_mincount': 12,
|
||||
'playlist': [{
|
||||
'md5': 'd07559829f61d5a93a75755987ded760',
|
||||
'info_dict': {
|
||||
'id': '2516679',
|
||||
'ext': 'mp3',
|
||||
'title': 'md5:c6e1234e0b747ad883cb91b7ad06b98c',
|
||||
'timestamp': 1592654400,
|
||||
'upload_date': '20200620',
|
||||
'duration': 1430,
|
||||
'thumbnail': r're:^https?://static\.prsa\.pl/images/.*\.jpg$'
|
||||
},
|
||||
}],
|
||||
}, { # Old-style multiple broadcast playlist.
|
||||
'url': 'https://www.polskieradio.pl/8/4346/Artykul/2487823,Marek-Kondrat-czyta-Mistrza-i-Malgorzate',
|
||||
'info_dict': {
|
||||
'id': '2487823',
|
||||
'title': 'Marek Kondrat czyta "Mistrza i Małgorzatę"',
|
||||
'description': 'md5:8422a95cc83834f2aaeff9d82e9c8f39',
|
||||
},
|
||||
'playlist_mincount': 50,
|
||||
}, { # New-style multiple broadcast playlist.
|
||||
'url': 'https://www.polskieradio.pl/8/4346/Artykul/2541317,Czytamy-Kalendarz-i-klepsydre-Tadeusza-Konwickiego',
|
||||
'info_dict': {
|
||||
'id': '2541317',
|
||||
'title': 'Czytamy "Kalendarz i klepsydrę" Tadeusza Konwickiego',
|
||||
'description': 'md5:0baeaa46d877f1351fb2eeed3e871f9f',
|
||||
},
|
||||
'playlist_mincount': 15,
|
||||
}, {
|
||||
'url': 'http://polskieradio.pl/9/305/Artykul/1632955,Bardzo-popularne-slowo-remis',
|
||||
'only_matching': True,
|
||||
@@ -78,8 +106,8 @@ class PolskieRadioIE(InfoExtractor):
|
||||
|
||||
media_urls = set()
|
||||
|
||||
for data_media in re.findall(r'<[^>]+data-media=({[^>]+})', content):
|
||||
media = self._parse_json(data_media, playlist_id, fatal=False)
|
||||
for data_media in re.findall(r'<[^>]+data-media="?({[^>]+})"?', content):
|
||||
media = self._parse_json(data_media, playlist_id, transform_source=unescapeHTML, fatal=False)
|
||||
if not media.get('file') or not media.get('desc'):
|
||||
continue
|
||||
media_url = self._proto_relative_url(media['file'], 'http:')
|
||||
@@ -98,6 +126,7 @@ class PolskieRadioIE(InfoExtractor):
|
||||
|
||||
title = self._og_search_title(webpage).strip()
|
||||
description = strip_or_none(self._og_search_description(webpage))
|
||||
description = description.replace('\xa0', ' ') if description is not None else None
|
||||
|
||||
return self.playlist_result(entries, playlist_id, title, description)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import random
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
@@ -49,7 +48,7 @@ class RedditIE(InfoExtractor):
|
||||
|
||||
|
||||
class RedditRIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:[^/]+\.)?reddit\.com/r/(?P<slug>[^/]+/comments/(?P<id>[^/?#&]+))'
|
||||
_VALID_URL = r'https?://(?P<subdomain>[^/]+\.)?reddit(?:media)?\.com/r/(?P<slug>[^/]+/comments/(?P<id>[^/?#&]+))'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.reddit.com/r/videos/comments/6rrwyj/that_small_heart_attack/',
|
||||
'info_dict': {
|
||||
@@ -94,15 +93,27 @@ class RedditRIE(InfoExtractor):
|
||||
# reddit video @ nm reddit
|
||||
'url': 'https://nm.reddit.com/r/Cricket/comments/8idvby/lousy_cameraman_finds_himself_in_cairns_line_of/',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://www.redditmedia.com/r/serbia/comments/pu9wbx/ako_vu%C4%8Di%C4%87_izgubi_izbore_ja_%C4%87u_da_crknem/',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
@staticmethod
|
||||
def _gen_session_id():
|
||||
id_length = 16
|
||||
rand_max = 1 << (id_length * 4)
|
||||
return '%0.*x' % (id_length, random.randrange(rand_max))
|
||||
|
||||
def _real_extract(self, url):
|
||||
slug, video_id = self._match_valid_url(url).group('slug', 'id')
|
||||
|
||||
self._set_cookie('reddit.com', '_options', '%7B%22pref_quarantine_optin%22%3A%20true%7D')
|
||||
data = self._download_json(
|
||||
f'https://old.reddit.com/r/{slug}/.json', video_id)[0]['data']['children'][0]['data']
|
||||
subdomain, slug, video_id = self._match_valid_url(url).group('subdomain', 'slug', 'id')
|
||||
|
||||
self._set_cookie('.reddit.com', 'reddit_session', self._gen_session_id())
|
||||
self._set_cookie('.reddit.com', '_options', '%7B%22pref_quarantine_optin%22%3A%20true%7D')
|
||||
data = self._download_json(f'https://{subdomain}reddit.com/r/{slug}/.json', video_id, fatal=False)
|
||||
if not data:
|
||||
# Fall back to old.reddit.com in case the requested subdomain fails
|
||||
data = self._download_json(f'https://old.reddit.com/r/{slug}/.json', video_id)
|
||||
data = data[0]['data']['children'][0]['data']
|
||||
video_url = data['url']
|
||||
|
||||
# Avoid recursing into the same reddit URL
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_str
|
||||
from ..compat import compat_str, compat_HTTPError
|
||||
from ..utils import (
|
||||
determine_ext,
|
||||
int_or_none,
|
||||
parse_iso8601,
|
||||
try_get,
|
||||
ExtractorError,
|
||||
)
|
||||
|
||||
|
||||
@@ -75,3 +77,36 @@ class RumbleEmbedIE(InfoExtractor):
|
||||
'channel_url': author.get('url'),
|
||||
'duration': int_or_none(video.get('duration')),
|
||||
}
|
||||
|
||||
|
||||
class RumbleChannelIE(InfoExtractor):
|
||||
_VALID_URL = r'(?P<url>https?://(?:www\.)?rumble\.com/(?:c|user)/(?P<id>[^&?#$/]+))'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://rumble.com/c/Styxhexenhammer666',
|
||||
'playlist_mincount': 1160,
|
||||
'info_dict': {
|
||||
'id': 'Styxhexenhammer666',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://rumble.com/user/goldenpoodleharleyeuna',
|
||||
'playlist_count': 4,
|
||||
'info_dict': {
|
||||
'id': 'goldenpoodleharleyeuna',
|
||||
},
|
||||
}]
|
||||
|
||||
def entries(self, url, playlist_id):
|
||||
for page in itertools.count(1):
|
||||
try:
|
||||
webpage = self._download_webpage(f'{url}?page={page}', playlist_id, note='Downloading page %d' % page)
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 404:
|
||||
break
|
||||
raise
|
||||
for video_url in re.findall(r'class=video-item--a\s?href=([^>]+\.html)', webpage):
|
||||
yield self.url_result('https://rumble.com' + video_url)
|
||||
|
||||
def _real_extract(self, url):
|
||||
url, playlist_id = self._match_valid_url(url).groups()
|
||||
return self.playlist_result(self.entries(url, playlist_id), playlist_id=playlist_id)
|
||||
|
||||
@@ -707,6 +707,8 @@ class SoundcloudPagedPlaylistBaseIE(SoundcloudIE):
|
||||
yield resolve_entry(e, e.get('track'), e.get('playlist'))
|
||||
|
||||
url = response.get('next_href')
|
||||
if not url:
|
||||
break
|
||||
query.pop('offset', None)
|
||||
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ class SovietsClosetIE(SovietsClosetBaseIE):
|
||||
'upload_date': '20170413',
|
||||
'uploader_id': 'SovietWomble',
|
||||
'uploader_url': 'https://www.twitch.tv/SovietWomble',
|
||||
'duration': 7007,
|
||||
'was_live': True,
|
||||
'availability': 'public',
|
||||
'series': 'The Witcher',
|
||||
@@ -96,6 +97,7 @@ class SovietsClosetIE(SovietsClosetBaseIE):
|
||||
'upload_date': '20160420',
|
||||
'uploader_id': 'SovietWomble',
|
||||
'uploader_url': 'https://www.twitch.tv/SovietWomble',
|
||||
'duration': 8804,
|
||||
'was_live': True,
|
||||
'availability': 'public',
|
||||
'series': 'Arma 3',
|
||||
@@ -116,9 +118,16 @@ class SovietsClosetIE(SovietsClosetBaseIE):
|
||||
m3u8_formats = self._extract_m3u8_formats(m3u8_url, video_id, headers=self.MEDIADELIVERY_REFERER)
|
||||
self._sort_formats(m3u8_formats)
|
||||
|
||||
if not m3u8_formats:
|
||||
duration = None
|
||||
else:
|
||||
duration = self._extract_m3u8_vod_duration(
|
||||
m3u8_formats[0]['url'], video_id, headers=self.MEDIADELIVERY_REFERER)
|
||||
|
||||
return {
|
||||
'formats': m3u8_formats,
|
||||
'thumbnail': thumbnail_url,
|
||||
'duration': duration,
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
|
||||
@@ -8,6 +8,8 @@ from ..utils import (
|
||||
ExtractorError,
|
||||
float_or_none,
|
||||
int_or_none,
|
||||
try_get,
|
||||
parse_codecs,
|
||||
)
|
||||
|
||||
|
||||
@@ -29,7 +31,7 @@ class StreamableIE(InfoExtractor):
|
||||
'view_count': int,
|
||||
}
|
||||
},
|
||||
# older video without bitrate, width/height, etc. info
|
||||
# older video without bitrate, width/height, codecs, etc. info
|
||||
{
|
||||
'url': 'https://streamable.com/moo',
|
||||
'md5': '2cf6923639b87fba3279ad0df3a64e73',
|
||||
@@ -95,7 +97,9 @@ class StreamableIE(InfoExtractor):
|
||||
'height': int_or_none(info.get('height')),
|
||||
'filesize': int_or_none(info.get('size')),
|
||||
'fps': int_or_none(info.get('framerate')),
|
||||
'vbr': float_or_none(info.get('bitrate'), 1000)
|
||||
'vbr': float_or_none(info.get('bitrate'), 1000),
|
||||
'vcodec': parse_codecs(try_get(info, lambda x: x['input_metadata']['video_codec_name'])).get('vcodec'),
|
||||
'acodec': parse_codecs(try_get(info, lambda x: x['input_metadata']['audio_codec_name'])).get('acodec'),
|
||||
})
|
||||
self._sort_formats(formats)
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ from .common import InfoExtractor
|
||||
from ..utils import try_get
|
||||
|
||||
|
||||
class ThetaIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?theta\.tv/(?P<id>[a-z0-9]+)'
|
||||
class ThetaStreamIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?theta\.tv/(?!video/)(?P<id>[a-z0-9]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.theta.tv/davirus',
|
||||
'skip': 'The live may have ended',
|
||||
@@ -49,3 +49,39 @@ class ThetaIE(InfoExtractor):
|
||||
'formats': formats,
|
||||
'thumbnail': try_get(info, lambda x: x['live_stream']['thumbnail_url']),
|
||||
}
|
||||
|
||||
|
||||
class ThetaVideoIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?theta\.tv/video/(?P<id>vid[a-z0-9]+)'
|
||||
_TEST = {
|
||||
'url': 'https://www.theta.tv/video/vidiq6aaet3kzf799p0',
|
||||
'md5': '633d8c29eb276bb38a111dbd591c677f',
|
||||
'info_dict': {
|
||||
'id': 'vidiq6aaet3kzf799p0',
|
||||
'ext': 'mp4',
|
||||
'title': 'Theta EdgeCast Tutorial',
|
||||
'uploader': 'Pixiekittie',
|
||||
'description': 'md5:e316253f5bdced8b5a46bb50ae60a09f',
|
||||
'thumbnail': r're:https://user-prod-theta-tv\.imgix\.net/.+/vod_thumb/.+.jpg',
|
||||
}
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
info = self._download_json(f'https://api.theta.tv/v1/video/{video_id}/raw', video_id)['body']
|
||||
|
||||
m3u8_playlist = try_get(info, lambda x: x['video_urls'][0]['url'])
|
||||
|
||||
formats = self._extract_m3u8_formats(m3u8_playlist, video_id, 'mp4', m3u8_id='hls')
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': info.get('title'),
|
||||
'uploader': try_get(info, lambda x: x['user']['username']),
|
||||
'description': info.get('description'),
|
||||
'view_count': info.get('view_count'),
|
||||
'like_count': info.get('like_count'),
|
||||
'formats': formats,
|
||||
'thumbnail': info.get('thumbnail_url'),
|
||||
}
|
||||
|
||||
+204
-60
@@ -8,12 +8,14 @@ import time
|
||||
import json
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_urllib_parse_unquote
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
int_or_none,
|
||||
str_or_none,
|
||||
traverse_obj,
|
||||
try_get,
|
||||
url_or_none,
|
||||
qualities,
|
||||
)
|
||||
|
||||
@@ -21,6 +23,10 @@ from ..utils import (
|
||||
class TikTokBaseIE(InfoExtractor):
|
||||
_APP_VERSION = '20.9.3'
|
||||
_MANIFEST_APP_VERSION = '291'
|
||||
_APP_NAME = 'trill'
|
||||
_AID = 1180
|
||||
_API_HOSTNAME = 'api-t2.tiktokv.com'
|
||||
_UPLOADER_URL_FORMAT = 'https://www.tiktok.com/@%s'
|
||||
QUALITIES = ('360p', '540p', '720p')
|
||||
|
||||
def _call_api(self, ep, query, video_id, fatal=True,
|
||||
@@ -46,7 +52,7 @@ class TikTokBaseIE(InfoExtractor):
|
||||
'carrier_region': 'US',
|
||||
'sys_region': 'US',
|
||||
'region': 'US',
|
||||
'app_name': 'trill',
|
||||
'app_name': self._APP_NAME,
|
||||
'app_language': 'en',
|
||||
'language': 'en',
|
||||
'timezone_name': 'America/New_York',
|
||||
@@ -55,20 +61,20 @@ class TikTokBaseIE(InfoExtractor):
|
||||
'ac': 'wifi',
|
||||
'mcc_mnc': '310260',
|
||||
'is_my_cn': 0,
|
||||
'aid': 1180,
|
||||
'aid': self._AID,
|
||||
'ssmix': 'a',
|
||||
'as': 'a1qwert123',
|
||||
'cp': 'cbfhckdckkde1',
|
||||
}
|
||||
self._set_cookie('.tiktokv.com', 'odin_tt', ''.join(random.choice('0123456789abcdef') for i in range(160)))
|
||||
self._set_cookie(self._API_HOSTNAME, 'odin_tt', ''.join(random.choice('0123456789abcdef') for i in range(160)))
|
||||
return self._download_json(
|
||||
'https://api-t2.tiktokv.com/aweme/v1/%s/' % ep, video_id=video_id,
|
||||
'https://%s/aweme/v1/%s/' % (self._API_HOSTNAME, ep), video_id=video_id,
|
||||
fatal=fatal, note=note, errnote=errnote, headers={
|
||||
'User-Agent': f'com.ss.android.ugc.trill/{self._MANIFEST_APP_VERSION} (Linux; U; Android 10; en_US; Pixel 4; Build/QQ3A.200805.001; Cronet/58.0.2991.0)',
|
||||
'Accept': 'application/json',
|
||||
}, query=real_query)
|
||||
|
||||
def _parse_aweme_video(self, aweme_detail):
|
||||
def _parse_aweme_video_app(self, aweme_detail):
|
||||
aweme_id = aweme_detail['aweme_id']
|
||||
video_info = aweme_detail['video']
|
||||
|
||||
@@ -146,6 +152,7 @@ class TikTokBaseIE(InfoExtractor):
|
||||
'tbr': try_get(bitrate, lambda x: x['bit_rate'] / 1000),
|
||||
'vcodec': 'h265' if traverse_obj(
|
||||
bitrate, 'is_bytevc1', 'is_h265') else 'h264',
|
||||
'fps': bitrate.get('FPS'),
|
||||
}))
|
||||
|
||||
self._remove_duplicate_formats(formats)
|
||||
@@ -165,7 +172,9 @@ class TikTokBaseIE(InfoExtractor):
|
||||
stats_info = aweme_detail.get('statistics', {})
|
||||
author_info = aweme_detail.get('author', {})
|
||||
music_info = aweme_detail.get('music', {})
|
||||
user_id = str_or_none(author_info.get('nickname'))
|
||||
user_url = self._UPLOADER_URL_FORMAT % (traverse_obj(author_info,
|
||||
'sec_uid', 'id', 'uid', 'unique_id',
|
||||
expected_type=str_or_none, get_all=False))
|
||||
|
||||
contained_music_track = traverse_obj(
|
||||
music_info, ('matched_song', 'title'), ('matched_pgc_sound', 'title'), expected_type=str)
|
||||
@@ -187,9 +196,9 @@ class TikTokBaseIE(InfoExtractor):
|
||||
'repost_count': int_or_none(stats_info.get('share_count')),
|
||||
'comment_count': int_or_none(stats_info.get('comment_count')),
|
||||
'uploader': str_or_none(author_info.get('unique_id')),
|
||||
'creator': user_id,
|
||||
'creator': str_or_none(author_info.get('nickname')),
|
||||
'uploader_id': str_or_none(author_info.get('uid')),
|
||||
'uploader_url': f'https://www.tiktok.com/@{user_id}' if user_id else None,
|
||||
'uploader_url': user_url,
|
||||
'track': music_track,
|
||||
'album': str_or_none(music_info.get('album')) or None,
|
||||
'artist': music_author,
|
||||
@@ -199,6 +208,79 @@ class TikTokBaseIE(InfoExtractor):
|
||||
'duration': int_or_none(traverse_obj(video_info, 'duration', ('download_addr', 'duration')), scale=1000)
|
||||
}
|
||||
|
||||
def _parse_aweme_video_web(self, aweme_detail, webpage, url):
|
||||
video_info = aweme_detail['video']
|
||||
author_info = traverse_obj(aweme_detail, 'author', 'authorInfo', default={})
|
||||
music_info = aweme_detail.get('music') or {}
|
||||
stats_info = aweme_detail.get('stats') or {}
|
||||
user_url = self._UPLOADER_URL_FORMAT % (traverse_obj(author_info,
|
||||
'secUid', 'id', 'uid', 'uniqueId',
|
||||
expected_type=str_or_none, get_all=False))
|
||||
|
||||
formats = []
|
||||
play_url = video_info.get('playAddr')
|
||||
width = video_info.get('width')
|
||||
height = video_info.get('height')
|
||||
if isinstance(play_url, str):
|
||||
formats = [{
|
||||
'url': self._proto_relative_url(play_url),
|
||||
'ext': 'mp4',
|
||||
'width': width,
|
||||
'height': height,
|
||||
}]
|
||||
elif isinstance(play_url, list):
|
||||
formats = [{
|
||||
'url': self._proto_relative_url(url),
|
||||
'ext': 'mp4',
|
||||
'width': width,
|
||||
'height': height,
|
||||
} for url in traverse_obj(play_url, (..., 'src'), expected_type=url_or_none, default=[]) if url]
|
||||
|
||||
download_url = url_or_none(video_info.get('downloadAddr')) or traverse_obj(video_info, ('download', 'url'), expected_type=url_or_none)
|
||||
if download_url:
|
||||
formats.append({
|
||||
'format_id': 'download',
|
||||
'url': self._proto_relative_url(download_url),
|
||||
'ext': 'mp4',
|
||||
'width': width,
|
||||
'height': height,
|
||||
})
|
||||
self._remove_duplicate_formats(formats)
|
||||
self._sort_formats(formats)
|
||||
|
||||
thumbnails = []
|
||||
for thumbnail_name in ('thumbnail', 'cover', 'dynamicCover', 'originCover'):
|
||||
if aweme_detail.get(thumbnail_name):
|
||||
thumbnails = [{
|
||||
'url': self._proto_relative_url(aweme_detail[thumbnail_name]),
|
||||
'width': width,
|
||||
'height': height
|
||||
}]
|
||||
|
||||
return {
|
||||
'id': traverse_obj(aweme_detail, 'id', 'awemeId', expected_type=str_or_none),
|
||||
'title': aweme_detail.get('desc'),
|
||||
'duration': try_get(aweme_detail, lambda x: x['video']['duration'], int),
|
||||
'view_count': int_or_none(stats_info.get('playCount')),
|
||||
'like_count': int_or_none(stats_info.get('diggCount')),
|
||||
'repost_count': int_or_none(stats_info.get('shareCount')),
|
||||
'comment_count': int_or_none(stats_info.get('commentCount')),
|
||||
'timestamp': int_or_none(aweme_detail.get('createTime')),
|
||||
'creator': str_or_none(author_info.get('nickname')),
|
||||
'uploader': str_or_none(author_info.get('uniqueId')),
|
||||
'uploader_id': str_or_none(author_info.get('id')),
|
||||
'uploader_url': user_url,
|
||||
'track': str_or_none(music_info.get('title')),
|
||||
'album': str_or_none(music_info.get('album')) or None,
|
||||
'artist': str_or_none(music_info.get('authorName')),
|
||||
'formats': formats,
|
||||
'thumbnails': thumbnails,
|
||||
'description': str_or_none(aweme_detail.get('desc')),
|
||||
'http_headers': {
|
||||
'Referer': url
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TikTokIE(TikTokBaseIE):
|
||||
_VALID_URL = r'https?://www\.tiktok\.com/@[\w\.-]+/video/(?P<id>\d+)'
|
||||
@@ -255,60 +337,10 @@ class TikTokIE(TikTokBaseIE):
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _extract_aweme(self, props_data, webpage, url):
|
||||
video_info = try_get(
|
||||
props_data, lambda x: x['pageProps']['itemInfo']['itemStruct'], dict)
|
||||
author_info = try_get(
|
||||
props_data, lambda x: x['pageProps']['itemInfo']['itemStruct']['author'], dict) or {}
|
||||
music_info = try_get(
|
||||
props_data, lambda x: x['pageProps']['itemInfo']['itemStruct']['music'], dict) or {}
|
||||
stats_info = try_get(props_data, lambda x: x['pageProps']['itemInfo']['itemStruct']['stats'], dict) or {}
|
||||
|
||||
user_id = str_or_none(author_info.get('uniqueId'))
|
||||
download_url = try_get(video_info, (lambda x: x['video']['playAddr'],
|
||||
lambda x: x['video']['downloadAddr']))
|
||||
height = try_get(video_info, lambda x: x['video']['height'], int)
|
||||
width = try_get(video_info, lambda x: x['video']['width'], int)
|
||||
thumbnails = [{
|
||||
'url': video_info.get('thumbnail') or self._og_search_thumbnail(webpage),
|
||||
'width': width,
|
||||
'height': height
|
||||
}]
|
||||
tracker = try_get(props_data, lambda x: x['initialProps']['$wid'])
|
||||
|
||||
return {
|
||||
'id': str_or_none(video_info.get('id')),
|
||||
'url': download_url,
|
||||
'ext': 'mp4',
|
||||
'height': height,
|
||||
'width': width,
|
||||
'title': video_info.get('desc') or self._og_search_title(webpage),
|
||||
'duration': try_get(video_info, lambda x: x['video']['duration'], int),
|
||||
'view_count': int_or_none(stats_info.get('playCount')),
|
||||
'like_count': int_or_none(stats_info.get('diggCount')),
|
||||
'repost_count': int_or_none(stats_info.get('shareCount')),
|
||||
'comment_count': int_or_none(stats_info.get('commentCount')),
|
||||
'timestamp': try_get(video_info, lambda x: int(x['createTime']), int),
|
||||
'creator': str_or_none(author_info.get('nickname')),
|
||||
'uploader': user_id,
|
||||
'uploader_id': str_or_none(author_info.get('id')),
|
||||
'uploader_url': f'https://www.tiktok.com/@{user_id}',
|
||||
'track': str_or_none(music_info.get('title')),
|
||||
'album': str_or_none(music_info.get('album')) or None,
|
||||
'artist': str_or_none(music_info.get('authorName')),
|
||||
'thumbnails': thumbnails,
|
||||
'description': str_or_none(video_info.get('desc')),
|
||||
'webpage_url': self._og_search_url(webpage),
|
||||
'http_headers': {
|
||||
'Referer': url,
|
||||
'Cookie': 'tt_webid=%s; tt_webid_v2=%s' % (tracker, tracker),
|
||||
}
|
||||
}
|
||||
|
||||
def _extract_aweme_app(self, aweme_id):
|
||||
aweme_detail = self._call_api('aweme/detail', {'aweme_id': aweme_id}, aweme_id,
|
||||
note='Downloading video details', errnote='Unable to download video details')['aweme_detail']
|
||||
return self._parse_aweme_video(aweme_detail)
|
||||
return self._parse_aweme_video_app(aweme_detail)
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
@@ -330,7 +362,7 @@ class TikTokIE(TikTokBaseIE):
|
||||
# Chech statusCode for success
|
||||
status = props_data.get('pageProps').get('statusCode')
|
||||
if status == 0:
|
||||
return self._extract_aweme(props_data, webpage, url)
|
||||
return self._parse_aweme_video_web(props_data['pageProps']['itemInfo']['itemStruct'], webpage, url)
|
||||
elif status == 10216:
|
||||
raise ExtractorError('This video is private', expected=True)
|
||||
|
||||
@@ -413,3 +445,115 @@ class TikTokUserIE(TikTokBaseIE):
|
||||
})
|
||||
own_id = self._html_search_regex(r'snssdk\d*://user/profile/(\d+)', webpage, 'user ID')
|
||||
return self.playlist_result(self._entries_api(webpage, own_id, user_id), user_id)
|
||||
|
||||
|
||||
class DouyinIE(TikTokIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?douyin\.com/video/(?P<id>[0-9]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.douyin.com/video/6961737553342991651',
|
||||
'md5': '10523312c8b8100f353620ac9dc8f067',
|
||||
'info_dict': {
|
||||
'id': '6961737553342991651',
|
||||
'ext': 'mp4',
|
||||
'title': '#杨超越 小小水手带你去远航❤️',
|
||||
'uploader': '杨超越',
|
||||
'upload_date': '20210513',
|
||||
'timestamp': 1620905839,
|
||||
'uploader_id': '110403406559',
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'repost_count': int,
|
||||
'comment_count': int,
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.douyin.com/video/6982497745948921092',
|
||||
'md5': 'd78408c984b9b5102904cf6b6bc2d712',
|
||||
'info_dict': {
|
||||
'id': '6982497745948921092',
|
||||
'ext': 'mp4',
|
||||
'title': '这个夏日和小羊@杨超越 一起遇见白色幻想',
|
||||
'uploader': '杨超越工作室',
|
||||
'upload_date': '20210708',
|
||||
'timestamp': 1625739481,
|
||||
'uploader_id': '408654318141572',
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'repost_count': int,
|
||||
'comment_count': int,
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.douyin.com/video/6953975910773099811',
|
||||
'md5': '72e882e24f75064c218b76c8b713c185',
|
||||
'info_dict': {
|
||||
'id': '6953975910773099811',
|
||||
'ext': 'mp4',
|
||||
'title': '#一起看海 出现在你的夏日里',
|
||||
'uploader': '杨超越',
|
||||
'upload_date': '20210422',
|
||||
'timestamp': 1619098692,
|
||||
'uploader_id': '110403406559',
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'repost_count': int,
|
||||
'comment_count': int,
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.douyin.com/video/6950251282489675042',
|
||||
'md5': 'b4db86aec367ef810ddd38b1737d2fed',
|
||||
'info_dict': {
|
||||
'id': '6950251282489675042',
|
||||
'ext': 'mp4',
|
||||
'title': '哈哈哈,成功了哈哈哈哈哈哈',
|
||||
'uploader': '杨超越',
|
||||
'upload_date': '20210412',
|
||||
'timestamp': 1618231483,
|
||||
'uploader_id': '110403406559',
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'repost_count': int,
|
||||
'comment_count': int,
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.douyin.com/video/6963263655114722595',
|
||||
'md5': '1abe1c477d05ee62efb40bf2329957cf',
|
||||
'info_dict': {
|
||||
'id': '6963263655114722595',
|
||||
'ext': 'mp4',
|
||||
'title': '#哪个爱豆的105度最甜 换个角度看看我哈哈',
|
||||
'uploader': '杨超越',
|
||||
'upload_date': '20210517',
|
||||
'timestamp': 1621261163,
|
||||
'uploader_id': '110403406559',
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'repost_count': int,
|
||||
'comment_count': int,
|
||||
}
|
||||
}]
|
||||
_APP_VERSION = '9.6.0'
|
||||
_MANIFEST_APP_VERSION = '960'
|
||||
_APP_NAME = 'aweme'
|
||||
_AID = 1128
|
||||
_API_HOSTNAME = 'aweme.snssdk.com'
|
||||
_UPLOADER_URL_FORMAT = 'https://www.douyin.com/user/%s'
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
|
||||
try:
|
||||
return self._extract_aweme_app(video_id)
|
||||
except ExtractorError as e:
|
||||
self.report_warning(f'{e}; Retrying with webpage')
|
||||
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
render_data_json = self._search_regex(
|
||||
r'<script [^>]*\bid=[\'"]RENDER_DATA[\'"][^>]*>(%7B.+%7D)</script>',
|
||||
webpage, 'render data', default=None)
|
||||
if not render_data_json:
|
||||
# TODO: Run verification challenge code to generate signature cookies
|
||||
raise ExtractorError('Fresh cookies (not necessarily logged in) are needed')
|
||||
|
||||
render_data = self._parse_json(
|
||||
render_data_json, video_id, transform_source=compat_urllib_parse_unquote)
|
||||
return self._parse_aweme_video_web(
|
||||
traverse_obj(render_data, (..., 'aweme', 'detail'), get_all=False), webpage, url)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
|
||||
from ..utils import (
|
||||
int_or_none,
|
||||
mimetype2ext,
|
||||
unified_timestamp,
|
||||
url_or_none,
|
||||
)
|
||||
|
||||
|
||||
class VeoIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://app\.veo\.co/matches/(?P<id>[0-9A-Za-z-]+)'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://app.veo.co/matches/20201027-last-period/',
|
||||
'info_dict': {
|
||||
'id': '20201027-last-period',
|
||||
'ext': 'mp4',
|
||||
'title': 'Akidemy u11s v Bradford Boys u11s (Game 3)',
|
||||
'thumbnail': 're:https://c.veocdn.com/.+/thumbnail.jpg',
|
||||
'upload_date': '20201028',
|
||||
'timestamp': 1603847208,
|
||||
'duration': 1916,
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
|
||||
metadata = self._download_json(
|
||||
'https://app.veo.co/api/app/matches/%s' % video_id, video_id)
|
||||
|
||||
video_data = self._download_json(
|
||||
'https://app.veo.co/api/app/matches/%s/videos' % video_id, video_id, 'Downloading video data')
|
||||
|
||||
title = metadata.get('title')
|
||||
thumbnail = url_or_none(metadata.get('thumbnail'))
|
||||
|
||||
timestamp = unified_timestamp(metadata.get('created'))
|
||||
duration = int_or_none(metadata.get('duration'))
|
||||
view_count = int_or_none(metadata.get('view_count'))
|
||||
|
||||
formats = []
|
||||
for fmt in video_data:
|
||||
mimetype = fmt.get('mime_type')
|
||||
# skip configuration file for panoramic video
|
||||
if mimetype == 'video/mp2t':
|
||||
continue
|
||||
height = int_or_none(fmt.get('height'))
|
||||
bitrate = int_or_none(fmt.get('bit_rate'), scale=1000)
|
||||
render_type = fmt.get('render_type')
|
||||
formats.append({
|
||||
'url': url_or_none(fmt.get('url')),
|
||||
'format_id': '%s-%sp' % (render_type, height),
|
||||
'ext': mimetype2ext(mimetype),
|
||||
'width': int_or_none(fmt.get('width')),
|
||||
'height': height,
|
||||
'vbr': bitrate
|
||||
})
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'formats': formats,
|
||||
'thumbnail': thumbnail,
|
||||
'timestamp': timestamp,
|
||||
'view_count': view_count,
|
||||
'duration': duration
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_HTTPError
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
int_or_none,
|
||||
float_or_none,
|
||||
parse_iso8601,
|
||||
url_or_none,
|
||||
)
|
||||
|
||||
|
||||
class VidmeIE(InfoExtractor):
|
||||
IE_NAME = 'vidme'
|
||||
_VALID_URL = r'https?://vid\.me/(?:e/)?(?P<id>[\da-zA-Z]{,5})(?:[^\da-zA-Z]|$)'
|
||||
_TESTS = [{
|
||||
'url': 'https://vid.me/QNB',
|
||||
'md5': 'f42d05e7149aeaec5c037b17e5d3dc82',
|
||||
'info_dict': {
|
||||
'id': 'QNB',
|
||||
'ext': 'mp4',
|
||||
'title': 'Fishing for piranha - the easy way',
|
||||
'description': 'source: https://www.facebook.com/photo.php?v=312276045600871',
|
||||
'thumbnail': r're:^https?://.*\.jpg',
|
||||
'timestamp': 1406313244,
|
||||
'upload_date': '20140725',
|
||||
'age_limit': 0,
|
||||
'duration': 119.92,
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'comment_count': int,
|
||||
},
|
||||
}, {
|
||||
'url': 'https://vid.me/Gc6M',
|
||||
'md5': 'f42d05e7149aeaec5c037b17e5d3dc82',
|
||||
'info_dict': {
|
||||
'id': 'Gc6M',
|
||||
'ext': 'mp4',
|
||||
'title': 'O Mere Dil ke chain - Arnav and Khushi VM',
|
||||
'thumbnail': r're:^https?://.*\.jpg',
|
||||
'timestamp': 1441211642,
|
||||
'upload_date': '20150902',
|
||||
'uploader': 'SunshineM',
|
||||
'uploader_id': '3552827',
|
||||
'age_limit': 0,
|
||||
'duration': 223.72,
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'comment_count': int,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
# tests uploader field
|
||||
'url': 'https://vid.me/4Iib',
|
||||
'info_dict': {
|
||||
'id': '4Iib',
|
||||
'ext': 'mp4',
|
||||
'title': 'The Carver',
|
||||
'description': 'md5:e9c24870018ae8113be936645b93ba3c',
|
||||
'thumbnail': r're:^https?://.*\.jpg',
|
||||
'timestamp': 1433203629,
|
||||
'upload_date': '20150602',
|
||||
'uploader': 'Thomas',
|
||||
'uploader_id': '109747',
|
||||
'age_limit': 0,
|
||||
'duration': 97.859999999999999,
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'comment_count': int,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
# nsfw test from http://naked-yogi.tumblr.com/post/118312946248/naked-smoking-stretching
|
||||
'url': 'https://vid.me/e/Wmur',
|
||||
'info_dict': {
|
||||
'id': 'Wmur',
|
||||
'ext': 'mp4',
|
||||
'title': 'naked smoking & stretching',
|
||||
'thumbnail': r're:^https?://.*\.jpg',
|
||||
'timestamp': 1430931613,
|
||||
'upload_date': '20150506',
|
||||
'uploader': 'naked-yogi',
|
||||
'uploader_id': '1638622',
|
||||
'age_limit': 18,
|
||||
'duration': 653.26999999999998,
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'comment_count': int,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
# nsfw, user-disabled
|
||||
'url': 'https://vid.me/dzGJ',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
# suspended
|
||||
'url': 'https://vid.me/Ox3G',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
# deleted
|
||||
'url': 'https://vid.me/KTPm',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
# no formats in the API response
|
||||
'url': 'https://vid.me/e5g',
|
||||
'info_dict': {
|
||||
'id': 'e5g',
|
||||
'ext': 'mp4',
|
||||
'title': 'Video upload (e5g)',
|
||||
'thumbnail': r're:^https?://.*\.jpg',
|
||||
'timestamp': 1401480195,
|
||||
'upload_date': '20140530',
|
||||
'uploader': None,
|
||||
'uploader_id': None,
|
||||
'age_limit': 0,
|
||||
'duration': 483,
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'comment_count': int,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
|
||||
try:
|
||||
response = self._download_json(
|
||||
'https://api.vid.me/videoByUrl/%s' % video_id, video_id)
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
|
||||
response = self._parse_json(e.cause.read(), video_id)
|
||||
else:
|
||||
raise
|
||||
|
||||
error = response.get('error')
|
||||
if error:
|
||||
raise ExtractorError(
|
||||
'%s returned error: %s' % (self.IE_NAME, error), expected=True)
|
||||
|
||||
video = response['video']
|
||||
|
||||
if video.get('state') == 'deleted':
|
||||
raise ExtractorError(
|
||||
'Vidme said: Sorry, this video has been deleted.',
|
||||
expected=True)
|
||||
|
||||
if video.get('state') in ('user-disabled', 'suspended'):
|
||||
raise ExtractorError(
|
||||
'Vidme said: This video has been suspended either due to a copyright claim, '
|
||||
'or for violating the terms of use.',
|
||||
expected=True)
|
||||
|
||||
formats = []
|
||||
for f in video.get('formats', []):
|
||||
format_url = url_or_none(f.get('uri'))
|
||||
if not format_url:
|
||||
continue
|
||||
format_type = f.get('type')
|
||||
if format_type == 'dash':
|
||||
formats.extend(self._extract_mpd_formats(
|
||||
format_url, video_id, mpd_id='dash', fatal=False))
|
||||
elif format_type == 'hls':
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
format_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||
m3u8_id='hls', fatal=False))
|
||||
else:
|
||||
formats.append({
|
||||
'format_id': f.get('type'),
|
||||
'url': format_url,
|
||||
'width': int_or_none(f.get('width')),
|
||||
'height': int_or_none(f.get('height')),
|
||||
# Clips should never be prefered over full video
|
||||
'preference': 0 if f.get('type', '').endswith(
|
||||
'clip') else 1,
|
||||
})
|
||||
|
||||
if not formats and video.get('complete_url'):
|
||||
formats.append({
|
||||
'url': video.get('complete_url'),
|
||||
'width': int_or_none(video.get('width')),
|
||||
'height': int_or_none(video.get('height')),
|
||||
})
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
title = video['title']
|
||||
description = video.get('description')
|
||||
thumbnail = video.get('thumbnail_url')
|
||||
timestamp = parse_iso8601(video.get('date_created'), ' ')
|
||||
uploader = video.get('user', {}).get('username')
|
||||
uploader_id = video.get('user', {}).get('user_id')
|
||||
age_limit = 18 if video.get('nsfw') is True else 0
|
||||
duration = float_or_none(video.get('duration'))
|
||||
view_count = int_or_none(video.get('view_count'))
|
||||
like_count = int_or_none(video.get('likes_count'))
|
||||
comment_count = int_or_none(video.get('comment_count'))
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': title or 'Video upload (%s)' % video_id,
|
||||
'description': description,
|
||||
'thumbnail': thumbnail,
|
||||
'uploader': uploader,
|
||||
'uploader_id': uploader_id,
|
||||
'age_limit': age_limit,
|
||||
'timestamp': timestamp,
|
||||
'duration': duration,
|
||||
'view_count': view_count,
|
||||
'like_count': like_count,
|
||||
'comment_count': comment_count,
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
|
||||
class VidmeListBaseIE(InfoExtractor):
|
||||
# Max possible limit according to https://docs.vid.me/#api-Videos-List
|
||||
_LIMIT = 100
|
||||
|
||||
def _entries(self, user_id, user_name):
|
||||
for page_num in itertools.count(1):
|
||||
page = self._download_json(
|
||||
'https://api.vid.me/videos/%s?user=%s&limit=%d&offset=%d'
|
||||
% (self._API_ITEM, user_id, self._LIMIT, (page_num - 1) * self._LIMIT),
|
||||
user_name, 'Downloading user %s page %d' % (self._API_ITEM, page_num))
|
||||
|
||||
videos = page.get('videos', [])
|
||||
if not videos:
|
||||
break
|
||||
|
||||
for video in videos:
|
||||
video_url = video.get('full_url') or video.get('embed_url')
|
||||
if video_url:
|
||||
yield self.url_result(video_url, VidmeIE.ie_key())
|
||||
|
||||
total = int_or_none(page.get('page', {}).get('total'))
|
||||
if total and self._LIMIT * page_num >= total:
|
||||
break
|
||||
|
||||
def _real_extract(self, url):
|
||||
user_name = self._match_id(url)
|
||||
|
||||
user_id = self._download_json(
|
||||
'https://api.vid.me/userByUsername?username=%s' % user_name,
|
||||
user_name)['user']['user_id']
|
||||
|
||||
return self.playlist_result(
|
||||
self._entries(user_id, user_name), user_id,
|
||||
'%s - %s' % (user_name, self._TITLE))
|
||||
|
||||
|
||||
class VidmeUserIE(VidmeListBaseIE):
|
||||
IE_NAME = 'vidme:user'
|
||||
_VALID_URL = r'https?://vid\.me/(?:e/)?(?P<id>[\da-zA-Z_-]{6,})(?!/likes)(?:[^\da-zA-Z_-]|$)'
|
||||
_API_ITEM = 'list'
|
||||
_TITLE = 'Videos'
|
||||
_TESTS = [{
|
||||
'url': 'https://vid.me/MasakoX',
|
||||
'info_dict': {
|
||||
'id': '16112341',
|
||||
'title': 'MasakoX - %s' % _TITLE,
|
||||
},
|
||||
'playlist_mincount': 191,
|
||||
}, {
|
||||
'url': 'https://vid.me/unsQuare_netWork',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
|
||||
class VidmeUserLikesIE(VidmeListBaseIE):
|
||||
IE_NAME = 'vidme:user:likes'
|
||||
_VALID_URL = r'https?://vid\.me/(?:e/)?(?P<id>[\da-zA-Z_-]{6,})/likes'
|
||||
_API_ITEM = 'likes'
|
||||
_TITLE = 'Likes'
|
||||
_TESTS = [{
|
||||
'url': 'https://vid.me/ErinAlexis/likes',
|
||||
'info_dict': {
|
||||
'id': '6483530',
|
||||
'title': 'ErinAlexis - %s' % _TITLE,
|
||||
},
|
||||
'playlist_mincount': 415,
|
||||
}, {
|
||||
'url': 'https://vid.me/Kaleidoscope-Ish/likes',
|
||||
'only_matching': True,
|
||||
}]
|
||||
@@ -0,0 +1,51 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
parse_duration,
|
||||
parse_filesize,
|
||||
extract_attributes,
|
||||
int_or_none,
|
||||
)
|
||||
|
||||
|
||||
class VuploadIE(InfoExtractor):
|
||||
_VALID_URL = r'https://vupload\.com/v/(?P<id>[a-z0-9]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://vupload.com/v/u28d0pl2tphy',
|
||||
'md5': '9b42a4a193cca64d80248e58527d83c8',
|
||||
'info_dict': {
|
||||
'id': 'u28d0pl2tphy',
|
||||
'ext': 'mp4',
|
||||
'description': 'md5:e9e6c0045c78cbf0d5bb19a55ce199fb',
|
||||
'title': 'md5:e9e6c0045c78cbf0d5bb19a55ce199fb',
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
title = self._html_search_regex(r'<title>(.+?)</title>', webpage, 'title')
|
||||
video_e = self._html_search_regex(r'\|([a-z0-9]{60})\|', webpage, 'video')
|
||||
video_url = f'https://wurize.megaupload.to/{video_e}/v.mp4'
|
||||
duration = parse_duration(self._html_search_regex(
|
||||
r'<i\s*class=["\']fad\s*fa-clock["\']></i>\s*([\d:]+)\s*</div>', webpage, 'duration', fatal=False))
|
||||
filesize_approx = parse_filesize(self._html_search_regex(
|
||||
r'<i\s*class=["\']fad\s*fa-save["\']></i>\s*([^<]+)\s*</div>', webpage, 'filesize', fatal=False))
|
||||
extra_video_info = extract_attributes(self._html_search_regex(
|
||||
r'(<video[^>]+>)', webpage, 'video_info', fatal=False))
|
||||
description = self._html_search_meta('description', webpage)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'url': video_url,
|
||||
'duration': duration,
|
||||
'filesize_approx': filesize_approx,
|
||||
'width': int_or_none(extra_video_info.get('width')),
|
||||
'height': int_or_none(extra_video_info.get('height')),
|
||||
'format_id': extra_video_info.get('height', '') + 'p',
|
||||
'title': title,
|
||||
'description': description,
|
||||
}
|
||||
+172
-61
@@ -579,12 +579,12 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
data=json.dumps(data).encode('utf8'), headers=real_headers,
|
||||
query={'key': api_key or self._extract_api_key()})
|
||||
|
||||
def extract_yt_initial_data(self, video_id, webpage):
|
||||
return self._parse_json(
|
||||
self._search_regex(
|
||||
(r'%s\s*%s' % (self._YT_INITIAL_DATA_RE, self._YT_INITIAL_BOUNDARY_RE),
|
||||
self._YT_INITIAL_DATA_RE), webpage, 'yt initial data'),
|
||||
video_id)
|
||||
def extract_yt_initial_data(self, item_id, webpage, fatal=True):
|
||||
data = self._search_regex(
|
||||
(r'%s\s*%s' % (self._YT_INITIAL_DATA_RE, self._YT_INITIAL_BOUNDARY_RE),
|
||||
self._YT_INITIAL_DATA_RE), webpage, 'yt initial data', fatal=fatal)
|
||||
if data:
|
||||
return self._parse_json(data, item_id, fatal=fatal)
|
||||
|
||||
@staticmethod
|
||||
def _extract_session_index(*data):
|
||||
@@ -621,12 +621,22 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
return delegated_sid
|
||||
sync_ids = (try_get(
|
||||
data, (lambda x: x['responseContext']['mainAppWebResponseContext']['datasyncId'],
|
||||
lambda x: x['DATASYNC_ID']), compat_str) or '').split("||")
|
||||
lambda x: x['DATASYNC_ID']), compat_str) or '').split('||')
|
||||
if len(sync_ids) >= 2 and sync_ids[1]:
|
||||
# datasyncid is of the form "channel_syncid||user_syncid" for secondary channel
|
||||
# and just "user_syncid||" for primary channel. We only want the channel_syncid
|
||||
return sync_ids[0]
|
||||
|
||||
@staticmethod
|
||||
def _extract_visitor_data(*args):
|
||||
"""
|
||||
Extracts visitorData from an API response or ytcfg
|
||||
Appears to be used to track session state
|
||||
"""
|
||||
return traverse_obj(
|
||||
args, (..., ('VISITOR_DATA', ('INNERTUBE_CONTEXT', 'client', 'visitorData'), ('responseContext', 'visitorData'))),
|
||||
expected_type=compat_str, get_all=False)
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return bool(self._generate_sapisidhash_header())
|
||||
@@ -651,8 +661,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
'Origin': origin,
|
||||
'X-Youtube-Identity-Token': identity_token or self._extract_identity_token(ytcfg),
|
||||
'X-Goog-PageId': account_syncid or self._extract_account_syncid(ytcfg),
|
||||
'X-Goog-Visitor-Id': visitor_data or try_get(
|
||||
self._extract_context(ytcfg, default_client), lambda x: x['client']['visitorData'], compat_str)
|
||||
'X-Goog-Visitor-Id': visitor_data or self._extract_visitor_data(ytcfg)
|
||||
}
|
||||
if session_index is None:
|
||||
session_index = self._extract_session_index(ytcfg)
|
||||
@@ -826,9 +835,8 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
return
|
||||
|
||||
else:
|
||||
# Youtube may send alerts if there was an issue with the continuation page
|
||||
try:
|
||||
self._extract_and_report_alerts(response, expected=False, only_once=True)
|
||||
self._extract_and_report_alerts(response, only_once=True)
|
||||
except ExtractorError as e:
|
||||
# YouTube servers may return errors we want to retry on in a 200 OK response
|
||||
# See: https://github.com/yt-dlp/yt-dlp/issues/839
|
||||
@@ -1911,10 +1919,12 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
def _load_player(self, video_id, player_url, fatal=True) -> bool:
|
||||
player_id = self._extract_player_info(player_url)
|
||||
if player_id not in self._code_cache:
|
||||
self._code_cache[player_id] = self._download_webpage(
|
||||
code = self._download_webpage(
|
||||
player_url, video_id, fatal=fatal,
|
||||
note='Downloading player ' + player_id,
|
||||
errnote='Download of %s failed' % player_url)
|
||||
if code:
|
||||
self._code_cache[player_id] = code
|
||||
return player_id in self._code_cache
|
||||
|
||||
def _extract_signature_function(self, video_id, player_url, example_sig):
|
||||
@@ -3547,7 +3557,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
'url': 'https://www.youtube.com/feed/watch_later',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'note': 'Recommended - redirects to home page',
|
||||
'note': 'Recommended - redirects to home page.',
|
||||
'url': 'https://www.youtube.com/feed/recommended',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
@@ -3644,6 +3654,51 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
'availability': 'unlisted'
|
||||
},
|
||||
'playlist_count': 1,
|
||||
}, {
|
||||
'note': 'API Fallback: Recommended - redirects to home page. Requires visitorData',
|
||||
'url': 'https://www.youtube.com/feed/recommended',
|
||||
'info_dict': {
|
||||
'id': 'recommended',
|
||||
'title': 'recommended',
|
||||
},
|
||||
'playlist_mincount': 50,
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
'extractor_args': {'youtubetab': {'skip': ['webpage']}}
|
||||
},
|
||||
}, {
|
||||
'note': 'API Fallback: /videos tab, sorted by oldest first',
|
||||
'url': 'https://www.youtube.com/user/theCodyReeder/videos?view=0&sort=da&flow=grid',
|
||||
'info_dict': {
|
||||
'id': 'UCu6mSoMNzHQiBIOCkHUa2Aw',
|
||||
'title': 'Cody\'sLab - Videos',
|
||||
'description': 'md5:d083b7c2f0c67ee7a6c74c3e9b4243fa',
|
||||
'uploader': 'Cody\'sLab',
|
||||
'uploader_id': 'UCu6mSoMNzHQiBIOCkHUa2Aw',
|
||||
},
|
||||
'playlist_mincount': 650,
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
'extractor_args': {'youtubetab': {'skip': ['webpage']}}
|
||||
},
|
||||
}, {
|
||||
'note': 'API Fallback: Topic, should redirect to playlist?list=UU...',
|
||||
'url': 'https://music.youtube.com/browse/UC9ALqqC4aIeG5iDs7i90Bfw',
|
||||
'info_dict': {
|
||||
'id': 'UU9ALqqC4aIeG5iDs7i90Bfw',
|
||||
'uploader_id': 'UC9ALqqC4aIeG5iDs7i90Bfw',
|
||||
'title': 'Uploads from Royalty Free Music - Topic',
|
||||
'uploader': 'Royalty Free Music - Topic',
|
||||
},
|
||||
'expected_warnings': [
|
||||
'A channel/user page was given',
|
||||
'The URL does not have a videos tab',
|
||||
],
|
||||
'playlist_mincount': 101,
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
'extractor_args': {'youtubetab': {'skip': ['webpage']}}
|
||||
},
|
||||
}]
|
||||
|
||||
@classmethod
|
||||
@@ -3832,7 +3887,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
if entry:
|
||||
yield entry
|
||||
'''
|
||||
def _entries(self, tab, item_id, account_syncid, ytcfg):
|
||||
def _entries(self, tab, item_id, ytcfg, account_syncid, visitor_data):
|
||||
|
||||
def extract_entries(parent_renderer): # this needs to called again for continuation to work with feeds
|
||||
contents = try_get(parent_renderer, lambda x: x['contents'], list) or []
|
||||
@@ -3874,7 +3929,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
if not continuation_list[0]:
|
||||
continuation_list[0] = self._extract_continuation(parent_renderer)
|
||||
|
||||
continuation_list = [None] # Python 2 doesnot support nonlocal
|
||||
continuation_list = [None] # Python 2 does not support nonlocal
|
||||
tab_content = try_get(tab, lambda x: x['content'], dict)
|
||||
if not tab_content:
|
||||
return
|
||||
@@ -3884,7 +3939,6 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
for entry in extract_entries(parent_renderer):
|
||||
yield entry
|
||||
continuation = continuation_list[0]
|
||||
visitor_data = None
|
||||
|
||||
for page_num in itertools.count(1):
|
||||
if not continuation:
|
||||
@@ -3898,8 +3952,9 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
|
||||
if not response:
|
||||
break
|
||||
visitor_data = try_get(
|
||||
response, lambda x: x['responseContext']['visitorData'], compat_str) or visitor_data
|
||||
# Extracting updated visitor data is required to prevent an infinite extraction loop in some cases
|
||||
# See: https://github.com/ytdl-org/youtube-dl/issues/28702
|
||||
visitor_data = self._extract_visitor_data(response) or visitor_data
|
||||
|
||||
known_continuation_renderers = {
|
||||
'playlistVideoListContinuation': self._playlist_entries,
|
||||
@@ -3973,9 +4028,10 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
try_get(owner, lambda x: x['navigationEndpoint']['browseEndpoint']['canonicalBaseUrl'], compat_str))
|
||||
return {k: v for k, v in uploader.items() if v is not None}
|
||||
|
||||
def _extract_from_tabs(self, item_id, webpage, data, tabs):
|
||||
def _extract_from_tabs(self, item_id, ytcfg, data, tabs):
|
||||
playlist_id = title = description = channel_url = channel_name = channel_id = None
|
||||
thumbnails_list = tags = []
|
||||
thumbnails_list = []
|
||||
tags = []
|
||||
|
||||
selected_tab = self._extract_selected_tab(tabs)
|
||||
renderer = try_get(
|
||||
@@ -4040,18 +4096,15 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
'channel': metadata['uploader'],
|
||||
'channel_id': metadata['uploader_id'],
|
||||
'channel_url': metadata['uploader_url']})
|
||||
ytcfg = self.extract_ytcfg(item_id, webpage)
|
||||
return self.playlist_result(
|
||||
self._entries(
|
||||
selected_tab, playlist_id,
|
||||
self._extract_account_syncid(ytcfg, data), ytcfg),
|
||||
selected_tab, playlist_id, ytcfg,
|
||||
self._extract_account_syncid(ytcfg, data),
|
||||
self._extract_visitor_data(data, ytcfg)),
|
||||
**metadata)
|
||||
|
||||
def _extract_mix_playlist(self, playlist, playlist_id, data, webpage):
|
||||
first_id = last_id = None
|
||||
ytcfg = self.extract_ytcfg(playlist_id, webpage)
|
||||
headers = self.generate_api_headers(
|
||||
ytcfg=ytcfg, account_syncid=self._extract_account_syncid(ytcfg, data))
|
||||
def _extract_mix_playlist(self, playlist, playlist_id, data, ytcfg):
|
||||
first_id = last_id = response = None
|
||||
for page_num in itertools.count(1):
|
||||
videos = list(self._playlist_entries(playlist))
|
||||
if not videos:
|
||||
@@ -4068,6 +4121,9 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
last_id = videos[-1]['id']
|
||||
watch_endpoint = try_get(
|
||||
playlist, lambda x: x['contents'][-1]['playlistPanelVideoRenderer']['navigationEndpoint']['watchEndpoint'])
|
||||
headers = self.generate_api_headers(
|
||||
ytcfg=ytcfg, account_syncid=self._extract_account_syncid(ytcfg, data),
|
||||
visitor_data=self._extract_visitor_data(response, data, ytcfg))
|
||||
query = {
|
||||
'playlistId': playlist_id,
|
||||
'videoId': watch_endpoint.get('videoId') or last_id,
|
||||
@@ -4082,7 +4138,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
playlist = try_get(
|
||||
response, lambda x: x['contents']['twoColumnWatchNextResults']['playlist']['playlist'], dict)
|
||||
|
||||
def _extract_from_playlist(self, item_id, url, data, playlist, webpage):
|
||||
def _extract_from_playlist(self, item_id, url, data, playlist, ytcfg):
|
||||
title = playlist.get('title') or try_get(
|
||||
data, lambda x: x['titleText']['simpleText'], compat_str)
|
||||
playlist_id = playlist.get('playlistId') or item_id
|
||||
@@ -4097,7 +4153,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
video_title=title)
|
||||
|
||||
return self.playlist_result(
|
||||
self._extract_mix_playlist(playlist, playlist_id, data, webpage),
|
||||
self._extract_mix_playlist(playlist, playlist_id, data, ytcfg),
|
||||
playlist_id=playlist_id, playlist_title=title)
|
||||
|
||||
def _extract_availability(self, data):
|
||||
@@ -4141,7 +4197,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
if renderer:
|
||||
return renderer
|
||||
|
||||
def _reload_with_unavailable_videos(self, item_id, data, webpage):
|
||||
def _reload_with_unavailable_videos(self, item_id, data, ytcfg):
|
||||
"""
|
||||
Get playlist with unavailable videos if the 'show unavailable videos' button exists.
|
||||
"""
|
||||
@@ -4165,10 +4221,9 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
params = browse_endpoint.get('params')
|
||||
break
|
||||
|
||||
ytcfg = self.extract_ytcfg(item_id, webpage)
|
||||
headers = self.generate_api_headers(
|
||||
ytcfg=ytcfg, account_syncid=self._extract_account_syncid(ytcfg, data),
|
||||
visitor_data=try_get(self._extract_context(ytcfg), lambda x: x['client']['visitorData'], compat_str))
|
||||
visitor_data=self._extract_visitor_data(data, ytcfg))
|
||||
query = {
|
||||
'params': params or 'wgYCCAA=',
|
||||
'browseId': browse_id or 'VL%s' % item_id
|
||||
@@ -4178,28 +4233,87 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
check_get_keys='contents', fatal=False, ytcfg=ytcfg,
|
||||
note='Downloading API JSON with unavailable videos')
|
||||
|
||||
def _extract_webpage(self, url, item_id):
|
||||
def _extract_webpage(self, url, item_id, fatal=True):
|
||||
retries = self.get_param('extractor_retries', 3)
|
||||
count = -1
|
||||
last_error = 'Incomplete yt initial data recieved'
|
||||
webpage = data = last_error = None
|
||||
while count < retries:
|
||||
count += 1
|
||||
# Sometimes youtube returns a webpage with incomplete ytInitialData
|
||||
# See: https://github.com/yt-dlp/yt-dlp/issues/116
|
||||
if count:
|
||||
if last_error:
|
||||
self.report_warning('%s. Retrying ...' % last_error)
|
||||
webpage = self._download_webpage(
|
||||
url, item_id,
|
||||
'Downloading webpage%s' % (' (retry #%d)' % count if count else ''))
|
||||
data = self.extract_yt_initial_data(item_id, webpage)
|
||||
if data.get('contents') or data.get('currentVideoEndpoint'):
|
||||
try:
|
||||
webpage = self._download_webpage(
|
||||
url, item_id,
|
||||
note='Downloading webpage%s' % (' (retry #%d)' % count if count else '',))
|
||||
data = self.extract_yt_initial_data(item_id, webpage or '', fatal=fatal) or {}
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, network_exceptions):
|
||||
if not isinstance(e.cause, compat_HTTPError) or e.cause.code not in (403, 429):
|
||||
last_error = error_to_compat_str(e.cause or e.msg)
|
||||
if count < retries:
|
||||
continue
|
||||
if fatal:
|
||||
raise
|
||||
self.report_warning(error_to_compat_str(e))
|
||||
break
|
||||
# Extract alerts here only when there is error
|
||||
self._extract_and_report_alerts(data)
|
||||
if count >= retries:
|
||||
raise ExtractorError(last_error)
|
||||
else:
|
||||
try:
|
||||
self._extract_and_report_alerts(data)
|
||||
except ExtractorError as e:
|
||||
if fatal:
|
||||
raise
|
||||
self.report_warning(error_to_compat_str(e))
|
||||
break
|
||||
|
||||
if dict_get(data, ('contents', 'currentVideoEndpoint')):
|
||||
break
|
||||
|
||||
last_error = 'Incomplete yt initial data received'
|
||||
if count >= retries:
|
||||
if fatal:
|
||||
raise ExtractorError(last_error)
|
||||
self.report_warning(last_error)
|
||||
break
|
||||
|
||||
return webpage, data
|
||||
|
||||
def _extract_data(self, url, item_id, ytcfg=None, fatal=True, webpage_fatal=False, default_client='web'):
|
||||
data = None
|
||||
if 'webpage' not in self._configuration_arg('skip'):
|
||||
webpage, data = self._extract_webpage(url, item_id, fatal=webpage_fatal)
|
||||
ytcfg = ytcfg or self.extract_ytcfg(item_id, webpage)
|
||||
if not data:
|
||||
if not ytcfg and self.is_authenticated:
|
||||
msg = 'Playlists that require authentication may not extract correctly without a successful webpage download.'
|
||||
if 'authcheck' not in self._configuration_arg('skip') and fatal:
|
||||
raise ExtractorError(
|
||||
msg + ' If you are not downloading private content, or your cookies are only for the first account and channel,'
|
||||
' pass "--extractor-args youtubetab:skip=authcheck" to skip this check',
|
||||
expected=True)
|
||||
self.report_warning(msg, only_once=True)
|
||||
data = self._extract_tab_endpoint(url, item_id, ytcfg, fatal=fatal, default_client=default_client)
|
||||
return data, ytcfg
|
||||
|
||||
def _extract_tab_endpoint(self, url, item_id, ytcfg=None, fatal=True, default_client='web'):
|
||||
headers = self.generate_api_headers(ytcfg=ytcfg, default_client=default_client)
|
||||
resolve_response = self._extract_response(
|
||||
item_id=item_id, query={'url': url}, check_get_keys='endpoint', headers=headers, ytcfg=ytcfg, fatal=fatal,
|
||||
ep='navigation/resolve_url', note='Downloading API parameters API JSON', default_client=default_client)
|
||||
endpoints = {'browseEndpoint': 'browse', 'watchEndpoint': 'next'}
|
||||
for ep_key, ep in endpoints.items():
|
||||
params = try_get(resolve_response, lambda x: x['endpoint'][ep_key], dict)
|
||||
if params:
|
||||
return self._extract_response(
|
||||
item_id=item_id, query=params, ep=ep, headers=headers,
|
||||
ytcfg=ytcfg, fatal=fatal, default_client=default_client,
|
||||
check_get_keys=('contents', 'currentVideoEndpoint'))
|
||||
err_note = 'Failed to resolve url (does the playlist exist?)'
|
||||
if fatal:
|
||||
raise ExtractorError(err_note, expected=True)
|
||||
self.report_warning(err_note, item_id)
|
||||
|
||||
@staticmethod
|
||||
def _smuggle_data(entries, data):
|
||||
for entry in entries:
|
||||
@@ -4232,7 +4346,6 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
mobj = get_mobj(url)
|
||||
# Youtube returns incomplete data if tabname is not lower case
|
||||
pre, tab, post, is_channel = mobj['pre'], mobj['tab'].lower(), mobj['post'], not mobj['not_channel']
|
||||
|
||||
if is_channel:
|
||||
if smuggled_data.get('is_music_url'):
|
||||
if item_id[:2] == 'VL':
|
||||
@@ -4240,12 +4353,14 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
item_id = item_id[2:]
|
||||
pre, tab, post, is_channel = 'https://www.youtube.com/playlist?list=%s' % item_id, '', '', False
|
||||
elif item_id[:2] == 'MP':
|
||||
# Youtube music albums (/channel/MP...) have a OLAK playlist that can be extracted from the webpage
|
||||
item_id = self._search_regex(
|
||||
r'\\x22audioPlaylistId\\x22:\\x22([0-9A-Za-z_-]+)\\x22',
|
||||
self._download_webpage('https://music.youtube.com/channel/%s' % item_id, item_id),
|
||||
'playlist id')
|
||||
pre, tab, post, is_channel = 'https://www.youtube.com/playlist?list=%s' % item_id, '', '', False
|
||||
# Resolve albums (/[channel/browse]/MP...) to their equivalent playlist
|
||||
mdata = self._extract_tab_endpoint(
|
||||
'https://music.youtube.com/channel/%s' % item_id, item_id, default_client='web_music')
|
||||
murl = traverse_obj(
|
||||
mdata, ('microformat', 'microformatDataRenderer', 'urlCanonical'), get_all=False, expected_type=compat_str)
|
||||
if not murl:
|
||||
raise ExtractorError('Failed to resolve album to playlist.')
|
||||
return self.url_result(murl, ie=YoutubeTabIE.ie_key())
|
||||
elif mobj['channel_type'] == 'browse':
|
||||
# Youtube music /browse/ should be changed to /channel/
|
||||
pre = 'https://www.youtube.com/channel/%s' % item_id
|
||||
@@ -4279,7 +4394,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
return self.url_result(f'https://www.youtube.com/watch?v={video_id}', ie=YoutubeIE.ie_key(), video_id=video_id)
|
||||
self.to_screen('Downloading playlist %s; add --no-playlist to just download video %s' % (playlist_id, video_id))
|
||||
|
||||
webpage, data = self._extract_webpage(url, item_id)
|
||||
data, ytcfg = self._extract_data(url, item_id)
|
||||
|
||||
tabs = try_get(
|
||||
data, lambda x: x['contents']['twoColumnBrowseResultsRenderer']['tabs'], list)
|
||||
@@ -4297,11 +4412,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
pl_id = 'UU%s' % item_id[2:]
|
||||
pl_url = 'https://www.youtube.com/playlist?list=%s%s' % (pl_id, mobj['post'])
|
||||
try:
|
||||
pl_webpage, pl_data = self._extract_webpage(pl_url, pl_id)
|
||||
for alert_type, alert_message in self._extract_alerts(pl_data):
|
||||
if alert_type == 'error':
|
||||
raise ExtractorError('Youtube said: %s' % alert_message)
|
||||
item_id, url, webpage, data = pl_id, pl_url, pl_webpage, pl_data
|
||||
data, ytcfg, item_id, url = *self._extract_data(pl_url, pl_id, ytcfg=ytcfg, fatal=True), pl_id, pl_url
|
||||
except ExtractorError:
|
||||
self.report_warning('The playlist gave error. Falling back to channel URL')
|
||||
else:
|
||||
@@ -4311,17 +4422,17 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
|
||||
|
||||
# YouTube sometimes provides a button to reload playlist with unavailable videos.
|
||||
if 'no-youtube-unavailable-videos' not in compat_opts:
|
||||
data = self._reload_with_unavailable_videos(item_id, data, webpage) or data
|
||||
data = self._reload_with_unavailable_videos(item_id, data, ytcfg) or data
|
||||
self._extract_and_report_alerts(data, only_once=True)
|
||||
tabs = try_get(
|
||||
data, lambda x: x['contents']['twoColumnBrowseResultsRenderer']['tabs'], list)
|
||||
if tabs:
|
||||
return self._extract_from_tabs(item_id, webpage, data, tabs)
|
||||
return self._extract_from_tabs(item_id, ytcfg, data, tabs)
|
||||
|
||||
playlist = try_get(
|
||||
data, lambda x: x['contents']['twoColumnWatchNextResults']['playlist']['playlist'], dict)
|
||||
if playlist:
|
||||
return self._extract_from_playlist(item_id, url, data, playlist, webpage)
|
||||
return self._extract_from_playlist(item_id, url, data, playlist, ytcfg)
|
||||
|
||||
video_id = try_get(
|
||||
data, lambda x: x['currentVideoEndpoint']['watchEndpoint']['videoId'],
|
||||
|
||||
+85
-113
@@ -1,10 +1,13 @@
|
||||
import os
|
||||
|
||||
import functools
|
||||
from threading import Lock
|
||||
from .utils import compat_os_name, get_windows_version
|
||||
from .utils import supports_terminal_sequences, TERMINAL_SEQUENCES
|
||||
|
||||
|
||||
class MultilinePrinterBase():
|
||||
class MultilinePrinterBase:
|
||||
def __init__(self, stream=None, lines=1):
|
||||
self.stream = stream
|
||||
self.maximum = lines - 1
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
@@ -17,119 +20,88 @@ class MultilinePrinterBase():
|
||||
def end(self):
|
||||
pass
|
||||
|
||||
|
||||
class MultilinePrinter(MultilinePrinterBase):
|
||||
|
||||
def __init__(self, stream, lines):
|
||||
"""
|
||||
@param stream stream to write to
|
||||
@lines number of lines to be written
|
||||
"""
|
||||
self.stream = stream
|
||||
|
||||
is_win10 = compat_os_name == 'nt' and get_windows_version() >= (10, )
|
||||
self.CARRIAGE_RETURN = '\r'
|
||||
if os.getenv('TERM') and self._isatty() or is_win10:
|
||||
# reason not to use curses https://github.com/yt-dlp/yt-dlp/pull/1036#discussion_r713851492
|
||||
# escape sequences for Win10 https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
|
||||
self.UP = '\x1b[A'
|
||||
self.DOWN = '\n'
|
||||
self.ERASE_LINE = '\x1b[K'
|
||||
self._HAVE_FULLCAP = self._isatty() or is_win10
|
||||
else:
|
||||
self.UP = self.DOWN = self.ERASE_LINE = None
|
||||
self._HAVE_FULLCAP = False
|
||||
|
||||
# lines are numbered from top to bottom, counting from 0 to self.maximum
|
||||
self.maximum = lines - 1
|
||||
self.lastline = 0
|
||||
self.lastlength = 0
|
||||
|
||||
self.movelock = Lock()
|
||||
|
||||
@property
|
||||
def have_fullcap(self):
|
||||
"""
|
||||
True if the TTY is allowing to control cursor,
|
||||
so that multiline progress works
|
||||
"""
|
||||
return self._HAVE_FULLCAP
|
||||
|
||||
def _isatty(self):
|
||||
try:
|
||||
return self.stream.isatty()
|
||||
except BaseException:
|
||||
return False
|
||||
|
||||
def _move_cursor(self, dest):
|
||||
current = min(self.lastline, self.maximum)
|
||||
self.stream.write(self.CARRIAGE_RETURN)
|
||||
if current == dest:
|
||||
# current and dest are at same position, no need to move cursor
|
||||
return
|
||||
elif current > dest:
|
||||
# when maximum == 2,
|
||||
# 0. dest
|
||||
# 1.
|
||||
# 2. current
|
||||
self.stream.write(self.UP * (current - dest))
|
||||
elif current < dest:
|
||||
# when maximum == 2,
|
||||
# 0. current
|
||||
# 1.
|
||||
# 2. dest
|
||||
self.stream.write(self.DOWN * (dest - current))
|
||||
self.lastline = dest
|
||||
|
||||
def print_at_line(self, text, pos):
|
||||
with self.movelock:
|
||||
if self.have_fullcap:
|
||||
self._move_cursor(pos)
|
||||
self.stream.write(self.ERASE_LINE)
|
||||
self.stream.write(text)
|
||||
else:
|
||||
if self.maximum != 0:
|
||||
# let user know about which line is updating the status
|
||||
text = f'{pos + 1}: {text}'
|
||||
textlen = len(text)
|
||||
if self.lastline == pos:
|
||||
# move cursor at the start of progress when writing to same line
|
||||
self.stream.write(self.CARRIAGE_RETURN)
|
||||
if self.lastlength > textlen:
|
||||
text += ' ' * (self.lastlength - textlen)
|
||||
self.lastlength = textlen
|
||||
else:
|
||||
# otherwise, break the line
|
||||
self.stream.write('\n')
|
||||
self.lastlength = 0
|
||||
self.stream.write(text)
|
||||
self.lastline = pos
|
||||
|
||||
def end(self):
|
||||
with self.movelock:
|
||||
# move cursor to the end of the last line, and write line break
|
||||
# so that other to_screen calls can precede
|
||||
self._move_cursor(self.maximum)
|
||||
self.stream.write('\n')
|
||||
def _add_line_number(self, text, line):
|
||||
if self.maximum:
|
||||
return f'{line + 1}: {text}'
|
||||
return text
|
||||
|
||||
|
||||
class QuietMultilinePrinter(MultilinePrinterBase):
|
||||
def __init__(self):
|
||||
self.have_fullcap = True
|
||||
pass
|
||||
|
||||
|
||||
class MultilineLogger(MultilinePrinterBase):
|
||||
def print_at_line(self, text, pos):
|
||||
# stream is the logger object, not an actual stream
|
||||
self.stream.debug(self._add_line_number(text, pos))
|
||||
|
||||
|
||||
class BreaklineStatusPrinter(MultilinePrinterBase):
|
||||
|
||||
def __init__(self, stream, lines):
|
||||
"""
|
||||
@param stream stream to write to
|
||||
"""
|
||||
self.stream = stream
|
||||
self.maximum = lines
|
||||
self.have_fullcap = True
|
||||
|
||||
def print_at_line(self, text, pos):
|
||||
if self.maximum != 0:
|
||||
# let user know about which line is updating the status
|
||||
text = f'{pos + 1}: {text}'
|
||||
self.stream.write(text + '\n')
|
||||
self.stream.write(self._add_line_number(text, pos) + '\n')
|
||||
|
||||
|
||||
class MultilinePrinter(MultilinePrinterBase):
|
||||
def __init__(self, stream=None, lines=1, preserve_output=True):
|
||||
super().__init__(stream, lines)
|
||||
self.preserve_output = preserve_output
|
||||
self._lastline = self._lastlength = 0
|
||||
self._movelock = Lock()
|
||||
self._HAVE_FULLCAP = supports_terminal_sequences(self.stream)
|
||||
|
||||
def lock(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
with self._movelock:
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
def _move_cursor(self, dest):
|
||||
current = min(self._lastline, self.maximum)
|
||||
self.stream.write('\r')
|
||||
distance = dest - current
|
||||
if distance < 0:
|
||||
self.stream.write(TERMINAL_SEQUENCES['UP'] * -distance)
|
||||
elif distance > 0:
|
||||
self.stream.write(TERMINAL_SEQUENCES['DOWN'] * distance)
|
||||
self._lastline = dest
|
||||
|
||||
@lock
|
||||
def print_at_line(self, text, pos):
|
||||
if self._HAVE_FULLCAP:
|
||||
self._move_cursor(pos)
|
||||
self.stream.write(TERMINAL_SEQUENCES['ERASE_LINE'])
|
||||
self.stream.write(text)
|
||||
return
|
||||
|
||||
text = self._add_line_number(text, pos)
|
||||
textlen = len(text)
|
||||
if self._lastline == pos:
|
||||
# move cursor at the start of progress when writing to same line
|
||||
self.stream.write('\r')
|
||||
if self._lastlength > textlen:
|
||||
text += ' ' * (self._lastlength - textlen)
|
||||
self._lastlength = textlen
|
||||
else:
|
||||
# otherwise, break the line
|
||||
self.stream.write('\n')
|
||||
self._lastlength = textlen
|
||||
self.stream.write(text)
|
||||
self._lastline = pos
|
||||
|
||||
@lock
|
||||
def end(self):
|
||||
# move cursor to the end of the last line, and write line break
|
||||
# so that other to_screen calls can precede
|
||||
if self._HAVE_FULLCAP:
|
||||
self._move_cursor(self.maximum)
|
||||
if self.preserve_output:
|
||||
self.stream.write('\n')
|
||||
return
|
||||
|
||||
if self._HAVE_FULLCAP:
|
||||
self.stream.write(
|
||||
TERMINAL_SEQUENCES['ERASE_LINE']
|
||||
+ f'{TERMINAL_SEQUENCES["UP"]}{TERMINAL_SEQUENCES["ERASE_LINE"]}' * self.maximum)
|
||||
else:
|
||||
self.stream.write(' ' * self._lastlength)
|
||||
|
||||
+42
-19
@@ -17,6 +17,7 @@ from .utils import (
|
||||
get_executable_path,
|
||||
OUTTMPL_TYPES,
|
||||
preferredencoding,
|
||||
remove_end,
|
||||
write_string,
|
||||
)
|
||||
from .cookies import SUPPORTED_BROWSERS
|
||||
@@ -260,7 +261,7 @@ def parseOpts(overrideArguments=None):
|
||||
general.add_option(
|
||||
'--mark-watched',
|
||||
action='store_true', dest='mark_watched', default=False,
|
||||
help='Mark videos watched (YouTube only)')
|
||||
help='Mark videos watched (even with --simulate). Currently only supported for YouTube')
|
||||
general.add_option(
|
||||
'--no-mark-watched',
|
||||
action='store_false', dest='mark_watched',
|
||||
@@ -767,7 +768,7 @@ def parseOpts(overrideArguments=None):
|
||||
dest='encoding', metavar='ENCODING',
|
||||
help='Force the specified encoding (experimental)')
|
||||
workarounds.add_option(
|
||||
'--no-check-certificate',
|
||||
'--no-check-certificates',
|
||||
action='store_true', dest='no_check_certificate', default=False,
|
||||
help='Suppress HTTPS certificate validation')
|
||||
workarounds.add_option(
|
||||
@@ -909,12 +910,30 @@ def parseOpts(overrideArguments=None):
|
||||
help='Output progress bar as new lines')
|
||||
verbosity.add_option(
|
||||
'--no-progress',
|
||||
action='store_true', dest='noprogress', default=False,
|
||||
action='store_true', dest='noprogress', default=None,
|
||||
help='Do not print progress bar')
|
||||
verbosity.add_option(
|
||||
'--progress',
|
||||
action='store_false', dest='noprogress',
|
||||
help='Show progress bar, even if in quiet mode')
|
||||
verbosity.add_option(
|
||||
'--console-title',
|
||||
action='store_true', dest='consoletitle', default=False,
|
||||
help='Display progress in console titlebar')
|
||||
verbosity.add_option(
|
||||
'--progress-template',
|
||||
metavar='[TYPES:]TEMPLATE', dest='progress_template', default={}, type='str',
|
||||
action='callback', callback=_dict_from_options_callback,
|
||||
callback_kwargs={
|
||||
'allowed_keys': '(download|postprocess)(-title)?',
|
||||
'default_key': 'download'
|
||||
}, help=(
|
||||
'Template for progress outputs, optionally prefixed with one of "download:" (default), '
|
||||
'"download-title:" (the console title), "postprocess:", or "postprocess-title:". '
|
||||
'The video\'s fields are accessible under the "info" key and '
|
||||
'the progress attributes are accessible under "progress" key. Eg: '
|
||||
# TODO: Document the fields inside "progress"
|
||||
'--console-title --progress-template "download-title:%(info.id)s-%(progress.eta)s"'))
|
||||
verbosity.add_option(
|
||||
'-v', '--verbose',
|
||||
action='store_true', dest='verbose', default=False,
|
||||
@@ -952,9 +971,6 @@ def parseOpts(overrideArguments=None):
|
||||
dest='batchfile', metavar='FILE',
|
||||
help="File containing URLs to download ('-' for stdin), one URL per line. "
|
||||
"Lines starting with '#', ';' or ']' are considered as comments and ignored")
|
||||
filesystem.add_option(
|
||||
'--id', default=False,
|
||||
action='store_true', dest='useid', help=optparse.SUPPRESS_HELP)
|
||||
filesystem.add_option(
|
||||
'-P', '--paths',
|
||||
metavar='[TYPES:]PATH', dest='paths', default={}, type='str',
|
||||
@@ -1010,18 +1026,6 @@ def parseOpts(overrideArguments=None):
|
||||
'--trim-filenames', '--trim-file-names', metavar='LENGTH',
|
||||
dest='trim_file_name', default=0, type=int,
|
||||
help='Limit the filename length (excluding extension) to the specified number of characters')
|
||||
filesystem.add_option(
|
||||
'--auto-number',
|
||||
action='store_true', dest='autonumber', default=False,
|
||||
help=optparse.SUPPRESS_HELP)
|
||||
filesystem.add_option(
|
||||
'--title',
|
||||
action='store_true', dest='usetitle', default=False,
|
||||
help=optparse.SUPPRESS_HELP)
|
||||
filesystem.add_option(
|
||||
'--literal', default=False,
|
||||
action='store_true', dest='usetitle',
|
||||
help=optparse.SUPPRESS_HELP)
|
||||
filesystem.add_option(
|
||||
'-w', '--no-overwrites',
|
||||
action='store_false', dest='overwrites', default=None,
|
||||
@@ -1389,6 +1393,25 @@ def parseOpts(overrideArguments=None):
|
||||
'--no-force-keyframes-at-cuts',
|
||||
action='store_false', dest='force_keyframes_at_cuts',
|
||||
help='Do not force keyframes around the chapters when cutting/splitting (default)')
|
||||
_postprocessor_opts_parser = lambda key, val='': (
|
||||
*(item.split('=', 1) for item in (val.split(';') if val else [])),
|
||||
('key', remove_end(key, 'PP')))
|
||||
postproc.add_option(
|
||||
'--use-postprocessor',
|
||||
metavar='NAME[:ARGS]', dest='add_postprocessors', default=[], type='str',
|
||||
action='callback', callback=_list_from_options_callback,
|
||||
callback_kwargs={
|
||||
'delim': None,
|
||||
'process': lambda val: dict(_postprocessor_opts_parser(*val.split(':', 1)))
|
||||
}, help=(
|
||||
'The (case sensitive) name of plugin postprocessors to be enabled, '
|
||||
'and (optionally) arguments to be passed to it, seperated by a colon ":". '
|
||||
'ARGS are a semicolon ";" delimited list of NAME=VALUE. '
|
||||
'The "when" argument determines when the postprocessor is invoked. '
|
||||
'It can be one of "pre_process" (after extraction), '
|
||||
'"before_dl" (before video download), "post_process" (after video download; default) '
|
||||
'or "after_move" (after moving file to their final locations). '
|
||||
'This option can be used multiple times to add different postprocessors'))
|
||||
|
||||
sponsorblock = optparse.OptionGroup(parser, 'SponsorBlock Options', description=(
|
||||
'Make chapter entries for, or remove various segments (sponsor, introductions, etc.) '
|
||||
@@ -1587,7 +1610,7 @@ def parseOpts(overrideArguments=None):
|
||||
argv = configs['system'] + configs['user'] + configs['home'] + configs['portable'] + configs['custom'] + configs['command-line']
|
||||
opts, args = parser.parse_args(argv)
|
||||
if opts.verbose:
|
||||
for label in ('System', 'User', 'Portable', 'Home', 'Custom', 'Command-line'):
|
||||
for label in ('Command-line', 'Custom', 'Portable', 'Home', 'User', 'System'):
|
||||
key = label.lower()
|
||||
if paths.get(key):
|
||||
write_string(f'[debug] {label} config file: {paths[key]}\n')
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
# flake8: noqa: F401
|
||||
|
||||
from ..utils import load_plugins
|
||||
|
||||
from .embedthumbnail import EmbedThumbnailPP
|
||||
from .exec import ExecPP, ExecAfterDownloadPP
|
||||
from .ffmpeg import (
|
||||
FFmpegPostProcessor,
|
||||
FFmpegEmbedSubtitlePP,
|
||||
@@ -18,48 +21,23 @@ from .ffmpeg import (
|
||||
FFmpegVideoConvertorPP,
|
||||
FFmpegVideoRemuxerPP,
|
||||
)
|
||||
from .xattrpp import XAttrMetadataPP
|
||||
from .exec import ExecPP, ExecAfterDownloadPP
|
||||
from .metadataparser import (
|
||||
MetadataFromFieldPP,
|
||||
MetadataFromTitlePP,
|
||||
MetadataParserPP,
|
||||
)
|
||||
from .movefilesafterdownload import MoveFilesAfterDownloadPP
|
||||
from .sponsorblock import SponsorBlockPP
|
||||
from .sponskrub import SponSkrubPP
|
||||
from .modify_chapters import ModifyChaptersPP
|
||||
from .movefilesafterdownload import MoveFilesAfterDownloadPP
|
||||
from .sponskrub import SponSkrubPP
|
||||
from .sponsorblock import SponsorBlockPP
|
||||
from .xattrpp import XAttrMetadataPP
|
||||
|
||||
_PLUGIN_CLASSES = load_plugins('postprocessor', 'PP', globals())
|
||||
|
||||
|
||||
def get_postprocessor(key):
|
||||
return globals()[key + 'PP']
|
||||
|
||||
|
||||
__all__ = [
|
||||
'FFmpegPostProcessor',
|
||||
'EmbedThumbnailPP',
|
||||
'ExecPP',
|
||||
'ExecAfterDownloadPP',
|
||||
'FFmpegEmbedSubtitlePP',
|
||||
'FFmpegExtractAudioPP',
|
||||
'FFmpegSplitChaptersPP',
|
||||
'FFmpegFixupDurationPP',
|
||||
'FFmpegFixupM3u8PP',
|
||||
'FFmpegFixupM4aPP',
|
||||
'FFmpegFixupStretchedPP',
|
||||
'FFmpegFixupTimestampPP',
|
||||
'FFmpegMergerPP',
|
||||
'FFmpegMetadataPP',
|
||||
'FFmpegSubtitlesConvertorPP',
|
||||
'FFmpegThumbnailsConvertorPP',
|
||||
'FFmpegVideoConvertorPP',
|
||||
'FFmpegVideoRemuxerPP',
|
||||
'MetadataParserPP',
|
||||
'MetadataFromFieldPP',
|
||||
'MetadataFromTitlePP',
|
||||
'MoveFilesAfterDownloadPP',
|
||||
'SponsorBlockPP',
|
||||
'SponSkrubPP',
|
||||
'ModifyChaptersPP',
|
||||
'XAttrMetadataPP',
|
||||
]
|
||||
__all__ = [name for name in globals().keys() if name.endswith('IE')]
|
||||
__all__.append('FFmpegPostProcessor')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import copy
|
||||
import functools
|
||||
import os
|
||||
|
||||
@@ -11,7 +12,26 @@ from ..utils import (
|
||||
)
|
||||
|
||||
|
||||
class PostProcessor(object):
|
||||
class PostProcessorMetaClass(type):
|
||||
@staticmethod
|
||||
def run_wrapper(func):
|
||||
@functools.wraps(func)
|
||||
def run(self, info, *args, **kwargs):
|
||||
self._hook_progress({'status': 'started'}, info)
|
||||
ret = func(self, info, *args, **kwargs)
|
||||
if ret is not None:
|
||||
_, info = ret
|
||||
self._hook_progress({'status': 'finished'}, info)
|
||||
return ret
|
||||
return run
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
if 'run' in attrs:
|
||||
attrs['run'] = cls.run_wrapper(attrs['run'])
|
||||
return type.__new__(cls, name, bases, attrs)
|
||||
|
||||
|
||||
class PostProcessor(metaclass=PostProcessorMetaClass):
|
||||
"""Post Processor class.
|
||||
|
||||
PostProcessor objects can be added to downloaders with their
|
||||
@@ -34,7 +54,9 @@ class PostProcessor(object):
|
||||
_downloader = None
|
||||
|
||||
def __init__(self, downloader=None):
|
||||
self._downloader = downloader
|
||||
self._progress_hooks = []
|
||||
self.add_progress_hook(self.report_progress)
|
||||
self.set_downloader(downloader)
|
||||
self.PP_NAME = self.pp_key()
|
||||
|
||||
@classmethod
|
||||
@@ -68,6 +90,10 @@ class PostProcessor(object):
|
||||
def set_downloader(self, downloader):
|
||||
"""Sets the downloader for this PP."""
|
||||
self._downloader = downloader
|
||||
if not downloader:
|
||||
return
|
||||
for ph in downloader._postprocessor_hooks:
|
||||
self.add_progress_hook(ph)
|
||||
|
||||
@staticmethod
|
||||
def _restrict_to(*, video=True, audio=True, images=True):
|
||||
@@ -115,6 +141,39 @@ class PostProcessor(object):
|
||||
return _configuration_args(
|
||||
self.pp_key(), self.get_param('postprocessor_args'), exe, *args, **kwargs)
|
||||
|
||||
def _hook_progress(self, status, info_dict):
|
||||
if not self._progress_hooks:
|
||||
return
|
||||
info_dict = dict(info_dict)
|
||||
for key in ('__original_infodict', '__postprocessors'):
|
||||
info_dict.pop(key, None)
|
||||
status.update({
|
||||
'info_dict': copy.deepcopy(info_dict),
|
||||
'postprocessor': self.pp_key(),
|
||||
})
|
||||
for ph in self._progress_hooks:
|
||||
ph(status)
|
||||
|
||||
def add_progress_hook(self, ph):
|
||||
# See YoutubeDl.py (search for postprocessor_hooks) for a description of this interface
|
||||
self._progress_hooks.append(ph)
|
||||
|
||||
def report_progress(self, s):
|
||||
s['_default_template'] = '%(postprocessor)s %(status)s' % s
|
||||
|
||||
progress_dict = s.copy()
|
||||
progress_dict.pop('info_dict')
|
||||
progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
|
||||
|
||||
progress_template = self.get_param('progress_template', {})
|
||||
tmpl = progress_template.get('postprocess')
|
||||
if tmpl:
|
||||
self._downloader.to_stdout(self._downloader.evaluate_outtmpl(tmpl, progress_dict))
|
||||
|
||||
self._downloader.to_console_title(self._downloader.evaluate_outtmpl(
|
||||
progress_template.get('postprocess-title') or 'yt-dlp %(progress._default_template)s',
|
||||
progress_dict))
|
||||
|
||||
|
||||
class AudioConversionError(PostProcessingError):
|
||||
pass
|
||||
|
||||
@@ -262,7 +262,7 @@ class FFmpegPostProcessor(PostProcessor):
|
||||
oldest_mtime = min(
|
||||
os.stat(encodeFilename(path)).st_mtime for path, _ in input_path_opts if path)
|
||||
|
||||
cmd = [encodeFilename(self.executable, True), encodeArgument('-y')]
|
||||
cmd = [encodeFilename(self.executable, True), encodeArgument('-y'), encodeArgument('-probesize'), encodeArgument('max')]
|
||||
# avconv does not have repeat option
|
||||
if self.basename == 'ffmpeg':
|
||||
cmd += [encodeArgument('-loglevel'), encodeArgument('repeat+info')]
|
||||
@@ -478,7 +478,7 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor):
|
||||
class FFmpegVideoConvertorPP(FFmpegPostProcessor):
|
||||
SUPPORTED_EXTS = ('mp4', 'mkv', 'flv', 'webm', 'mov', 'avi', 'mp3', 'mka', 'm4a', 'ogg', 'opus')
|
||||
FORMAT_RE = re.compile(r'{0}(?:/{0})*$'.format(r'(?:\w+>)?(?:%s)' % '|'.join(SUPPORTED_EXTS)))
|
||||
_action = 'converting'
|
||||
_ACTION = 'converting'
|
||||
|
||||
def __init__(self, downloader=None, preferedformat=None):
|
||||
super(FFmpegVideoConvertorPP, self).__init__(downloader)
|
||||
@@ -497,29 +497,28 @@ class FFmpegVideoConvertorPP(FFmpegPostProcessor):
|
||||
return []
|
||||
|
||||
@PostProcessor._restrict_to(images=False)
|
||||
def run(self, information):
|
||||
path, source_ext = information['filepath'], information['ext'].lower()
|
||||
def run(self, info):
|
||||
filename, source_ext = info['filepath'], info['ext'].lower()
|
||||
target_ext = self._target_ext(source_ext)
|
||||
_skip_msg = (
|
||||
'could not find a mapping for %s' if not target_ext
|
||||
else 'already is in target format %s' if source_ext == target_ext
|
||||
f'could not find a mapping for {source_ext}' if not target_ext
|
||||
else f'already is in target format {source_ext}' if source_ext == target_ext
|
||||
else None)
|
||||
if _skip_msg:
|
||||
self.to_screen('Not %s media file "%s"; %s' % (self._action, path, _skip_msg % source_ext))
|
||||
return [], information
|
||||
self.to_screen(f'Not {self._ACTION} media file {filename!r}; {_skip_msg}')
|
||||
return [], info
|
||||
|
||||
prefix, sep, oldext = path.rpartition('.')
|
||||
outpath = prefix + sep + target_ext
|
||||
self.to_screen('%s video from %s to %s; Destination: %s' % (self._action.title(), source_ext, target_ext, outpath))
|
||||
self.run_ffmpeg(path, outpath, self._options(target_ext))
|
||||
outpath = replace_extension(filename, target_ext, source_ext)
|
||||
self.to_screen(f'{self._ACTION.title()} video from {source_ext} to {target_ext}; Destination: {outpath}')
|
||||
self.run_ffmpeg(filename, outpath, self._options(target_ext))
|
||||
|
||||
information['filepath'] = outpath
|
||||
information['format'] = information['ext'] = target_ext
|
||||
return [path], information
|
||||
info['filepath'] = outpath
|
||||
info['format'] = info['ext'] = target_ext
|
||||
return [filename], info
|
||||
|
||||
|
||||
class FFmpegVideoRemuxerPP(FFmpegVideoConvertorPP):
|
||||
_action = 'remuxing'
|
||||
_ACTION = 'remuxing'
|
||||
|
||||
@staticmethod
|
||||
def _options(target_ext):
|
||||
@@ -545,7 +544,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
|
||||
return [], information
|
||||
|
||||
filename = information['filepath']
|
||||
if self._duration_mismatch(
|
||||
if information.get('duration') and self._duration_mismatch(
|
||||
self._get_real_video_duration(information, False), information['duration']):
|
||||
self.to_screen(f'Skipping {self.pp_key()} since the real and expected durations mismatch')
|
||||
return [], information
|
||||
@@ -556,7 +555,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
|
||||
mp4_ass_warn = False
|
||||
|
||||
for lang, sub_info in subtitles.items():
|
||||
if not os.path.exists(information.get('filepath', '')):
|
||||
if not os.path.exists(sub_info.get('filepath', '')):
|
||||
self.report_warning(f'Skipping embedding {lang} subtitle because the file is missing')
|
||||
continue
|
||||
sub_ext = sub_info['ext']
|
||||
@@ -846,6 +845,9 @@ class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor):
|
||||
self.to_screen('Converting subtitles')
|
||||
sub_filenames = []
|
||||
for lang, sub in subs.items():
|
||||
if not os.path.exists(sub.get('filepath', '')):
|
||||
self.report_warning(f'Skipping embedding {lang} subtitle because the file is missing')
|
||||
continue
|
||||
ext = sub['ext']
|
||||
if ext == new_ext:
|
||||
self.to_screen('Subtitle file for %s is already in the requested format' % new_ext)
|
||||
|
||||
@@ -62,8 +62,7 @@ class MetadataParserPP(PostProcessor):
|
||||
|
||||
def interpretter(self, inp, out):
|
||||
def f(info):
|
||||
outtmpl, tmpl_dict = self._downloader.prepare_outtmpl(template, info)
|
||||
data_to_parse = self._downloader.escape_outtmpl(outtmpl) % tmpl_dict
|
||||
data_to_parse = self._downloader.evaluate_outtmpl(template, info)
|
||||
self.write_debug(f'Searching for {out_re.pattern!r} in {template!r}')
|
||||
match = out_re.search(data_to_parse)
|
||||
if match is None:
|
||||
|
||||
@@ -292,8 +292,7 @@ class ModifyChaptersPP(FFmpegPostProcessor):
|
||||
'name': SponsorBlockPP.CATEGORIES[category],
|
||||
'category_names': [SponsorBlockPP.CATEGORIES[c] for c in cats]
|
||||
})
|
||||
outtmpl, tmpl_dict = self._downloader.prepare_outtmpl(self._sponsorblock_chapter_title, c)
|
||||
c['title'] = self._downloader.escape_outtmpl(outtmpl) % tmpl_dict
|
||||
c['title'] = self._downloader.evaluate_outtmpl(self._sponsorblock_chapter_title, c)
|
||||
# Merge identically named sponsors.
|
||||
if (new_chapters and 'categories' in new_chapters[-1]
|
||||
and new_chapters[-1]['title'] == c['title']):
|
||||
|
||||
@@ -4,7 +4,7 @@ from hashlib import sha256
|
||||
|
||||
from .ffmpeg import FFmpegPostProcessor
|
||||
from ..compat import compat_urllib_parse_urlencode, compat_HTTPError
|
||||
from ..utils import PostProcessingError, sanitized_Request
|
||||
from ..utils import PostProcessingError, network_exceptions, sanitized_Request
|
||||
|
||||
|
||||
class SponsorBlockPP(FFmpegPostProcessor):
|
||||
@@ -88,9 +88,9 @@ class SponsorBlockPP(FFmpegPostProcessor):
|
||||
self.write_debug(f'SponsorBlock query: {url}')
|
||||
try:
|
||||
rsp = self._downloader.urlopen(sanitized_Request(url))
|
||||
except compat_HTTPError as e:
|
||||
if e.code == 404:
|
||||
except network_exceptions as e:
|
||||
if isinstance(e, compat_HTTPError) and e.code == 404:
|
||||
return []
|
||||
raise PostProcessingError(f'Error communicating with SponsorBlock API - {e}')
|
||||
raise PostProcessingError(f'Unable to communicate with SponsorBlock API - {e}')
|
||||
|
||||
return json.loads(rsp.read().decode(rsp.info().get_param('charset') or 'utf-8'))
|
||||
|
||||
+32
-23
@@ -32,10 +32,12 @@ def rsa_verify(message, signature, key):
|
||||
|
||||
|
||||
def detect_variant():
|
||||
if hasattr(sys, 'frozen') and getattr(sys, '_MEIPASS', None):
|
||||
if sys._MEIPASS == os.path.dirname(sys.executable):
|
||||
return 'dir'
|
||||
return 'exe'
|
||||
if hasattr(sys, 'frozen'):
|
||||
if getattr(sys, '_MEIPASS', None):
|
||||
if sys._MEIPASS == os.path.dirname(sys.executable):
|
||||
return 'dir'
|
||||
return 'exe'
|
||||
return 'py2exe'
|
||||
elif isinstance(globals().get('__loader__'), zipimporter):
|
||||
return 'zip'
|
||||
elif os.path.basename(sys.argv[0]) == '__main__.py':
|
||||
@@ -43,6 +45,20 @@ def detect_variant():
|
||||
return 'unknown'
|
||||
|
||||
|
||||
_NON_UPDATEABLE_REASONS = {
|
||||
'exe': None,
|
||||
'zip': None,
|
||||
'dir': 'Auto-update is not supported for unpackaged windows executable. Re-download the latest release',
|
||||
'py2exe': 'There is no official release for py2exe executable. Build it again with the latest source code',
|
||||
'source': 'You cannot update when running from source code',
|
||||
'unknown': 'It looks like you installed yt-dlp with a package manager, pip, setup.py or a tarball. Use that to update',
|
||||
}
|
||||
|
||||
|
||||
def is_non_updateable():
|
||||
return _NON_UPDATEABLE_REASONS.get(detect_variant(), _NON_UPDATEABLE_REASONS['unknown'])
|
||||
|
||||
|
||||
def update_self(to_screen, verbose, opener):
|
||||
''' Exists for backward compatibility. Use run_update(ydl) instead '''
|
||||
|
||||
@@ -99,23 +115,6 @@ def run_update(ydl):
|
||||
h.update(mv[:n])
|
||||
return h.hexdigest()
|
||||
|
||||
ERRORS = {
|
||||
'exe': None,
|
||||
'zip': None,
|
||||
'dir': 'Auto-update is not supported for unpackaged windows executable. Re-download the latest release',
|
||||
'source': 'You cannot update when running from source code',
|
||||
'unknown': 'It looks like you installed yt-dlp with a package manager, pip, setup.py or a tarball. Use that to update',
|
||||
}
|
||||
err = ERRORS.get(detect_variant(), ERRORS['unknown'])
|
||||
if err:
|
||||
return report_error(err, expected=True)
|
||||
|
||||
# sys.executable is set to the full pathname of the exe-file for py2exe
|
||||
# though symlinks are not followed so that we need to do this manually
|
||||
# with help of realpath
|
||||
filename = compat_realpath(sys.executable if hasattr(sys, 'frozen') else sys.argv[0])
|
||||
ydl.to_screen('Current Build Hash %s' % calc_sha256sum(filename))
|
||||
|
||||
# Download and check versions info
|
||||
try:
|
||||
version_info = ydl._opener.open(JSON_URL).read().decode('utf-8')
|
||||
@@ -128,10 +127,20 @@ def run_update(ydl):
|
||||
|
||||
version_id = version_info['tag_name']
|
||||
if version_tuple(__version__) >= version_tuple(version_id):
|
||||
ydl.to_screen('yt-dlp is up to date (%s)' % __version__)
|
||||
ydl.to_screen(f'yt-dlp is up to date ({__version__})')
|
||||
return
|
||||
|
||||
ydl.to_screen('Updating to version ' + version_id + ' ...')
|
||||
err = is_non_updateable()
|
||||
if err:
|
||||
ydl.to_screen(f'Latest version: {version_id}, Current version: {__version__}')
|
||||
return report_error(err, expected=True)
|
||||
|
||||
# sys.executable is set to the full pathname of the exe-file for py2exe
|
||||
# though symlinks are not followed so that we need to do this manually
|
||||
# with help of realpath
|
||||
filename = compat_realpath(sys.executable if hasattr(sys, 'frozen') else sys.argv[0])
|
||||
ydl.to_screen(f'Current version {__version__}; Build Hash {calc_sha256sum(filename)}')
|
||||
ydl.to_screen(f'Updating to version {version_id} ...')
|
||||
|
||||
version_labels = {
|
||||
'zip_3': '',
|
||||
|
||||
+115
-40
@@ -2099,7 +2099,9 @@ def sanitize_filename(s, restricted=False, is_id=False):
|
||||
def replace_insane(char):
|
||||
if restricted and char in ACCENT_CHARS:
|
||||
return ACCENT_CHARS[char]
|
||||
if char == '?' or ord(char) < 32 or ord(char) == 127:
|
||||
elif not restricted and char == '\n':
|
||||
return ' '
|
||||
elif char == '?' or ord(char) < 32 or ord(char) == 127:
|
||||
return ''
|
||||
elif char == '"':
|
||||
return '' if restricted else '\''
|
||||
@@ -2350,29 +2352,42 @@ def formatSeconds(secs, delim=':', msec=False):
|
||||
return '%s.%03d' % (ret, secs % 1) if msec else ret
|
||||
|
||||
|
||||
def make_HTTPS_handler(params, **kwargs):
|
||||
opts_no_check_certificate = params.get('nocheckcertificate', False)
|
||||
if hasattr(ssl, 'create_default_context'): # Python >= 3.4 or 2.7.9
|
||||
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
||||
if opts_no_check_certificate:
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
def _ssl_load_windows_store_certs(ssl_context, storename):
|
||||
# Code adapted from _load_windows_store_certs in https://github.com/python/cpython/blob/main/Lib/ssl.py
|
||||
try:
|
||||
certs = [cert for cert, encoding, trust in ssl.enum_certificates(storename)
|
||||
if encoding == 'x509_asn' and (
|
||||
trust is True or ssl.Purpose.SERVER_AUTH.oid in trust)]
|
||||
except PermissionError:
|
||||
return
|
||||
for cert in certs:
|
||||
try:
|
||||
return YoutubeDLHTTPSHandler(params, context=context, **kwargs)
|
||||
except TypeError:
|
||||
# Python 2.7.8
|
||||
# (create_default_context present but HTTPSHandler has no context=)
|
||||
ssl_context.load_verify_locations(cadata=cert)
|
||||
except ssl.SSLError:
|
||||
pass
|
||||
|
||||
if sys.version_info < (3, 2):
|
||||
return YoutubeDLHTTPSHandler(params, **kwargs)
|
||||
else: # Python < 3.4
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
|
||||
context.verify_mode = (ssl.CERT_NONE
|
||||
if opts_no_check_certificate
|
||||
else ssl.CERT_REQUIRED)
|
||||
context.set_default_verify_paths()
|
||||
return YoutubeDLHTTPSHandler(params, context=context, **kwargs)
|
||||
|
||||
def make_HTTPS_handler(params, **kwargs):
|
||||
opts_check_certificate = not params.get('nocheckcertificate')
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
context.check_hostname = opts_check_certificate
|
||||
context.verify_mode = ssl.CERT_REQUIRED if opts_check_certificate else ssl.CERT_NONE
|
||||
if opts_check_certificate:
|
||||
try:
|
||||
context.load_default_certs()
|
||||
# Work around the issue in load_default_certs when there are bad certificates. See:
|
||||
# https://github.com/yt-dlp/yt-dlp/issues/1060,
|
||||
# https://bugs.python.org/issue35665, https://bugs.python.org/issue45312
|
||||
except ssl.SSLError:
|
||||
# enum_certificates is not present in mingw python. See https://github.com/yt-dlp/yt-dlp/issues/1151
|
||||
if sys.platform == 'win32' and hasattr(ssl, 'enum_certificates'):
|
||||
# Create a new context to discard any certificates that were already loaded
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
context.check_hostname, context.verify_mode = True, ssl.CERT_REQUIRED
|
||||
for storename in ('CA', 'ROOT'):
|
||||
_ssl_load_windows_store_certs(context, storename)
|
||||
context.set_default_verify_paths()
|
||||
return YoutubeDLHTTPSHandler(params, context=context, **kwargs)
|
||||
|
||||
|
||||
def bug_reports_message(before=';'):
|
||||
@@ -4474,12 +4489,12 @@ OUTTMPL_TYPES = {
|
||||
STR_FORMAT_RE_TMPL = r'''(?x)
|
||||
(?<!%)(?P<prefix>(?:%%)*)
|
||||
%
|
||||
(?P<has_key>\((?P<key>{0})\))? # mapping key
|
||||
(?P<has_key>\((?P<key>{0})\))?
|
||||
(?P<format>
|
||||
(?:[#0\-+ ]+)? # conversion flags (optional)
|
||||
(?:\d+)? # minimum field width (optional)
|
||||
(?:\.\d+)? # precision (optional)
|
||||
[hlL]? # length modifier (optional)
|
||||
(?P<conversion>[#0\-+ ]+)?
|
||||
(?P<min_width>\d+)?
|
||||
(?P<precision>\.\d+)?
|
||||
(?P<len_mod>[hlL])? # unused in python
|
||||
{1} # conversion type
|
||||
)
|
||||
'''
|
||||
@@ -4513,11 +4528,10 @@ def is_outdated_version(version, limit, assume_new=True):
|
||||
|
||||
def ytdl_is_updateable():
|
||||
""" Returns if yt-dlp can be updated with -U """
|
||||
return False
|
||||
|
||||
from zipimport import zipimporter
|
||||
from .update import is_non_updateable
|
||||
|
||||
return isinstance(globals().get('__loader__'), zipimporter) or hasattr(sys, 'frozen')
|
||||
return not is_non_updateable()
|
||||
|
||||
|
||||
def args_to_str(args):
|
||||
@@ -4538,20 +4552,24 @@ def mimetype2ext(mt):
|
||||
if mt is None:
|
||||
return None
|
||||
|
||||
ext = {
|
||||
mt, _, params = mt.partition(';')
|
||||
mt = mt.strip()
|
||||
|
||||
FULL_MAP = {
|
||||
'audio/mp4': 'm4a',
|
||||
# Per RFC 3003, audio/mpeg can be .mp1, .mp2 or .mp3. Here use .mp3 as
|
||||
# it's the most popular one
|
||||
'audio/mpeg': 'mp3',
|
||||
'audio/x-wav': 'wav',
|
||||
}.get(mt)
|
||||
'audio/wav': 'wav',
|
||||
'audio/wave': 'wav',
|
||||
}
|
||||
|
||||
ext = FULL_MAP.get(mt)
|
||||
if ext is not None:
|
||||
return ext
|
||||
|
||||
_, _, res = mt.rpartition('/')
|
||||
res = res.split(';')[0].strip().lower()
|
||||
|
||||
return {
|
||||
SUBTYPE_MAP = {
|
||||
'3gpp': '3gp',
|
||||
'smptett+xml': 'tt',
|
||||
'ttaf+xml': 'dfxp',
|
||||
@@ -4570,7 +4588,28 @@ def mimetype2ext(mt):
|
||||
'quicktime': 'mov',
|
||||
'mp2t': 'ts',
|
||||
'x-wav': 'wav',
|
||||
}.get(res, res)
|
||||
'filmstrip+json': 'fs',
|
||||
'svg+xml': 'svg',
|
||||
}
|
||||
|
||||
_, _, subtype = mt.rpartition('/')
|
||||
ext = SUBTYPE_MAP.get(subtype.lower())
|
||||
if ext is not None:
|
||||
return ext
|
||||
|
||||
SUFFIX_MAP = {
|
||||
'json': 'json',
|
||||
'xml': 'xml',
|
||||
'zip': 'zip',
|
||||
'gzip': 'gz',
|
||||
}
|
||||
|
||||
_, _, suffix = subtype.partition('+')
|
||||
ext = SUFFIX_MAP.get(suffix)
|
||||
if ext is not None:
|
||||
return ext
|
||||
|
||||
return subtype.replace('+', '.')
|
||||
|
||||
|
||||
def parse_codecs(codecs_str):
|
||||
@@ -6270,7 +6309,7 @@ def get_executable_path():
|
||||
|
||||
def load_plugins(name, suffix, namespace):
|
||||
plugin_info = [None]
|
||||
classes = []
|
||||
classes = {}
|
||||
try:
|
||||
plugin_info = imp.find_module(
|
||||
name, [os.path.join(get_executable_path(), 'ytdlp_plugins')])
|
||||
@@ -6281,8 +6320,7 @@ def load_plugins(name, suffix, namespace):
|
||||
if not name.endswith(suffix):
|
||||
continue
|
||||
klass = getattr(plugins, name)
|
||||
classes.append(klass)
|
||||
namespace[name] = klass
|
||||
classes[name] = namespace[name] = klass
|
||||
except ImportError:
|
||||
pass
|
||||
finally:
|
||||
@@ -6297,7 +6335,9 @@ def traverse_obj(
|
||||
''' Traverse nested list/dict/tuple
|
||||
@param path_list A list of paths which are checked one by one.
|
||||
Each path is a list of keys where each key is a string,
|
||||
a tuple of strings or "...". When a tuple is given,
|
||||
a function, a tuple of strings or "...".
|
||||
When a fuction is given, it takes the key as argument and
|
||||
returns whether the key matches or not. When a tuple is given,
|
||||
all the keys given in the tuple are traversed, and
|
||||
"..." traverses all the keys in the object
|
||||
@param default Default value to return
|
||||
@@ -6330,6 +6370,18 @@ def traverse_obj(
|
||||
_current_depth += 1
|
||||
depth = max(depth, _current_depth)
|
||||
return [_traverse_obj(inner_obj, path[i + 1:], _current_depth) for inner_obj in obj]
|
||||
elif callable(key):
|
||||
if isinstance(obj, (list, tuple, LazyList)):
|
||||
obj = enumerate(obj)
|
||||
elif isinstance(obj, dict):
|
||||
obj = obj.items()
|
||||
else:
|
||||
if not traverse_string:
|
||||
return None
|
||||
obj = str(obj)
|
||||
_current_depth += 1
|
||||
depth = max(depth, _current_depth)
|
||||
return [_traverse_obj(v, path[i + 1:], _current_depth) for k, v in obj if key(k)]
|
||||
elif isinstance(obj, dict) and not (is_user_input and key == ':'):
|
||||
obj = (obj.get(key) if casesense or (key in obj)
|
||||
else next((v for k, v in obj.items() if _lower(k) == key), None))
|
||||
@@ -6402,3 +6454,26 @@ def jwt_encode_hs256(payload_data, key, headers={}):
|
||||
signature_b64 = base64.b64encode(h.digest())
|
||||
token = header_b64 + b'.' + payload_b64 + b'.' + signature_b64
|
||||
return token
|
||||
|
||||
|
||||
def supports_terminal_sequences(stream):
|
||||
if compat_os_name == 'nt':
|
||||
if get_windows_version() < (10, ):
|
||||
return False
|
||||
elif not os.getenv('TERM'):
|
||||
return False
|
||||
try:
|
||||
return stream.isatty()
|
||||
except BaseException:
|
||||
return False
|
||||
|
||||
|
||||
TERMINAL_SEQUENCES = {
|
||||
'DOWN': '\n',
|
||||
'UP': '\x1b[A',
|
||||
'ERASE_LINE': '\x1b[K',
|
||||
'RED': '\033[0;31m',
|
||||
'YELLOW': '\033[0;33m',
|
||||
'BLUE': '\033[0;34m',
|
||||
'RESET_STYLE': '\033[0m',
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
__version__ = '2021.09.25'
|
||||
__version__ = '2021.10.09'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# flake8: noqa
|
||||
# flake8: noqa: F401
|
||||
|
||||
# ℹ️ The imported name must end in "IE"
|
||||
from .sample import SamplePluginIE
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
# coding: utf-8
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# ⚠ Don't use relative imports
|
||||
from yt_dlp.extractor.common import InfoExtractor
|
||||
|
||||
|
||||
# ℹ️ Instructions on making extractors can be found at:
|
||||
# 🔗 https://github.com/ytdl-org/youtube-dl#adding-support-for-a-new-site
|
||||
# 🔗 https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#adding-support-for-a-new-site
|
||||
|
||||
class SamplePluginIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# flake8: noqa: F401
|
||||
|
||||
# ℹ️ The imported name must end in "PP" and is the name to be used in --use-postprocessor
|
||||
from .sample import SamplePluginPP
|
||||
@@ -0,0 +1,23 @@
|
||||
# coding: utf-8
|
||||
|
||||
# ⚠ Don't use relative imports
|
||||
from yt_dlp.postprocessor.common import PostProcessor
|
||||
|
||||
|
||||
# ℹ️ See the docstring of yt_dlp.postprocessor.common.PostProcessor
|
||||
class SamplePluginPP(PostProcessor):
|
||||
def __init__(self, downloader=None, **kwargs):
|
||||
# ⚠ Only kwargs can be passed from the CLI, and all argument values will be string
|
||||
# Also, "downloader", "when" and "key" are reserved names
|
||||
super().__init__(downloader)
|
||||
self._kwargs = kwargs
|
||||
|
||||
# ℹ️ See docstring of yt_dlp.postprocessor.common.PostProcessor.run
|
||||
def run(self, info):
|
||||
filepath = info.get('filepath')
|
||||
if filepath: # PP was called after download (default)
|
||||
self.to_screen(f'Post-processed {filepath!r} with {self._kwargs}')
|
||||
else: # PP was called before actual download
|
||||
filepath = info.get('_filename')
|
||||
self.to_screen(f'Pre-processed {filepath!r} with {self._kwargs}')
|
||||
return [], info # return list_of_files_to_delete, info_dict
|
||||
Reference in New Issue
Block a user