mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-01-13 02:11:18 +00:00
Compare commits
192 Commits
2021.09.02
...
2021.10.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8063de5109 | ||
|
|
dec0d56fa9 | ||
|
|
21186af70a | ||
|
|
84999521c8 | ||
|
|
d1d5c08f29 | ||
|
|
2e01ba6218 | ||
|
|
c9652aa418 | ||
|
|
91b6c884c9 | ||
|
|
28fe35b4e3 | ||
|
|
aa9a92fdbb | ||
|
|
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 | ||
|
|
1fed277349 | ||
|
|
0ef787d773 | ||
|
|
a5de4099cb | ||
|
|
ff1c7fc9d3 | ||
|
|
600e900300 | ||
|
|
20b91b9b63 | ||
|
|
4c88ff87fc | ||
|
|
e27cc5d864 | ||
|
|
eb6d4ad1ca | ||
|
|
99e9e001de | ||
|
|
51ff9ca0b0 | ||
|
|
b19404591a | ||
|
|
1f8471e22c | ||
|
|
77c4a9ef68 | ||
|
|
8f70b0b82f | ||
|
|
be867b03f5 | ||
|
|
1813a6ccd4 | ||
|
|
8100c77223 | ||
|
|
9ada988bfc | ||
|
|
d1a7768432 | ||
|
|
49fa4d9af7 | ||
|
|
ee2b3563f3 | ||
|
|
bdc196a444 | ||
|
|
388bc4a640 | ||
|
|
50eff38c1c | ||
|
|
4be9dbdc24 | ||
|
|
a21e0ab1a1 | ||
|
|
a76e2e0f88 | ||
|
|
bd50a52b0d | ||
|
|
c12977bdc4 | ||
|
|
f6d8776d34 | ||
|
|
d806c9fd97 | ||
|
|
5e3f2f8fc4 | ||
|
|
1009f67c2a | ||
|
|
bd6f722de8 | ||
|
|
d9d8b85747 | ||
|
|
daf7ac2b92 | ||
|
|
96933fc1b6 | ||
|
|
0d32e124c6 | ||
|
|
cb2ec90e91 | ||
|
|
3cd786dbd7 | ||
|
|
1b629e1b4c | ||
|
|
8f8e8eba24 | ||
|
|
09906f554d | ||
|
|
a63d9bd0b0 | ||
|
|
f137e4c27c | ||
|
|
4762621925 | ||
|
|
57aa7b8511 | ||
|
|
9c1c3ec016 | ||
|
|
f9cc0161e6 | ||
|
|
c6af2dd8e5 | ||
|
|
7738bd3272 | ||
|
|
7c37ff97d3 | ||
|
|
d47f46e17e | ||
|
|
298bf1d275 | ||
|
|
d1b39ad844 | ||
|
|
edf65256aa | ||
|
|
7303f84abe | ||
|
|
f5aa5cfbff | ||
|
|
f1f6ca78b4 | ||
|
|
2fac2e9136 | ||
|
|
23dd2d9a32 | ||
|
|
b89378a69a | ||
|
|
0001fcb586 | ||
|
|
c589c1d395 | ||
|
|
f7590d4764 | ||
|
|
dbf7eca917 | ||
|
|
d21bba7853 | ||
|
|
a8cb7eca61 | ||
|
|
92790da2bb | ||
|
|
b5a39ed43b | ||
|
|
cc33cc4395 | ||
|
|
1722099ded | ||
|
|
40b18348e7 | ||
|
|
e9a30b181e | ||
|
|
9c95ac677e | ||
|
|
ea706726d6 | ||
|
|
f60990ddfc | ||
|
|
ad226b1dc9 | ||
|
|
ca46b94134 | ||
|
|
67ad7759af | ||
|
|
d5fe04f5c7 | ||
|
|
03c862794f | ||
|
|
0fd6661edb | ||
|
|
02c7ae8104 | ||
|
|
16f7e6be3a | ||
|
|
ffecd3034b | ||
|
|
1c5ce74c04 | ||
|
|
81a136b80f | ||
|
|
eab3f867e2 | ||
|
|
a7e999beec | ||
|
|
71407b3eca | ||
|
|
dc9de9cbd2 | ||
|
|
92ddaa415e | ||
|
|
b6de707d13 | ||
|
|
bccdbd22d5 | ||
|
|
bd9ff55bcd | ||
|
|
526d74ec5a | ||
|
|
e04a1ff92e | ||
|
|
aa6c25309a | ||
|
|
d98b006b85 | ||
|
|
265a7a8ee5 | ||
|
|
826446bd82 | ||
|
|
bc79491368 | ||
|
|
421ddcb8b4 | ||
|
|
c0ac49bcca | ||
|
|
02def2714c | ||
|
|
f9be9cb9fd | ||
|
|
4614bc22c1 | ||
|
|
8e5fecc88c | ||
|
|
165efb823b | ||
|
|
dd594deb2a | ||
|
|
409e18286e | ||
|
|
8113999995 | ||
|
|
8026e50152 |
19
.github/ISSUE_TEMPLATE/1_broken_site.md
vendored
19
.github/ISSUE_TEMPLATE/1_broken_site.md
vendored
@@ -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.08.10. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.10.10. 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.08.10**
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.10.10**
|
||||
- [ ] 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.08.10
|
||||
[debug] yt-dlp version 2021.10.10
|
||||
[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: {}
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/2_site_support_request.md
vendored
17
.github/ISSUE_TEMPLATE/2_site_support_request.md
vendored
@@ -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.08.10. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.10.10. 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.08.10**
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.10.10**
|
||||
- [ ] 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
|
||||
|
||||
15
.github/ISSUE_TEMPLATE/3_site_feature_request.md
vendored
15
.github/ISSUE_TEMPLATE/3_site_feature_request.md
vendored
@@ -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.08.10. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- 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.10. 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.08.10**
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.10.10**
|
||||
- [ ] 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
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/4_bug_report.md
vendored
21
.github/ISSUE_TEMPLATE/4_bug_report.md
vendored
@@ -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.08.10. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- First of, make sure you are using the latest version of yt-dlp. Run `yt-dlp --version` and ensure your version is 2021.10.10. 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.08.10**
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.10.10**
|
||||
- [ ] 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.08.10
|
||||
[debug] yt-dlp version 2021.10.10
|
||||
[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: {}
|
||||
|
||||
13
.github/ISSUE_TEMPLATE/5_feature_request.md
vendored
13
.github/ISSUE_TEMPLATE/5_feature_request.md
vendored
@@ -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.08.10. If it's not, see https://github.com/yt-dlp/yt-dlp on how to update. Issues with outdated version will be REJECTED.
|
||||
- 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.10. 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.08.10**
|
||||
- [ ] I've verified that I'm running yt-dlp version **2021.10.10**
|
||||
- [ ] 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
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/6_question.md
vendored
11
.github/ISSUE_TEMPLATE/6_question.md
vendored
@@ -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
|
||||
|
||||
15
.github/ISSUE_TEMPLATE_tmpl/1_broken_site.md
vendored
15
.github/ISSUE_TEMPLATE_tmpl/1_broken_site.md
vendored
@@ -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
|
||||
|
||||
17
.github/ISSUE_TEMPLATE_tmpl/4_bug_report.md
vendored
17
.github/ISSUE_TEMPLATE_tmpl/4_bug_report.md
vendored
@@ -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
|
||||
|
||||
11
.github/ISSUE_TEMPLATE_tmpl/5_feature_request.md
vendored
11
.github/ISSUE_TEMPLATE_tmpl/5_feature_request.md
vendored
@@ -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
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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)
|
||||
|
||||
|
||||
190
.github/workflows/build.yml
vendored
190
.github/workflows/build.yml
vendored
@@ -12,11 +12,15 @@ jobs:
|
||||
outputs:
|
||||
ytdlp_version: ${{ steps.bump_version.outputs.ytdlp_version }}
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
sha256_unix: ${{ steps.sha256_file.outputs.sha256_unix }}
|
||||
sha512_unix: ${{ steps.sha512_file.outputs.sha512_unix }}
|
||||
sha256_bin: ${{ steps.sha256_bin.outputs.sha256_bin }}
|
||||
sha512_bin: ${{ steps.sha512_bin.outputs.sha512_bin }}
|
||||
sha256_tar: ${{ steps.sha256_tar.outputs.sha256_tar }}
|
||||
sha512_tar: ${{ steps.sha512_tar.outputs.sha512_tar }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
@@ -25,11 +29,76 @@ jobs:
|
||||
run: sudo apt-get -y install zip pandoc man
|
||||
- name: Bump version
|
||||
id: bump_version
|
||||
run: python devscripts/update-version.py
|
||||
run: |
|
||||
python devscripts/update-version.py
|
||||
make issuetemplates
|
||||
- name: Print version
|
||||
run: echo "${{ steps.bump_version.outputs.ytdlp_version }}"
|
||||
- name: Update master
|
||||
id: push_update
|
||||
run: |
|
||||
git config --global user.email "${{ github.event.pusher.email }}"
|
||||
git config --global user.name "${{ github.event.pusher.name }}"
|
||||
git add -u
|
||||
git commit -m "[version] update" -m ":ci skip all"
|
||||
git pull --rebase origin ${{ github.event.repository.master_branch }}
|
||||
git push origin ${{ github.event.ref }}:${{ github.event.repository.master_branch }}
|
||||
echo ::set-output name=head_sha::$(git rev-parse HEAD)
|
||||
- name: Get Changelog
|
||||
id: get_changelog
|
||||
run: |
|
||||
changelog=$(cat Changelog.md | grep -oPz '(?s)(?<=### ${{ steps.bump_version.outputs.ytdlp_version }}\n{2}).+?(?=\n{2,3}###)') || true
|
||||
echo "changelog<<EOF" >> $GITHUB_ENV
|
||||
echo "$changelog" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
- name: Run Make
|
||||
run: make all tar
|
||||
- name: Get SHA2-256SUMS for yt-dlp
|
||||
id: sha256_bin
|
||||
run: echo "::set-output name=sha256_bin::$(sha256sum yt-dlp | awk '{print $1}')"
|
||||
- name: Get SHA2-256SUMS for yt-dlp.tar.gz
|
||||
id: sha256_tar
|
||||
run: echo "::set-output name=sha256_tar::$(sha256sum yt-dlp.tar.gz | awk '{print $1}')"
|
||||
- name: Get SHA2-512SUMS for yt-dlp
|
||||
id: sha512_bin
|
||||
run: echo "::set-output name=sha512_bin::$(sha512sum yt-dlp | awk '{print $1}')"
|
||||
- name: Get SHA2-512SUMS for yt-dlp.tar.gz
|
||||
id: sha512_tar
|
||||
run: echo "::set-output name=sha512_tar::$(sha512sum yt-dlp.tar.gz | awk '{print $1}')"
|
||||
- name: Install dependencies for pypi
|
||||
env:
|
||||
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||
if: "env.PYPI_TOKEN != ''"
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
- name: Build and publish on pypi
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
||||
if: "env.TWINE_PASSWORD != ''"
|
||||
run: |
|
||||
rm -rf dist/*
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
- name: Install SSH private key
|
||||
env:
|
||||
BREW_TOKEN: ${{ secrets.BREW_TOKEN }}
|
||||
if: "env.BREW_TOKEN != ''"
|
||||
uses: webfactory/ssh-agent@v0.5.3
|
||||
with:
|
||||
ssh-private-key: ${{ env.BREW_TOKEN }}
|
||||
- name: Update Homebrew Formulae
|
||||
env:
|
||||
BREW_TOKEN: ${{ secrets.BREW_TOKEN }}
|
||||
if: "env.BREW_TOKEN != ''"
|
||||
run: |
|
||||
git clone git@github.com:yt-dlp/homebrew-taps taps/
|
||||
python3 devscripts/update-formulae.py taps/Formula/yt-dlp.rb "${{ steps.bump_version.outputs.ytdlp_version }}"
|
||||
git -C taps/ config user.name github-actions
|
||||
git -C taps/ config user.email github-actions@example.com
|
||||
git -C taps/ commit -am 'yt-dlp: ${{ steps.bump_version.outputs.ytdlp_version }}'
|
||||
git -C taps/ push
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
@@ -38,9 +107,10 @@ jobs:
|
||||
with:
|
||||
tag_name: ${{ steps.bump_version.outputs.ytdlp_version }}
|
||||
release_name: yt-dlp ${{ steps.bump_version.outputs.ytdlp_version }}
|
||||
commitish: ${{ steps.push_update.outputs.head_sha }}
|
||||
body: |
|
||||
Changelog:
|
||||
PLACEHOLDER
|
||||
${{ env.changelog }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
- name: Upload yt-dlp Unix binary
|
||||
@@ -62,36 +132,16 @@ jobs:
|
||||
asset_path: ./yt-dlp.tar.gz
|
||||
asset_name: yt-dlp.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
- name: Get SHA2-256SUMS for yt-dlp
|
||||
id: sha256_file
|
||||
run: echo "::set-output name=sha256_unix::$(sha256sum yt-dlp | awk '{print $1}')"
|
||||
- name: Get SHA2-512SUMS for yt-dlp
|
||||
id: sha512_file
|
||||
run: echo "::set-output name=sha512_unix::$(sha512sum yt-dlp | awk '{print $1}')"
|
||||
- name: Install dependencies for pypi
|
||||
env:
|
||||
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||
if: "env.PYPI_TOKEN != ''"
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
- name: Build and publish on pypi
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
||||
if: "env.TWINE_PASSWORD != ''"
|
||||
run: |
|
||||
rm -rf dist/*
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
|
||||
build_windows:
|
||||
runs-on: windows-latest
|
||||
needs: build_unix
|
||||
|
||||
outputs:
|
||||
sha256_windows: ${{ steps.sha256_file_win.outputs.sha256_windows }}
|
||||
sha512_windows: ${{ steps.sha512_file_win.outputs.sha512_windows }}
|
||||
sha256_win: ${{ steps.sha256_win.outputs.sha256_win }}
|
||||
sha512_win: ${{ steps.sha512_win.outputs.sha512_win }}
|
||||
sha256_win_zip: ${{ steps.sha256_win_zip.outputs.sha256_win_zip }}
|
||||
sha512_win_zip: ${{ steps.sha512_win_zip.outputs.sha512_win_zip }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -104,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
|
||||
@@ -123,19 +173,41 @@ jobs:
|
||||
asset_name: yt-dlp.exe
|
||||
asset_content_type: application/vnd.microsoft.portable-executable
|
||||
- name: Get SHA2-256SUMS for yt-dlp.exe
|
||||
id: sha256_file_win
|
||||
run: echo "::set-output name=sha256_windows::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA256).Hash.ToLower())"
|
||||
id: sha256_win
|
||||
run: echo "::set-output name=sha256_win::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA256).Hash.ToLower())"
|
||||
- name: Get SHA2-512SUMS for yt-dlp.exe
|
||||
id: sha512_file_win
|
||||
run: echo "::set-output name=sha512_windows::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA512).Hash.ToLower())"
|
||||
id: sha512_win
|
||||
run: echo "::set-output name=sha512_win::$((Get-FileHash dist\yt-dlp.exe -Algorithm SHA512).Hash.ToLower())"
|
||||
- name: Run PyInstaller Script with --onedir
|
||||
run: python pyinst.py --onedir
|
||||
- uses: papeloto/action-zip@v1
|
||||
with:
|
||||
files: ./dist/yt-dlp
|
||||
dest: ./dist/yt-dlp.zip
|
||||
- name: Upload yt-dlp.zip Windows onedir
|
||||
id: upload-release-windows-zip
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.build_unix.outputs.upload_url }}
|
||||
asset_path: ./dist/yt-dlp.zip
|
||||
asset_name: yt-dlp.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Get SHA2-256SUMS for yt-dlp.zip
|
||||
id: sha256_win_zip
|
||||
run: echo "::set-output name=sha256_win_zip::$((Get-FileHash dist\yt-dlp.zip -Algorithm SHA256).Hash.ToLower())"
|
||||
- name: Get SHA2-512SUMS for yt-dlp.zip
|
||||
id: sha512_win_zip
|
||||
run: echo "::set-output name=sha512_win_zip::$((Get-FileHash dist\yt-dlp.zip -Algorithm SHA512).Hash.ToLower())"
|
||||
|
||||
build_windows32:
|
||||
runs-on: windows-latest
|
||||
needs: [build_unix, build_windows]
|
||||
|
||||
outputs:
|
||||
sha256_windows32: ${{ steps.sha256_file_win32.outputs.sha256_windows32 }}
|
||||
sha512_windows32: ${{ steps.sha512_file_win32.outputs.sha512_windows32 }}
|
||||
sha256_win32: ${{ steps.sha256_win32.outputs.sha256_win32 }}
|
||||
sha512_win32: ${{ steps.sha512_win32.outputs.sha512_win32 }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -148,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
|
||||
@@ -167,11 +239,11 @@ jobs:
|
||||
asset_name: yt-dlp_x86.exe
|
||||
asset_content_type: application/vnd.microsoft.portable-executable
|
||||
- name: Get SHA2-256SUMS for yt-dlp_x86.exe
|
||||
id: sha256_file_win32
|
||||
run: echo "::set-output name=sha256_windows32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA256).Hash.ToLower())"
|
||||
id: sha256_win32
|
||||
run: echo "::set-output name=sha256_win32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA256).Hash.ToLower())"
|
||||
- name: Get SHA2-512SUMS for yt-dlp_x86.exe
|
||||
id: sha512_file_win32
|
||||
run: echo "::set-output name=sha512_windows32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA512).Hash.ToLower())"
|
||||
id: sha512_win32
|
||||
run: echo "::set-output name=sha512_win32::$((Get-FileHash dist\yt-dlp_x86.exe -Algorithm SHA512).Hash.ToLower())"
|
||||
|
||||
finish:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -180,15 +252,17 @@ jobs:
|
||||
steps:
|
||||
- name: Make SHA2-256SUMS file
|
||||
env:
|
||||
SHA256_WINDOWS: ${{ needs.build_windows.outputs.sha256_windows }}
|
||||
SHA256_WINDOWS32: ${{ needs.build_windows32.outputs.sha256_windows32 }}
|
||||
SHA256_UNIX: ${{ needs.build_unix.outputs.sha256_unix }}
|
||||
YTDLP_VERSION: ${{ needs.build_unix.outputs.ytdlp_version }}
|
||||
SHA256_WIN: ${{ needs.build_windows.outputs.sha256_win }}
|
||||
SHA256_WIN_ZIP: ${{ needs.build_windows.outputs.sha256_win_zip }}
|
||||
SHA256_WIN32: ${{ needs.build_windows32.outputs.sha256_win32 }}
|
||||
SHA256_BIN: ${{ needs.build_unix.outputs.sha256_bin }}
|
||||
SHA256_TAR: ${{ needs.build_unix.outputs.sha256_tar }}
|
||||
run: |
|
||||
echo "version:${{ env.YTDLP_VERSION }}" >> SHA2-256SUMS
|
||||
echo "yt-dlp.exe:${{ env.SHA256_WINDOWS }}" >> SHA2-256SUMS
|
||||
echo "yt-dlp_x86.exe:${{ env.SHA256_WINDOWS32 }}" >> SHA2-256SUMS
|
||||
echo "yt-dlp:${{ env.SHA256_UNIX }}" >> SHA2-256SUMS
|
||||
echo "${{ env.SHA256_WIN }} yt-dlp.exe" >> SHA2-256SUMS
|
||||
echo "${{ env.SHA256_WIN32 }} yt-dlp_x86.exe" >> SHA2-256SUMS
|
||||
echo "${{ env.SHA256_BIN }} yt-dlp" >> SHA2-256SUMS
|
||||
echo "${{ env.SHA256_TAR }} yt-dlp.tar.gz" >> SHA2-256SUMS
|
||||
echo "${{ env.SHA256_WIN_ZIP }} yt-dlp.zip" >> SHA2-256SUMS
|
||||
- name: Upload 256SUMS file
|
||||
id: upload-sums
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -201,13 +275,17 @@ jobs:
|
||||
asset_content_type: text/plain
|
||||
- name: Make SHA2-512SUMS file
|
||||
env:
|
||||
SHA512_WINDOWS: ${{ needs.build_windows.outputs.sha512_windows }}
|
||||
SHA512_WINDOWS32: ${{ needs.build_windows32.outputs.sha512_windows32 }}
|
||||
SHA512_UNIX: ${{ needs.build_unix.outputs.sha512_unix }}
|
||||
SHA512_WIN: ${{ needs.build_windows.outputs.sha512_win }}
|
||||
SHA512_WIN_ZIP: ${{ needs.build_windows.outputs.sha512_win_zip }}
|
||||
SHA512_WIN32: ${{ needs.build_windows32.outputs.sha512_win32 }}
|
||||
SHA512_BIN: ${{ needs.build_unix.outputs.sha512_bin }}
|
||||
SHA512_TAR: ${{ needs.build_unix.outputs.sha512_tar }}
|
||||
run: |
|
||||
echo "${{ env.SHA512_WINDOWS }} yt-dlp.exe" >> SHA2-512SUMS
|
||||
echo "${{ env.SHA512_WINDOWS32 }} yt-dlp_x86.exe" >> SHA2-512SUMS
|
||||
echo "${{ env.SHA512_UNIX }} yt-dlp" >> SHA2-512SUMS
|
||||
echo "${{ env.SHA512_WIN }} yt-dlp.exe" >> SHA2-512SUMS
|
||||
echo "${{ env.SHA512_WIN32 }} yt-dlp_x86.exe" >> SHA2-512SUMS
|
||||
echo "${{ env.SHA512_BIN }} yt-dlp" >> SHA2-512SUMS
|
||||
echo "${{ env.SHA512_TAR }} yt-dlp.tar.gz" >> SHA2-512SUMS
|
||||
echo "${{ env.SHA512_WIN_ZIP }} yt-dlp.zip" >> SHA2-512SUMS
|
||||
- name: Upload 512SUMS file
|
||||
id: upload-512sums
|
||||
uses: actions/upload-release-asset@v1
|
||||
|
||||
2
.github/workflows/quick-test.yml
vendored
2
.github/workflows/quick-test.yml
vendored
@@ -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:
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -2,7 +2,8 @@
|
||||
*.conf
|
||||
*.spec
|
||||
cookies
|
||||
cookies.txt
|
||||
*cookies.txt
|
||||
.netrc
|
||||
|
||||
# Downloaded
|
||||
*.srt
|
||||
@@ -42,7 +43,7 @@ cookies.txt
|
||||
*.description
|
||||
|
||||
# Allow config/media files in testdata
|
||||
!test/testdata/**
|
||||
!test/**
|
||||
|
||||
# Python
|
||||
*.pyc
|
||||
|
||||
269
CONTRIBUTING.md
269
CONTRIBUTING.md
@@ -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
|
||||
|
||||
25
CONTRIBUTORS
25
CONTRIBUTORS
@@ -100,3 +100,28 @@ korli
|
||||
octotherp
|
||||
CeruleanSky
|
||||
zootedb0t
|
||||
chao813
|
||||
ChillingPepper
|
||||
ConquerorDopy
|
||||
dalanmiller
|
||||
DigitalDJ
|
||||
f4pp3rk1ng
|
||||
gesa
|
||||
Jules-A
|
||||
makeworld-the-better-one
|
||||
MKSherbini
|
||||
mrx23dot
|
||||
poschi3
|
||||
raphaeldore
|
||||
renalid
|
||||
sleaux-meaux
|
||||
sulyi
|
||||
tmarki
|
||||
Vangelis66
|
||||
AjaxGb
|
||||
ajj8
|
||||
jakubadamw
|
||||
jfogelman
|
||||
timethrow
|
||||
sarnoud
|
||||
Bojidarist
|
||||
|
||||
205
Changelog.md
205
Changelog.md
@@ -7,18 +7,200 @@
|
||||
* Update Changelog.md and CONTRIBUTORS
|
||||
* Change "Merged with ytdl" version in Readme.md if needed
|
||||
* Add new/fixed extractors in "new features" section of Readme.md
|
||||
* Commit to master as `Release <version>`
|
||||
* Commit as `Release <version>`
|
||||
* Push to origin/release using `git push origin master:release`
|
||||
build task will now run
|
||||
* Update version.py using `devscripts\update-version.py`
|
||||
* Run `make issuetemplates`
|
||||
* Commit to master as `[version] update :ci skip all`
|
||||
* Push to origin/master
|
||||
* Update changelog in /releases
|
||||
|
||||
-->
|
||||
|
||||
|
||||
### 2021.10.10
|
||||
|
||||
* [downloader/ffmpeg] Fix bug in initializing `FFmpegPostProcessor`
|
||||
* [minicurses] Fix when printing to file
|
||||
* [downloader] Fix throttledratelimit
|
||||
* [francetv] Fix extractor by [fstirlitz](https://github.com/fstirlitz), [sarnoud](https://github.com/sarnoud)
|
||||
* [NovaPlay] Add extractor by [Bojidarist](https://github.com/Bojidarist)
|
||||
* [ffmpeg] Revert "Set max probesize" - No longer needed
|
||||
* [docs] Remove incorrect dependency on VC++10
|
||||
* [build] Allow to release without changelog
|
||||
|
||||
### 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`
|
||||
* [outtmpl] Allow alternate fields using `,`
|
||||
* [outtmpl] Add format type `B` to treat the value as bytes (eg: to limit the filename to a certain number of bytes)
|
||||
* Separate the options `--ignore-errors` and `--no-abort-on-error`
|
||||
* Basic framework for simultaneous download of multiple formats by [nao20010128nao](https://github.com/nao20010128nao)
|
||||
* [17live] Add 17.live extractor by [nao20010128nao](https://github.com/nao20010128nao)
|
||||
* [bilibili] Add BiliIntlIE and BiliIntlSeriesIE by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [CAM4] Add extractor by [alerikaisattera](https://github.com/alerikaisattera)
|
||||
* [Chingari] Add extractors by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [CGTN] Add extractor by [chao813](https://github.com/chao813)
|
||||
* [damtomo] Add extractor by [nao20010128nao](https://github.com/nao20010128nao)
|
||||
* [gotostage] Add extractor by [poschi3](https://github.com/poschi3)
|
||||
* [Koo] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [Mediaite] Add Extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [Mediaklikk] Add Extractor by [tmarki](https://github.com/tmarki), [mrx23dot](https://github.com/mrx23dot), [coletdjnz](https://github.com/coletdjnz)
|
||||
* [MuseScore] Add Extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [Newgrounds] Add NewgroundsUserIE and improve extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [nzherald] Add NZHeraldIE by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [Olympics] Add replay extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [Peertube] Add channel and playlist extractors by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [radlive] Add extractor by [nyuszika7h](https://github.com/nyuszika7h)
|
||||
* [SovietsCloset] Add extractor by [ChillingPepper](https://github.com/ChillingPepper)
|
||||
* [Streamanity] Add Extractor by [alerikaisattera](https://github.com/alerikaisattera)
|
||||
* [Theta] Add extractor by [alerikaisattera](https://github.com/alerikaisattera)
|
||||
* [Yandex] Add ZenYandexIE and ZenYandexChannelIE by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [9Now] handle episodes of series by [dalanmiller](https://github.com/dalanmiller)
|
||||
* [AnimalPlanet] Fix extractor by [Sipherdrakon](https://github.com/Sipherdrakon)
|
||||
* [Arte] Improve description extraction by [renalid](https://github.com/renalid)
|
||||
* [atv.at] Use jwt for API by [NeroBurner](https://github.com/NeroBurner)
|
||||
* [brightcove] Extract subtitles from manifests
|
||||
* [CBC] Fix CBC Gem extractors by [makeworld-the-better-one](https://github.com/makeworld-the-better-one)
|
||||
* [cbs] Report appropriate error for DRM
|
||||
* [comedycentral] Support `collection-playlist` by [nixxo](https://github.com/nixxo)
|
||||
* [DIYNetwork] Support new format by [Sipherdrakon](https://github.com/Sipherdrakon)
|
||||
* [downloader/niconico] Pass custom headers by [nao20010128nao](https://github.com/nao20010128nao)
|
||||
* [dw] Fix extractor
|
||||
* [Fancode] Fix live streams by [zenerdi0de](https://github.com/zenerdi0de)
|
||||
* [funimation] Fix for locations outside US by [Jules-A](https://github.com/Jules-A), [pukkandan](https://github.com/pukkandan)
|
||||
* [globo] Fix GloboIE by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [HiDive] Fix extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [Hotstar] Add referer for subs by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [itv] Fix extractor, add subtitles and thumbnails by [coletdjnz](https://github.com/coletdjnz), [sleaux-meaux](https://github.com/sleaux-meaux), [Vangelis66](https://github.com/Vangelis66)
|
||||
* [lbry] Show error message from API response
|
||||
* [Mxplayer] Use mobile API by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [NDR] Rewrite NDRIE by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [Nuvid] Fix extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [Oreilly] Handle new web url by [MKSherbini](https://github.com/MKSherbini)
|
||||
* [pbs] Fix subtitle extraction by [coletdjnz](https://github.com/coletdjnz), [gesa](https://github.com/gesa), [raphaeldore](https://github.com/raphaeldore)
|
||||
* [peertube] Update instances by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [plutotv] Fix extractor for URLs with `/en`
|
||||
* [reddit] Workaround for 429 by redirecting to old.reddit.com
|
||||
* [redtube] Fix exts
|
||||
* [soundcloud] Make playlist extraction lazy
|
||||
* [soundcloud] Retry playlist pages on `502` error and update `_CLIENT_ID`
|
||||
* [southpark] Fix SouthParkDE by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [SovietsCloset] Fix playlists for games with only named categories by [ConquerorDopy](https://github.com/ConquerorDopy)
|
||||
* [SpankBang] Fix uploader by [f4pp3rk1ng](https://github.com/f4pp3rk1ng), [coletdjnz](https://github.com/coletdjnz)
|
||||
* [tiktok] Use API to fetch higher quality video by [MinePlayersPE](https://github.com/MinePlayersPE), [llacb47](https://github.com/llacb47)
|
||||
* [TikTokUser] Fix extractor using mobile API by [MinePlayersPE](https://github.com/MinePlayersPE), [llacb47](https://github.com/llacb47)
|
||||
* [videa] Fix some extraction errors by [nyuszika7h](https://github.com/nyuszika7h)
|
||||
* [VrtNU] Handle login errors by [llacb47](https://github.com/llacb47)
|
||||
* [vrv] Don't raise error when thumbnails are missing
|
||||
* [youtube] Cleanup authentication code by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [youtube] Fix `--mark-watched` with `--cookies-from-browser`
|
||||
* [youtube] Improvements to JS player extraction and add extractor-args to skip it by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [youtube] Retry on 'Unknown Error' by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [youtube] Return full URL instead of just ID
|
||||
* [youtube] Warn when trying to download clips
|
||||
* [zdf] Improve format sorting
|
||||
* [zype] Extract subtitles from the m3u8 manifest by [fstirlitz](https://github.com/fstirlitz)
|
||||
* Allow `--force-write-archive` to work with `--flat-playlist`
|
||||
* Download subtitles in order of `--sub-langs`
|
||||
* Allow `0` in `--playlist-items`
|
||||
* Handle more playlist errors with `-i`
|
||||
* Fix `--no-get-comments`
|
||||
* Fix `extra_info` being reused across runs
|
||||
* Fix compat options `no-direct-merge` and `playlist-index`
|
||||
* Dump files should obey `--trim-filename` by [sulyi](https://github.com/sulyi)
|
||||
* [aes] Add `aes_gcm_decrypt_and_verify` by [sulyi](https://github.com/sulyi), [pukkandan](https://github.com/pukkandan)
|
||||
* [aria2c] Fix IV for some AES-128 streams by [shirt](https://github.com/shirt-dev)
|
||||
* [compat] Don't ignore `HOME` (if set) on windows
|
||||
* [cookies] Make browser names case insensitive
|
||||
* [cookies] Print warning for cookie decoding error only once
|
||||
* [extractor] Fix root-relative URLs in MPD by [DigitalDJ](https://github.com/DigitalDJ)
|
||||
* [ffmpeg] Add `aac_adtstoasc` when merging if needed
|
||||
* [fragment,aria2c] Generalize and refactor some code
|
||||
* [fragment] Avoid repeated request for AES key
|
||||
* [fragment] Fix range header when using `-N` and media sequence by [shirt](https://github.com/shirt-dev)
|
||||
* [hls,aes] Fallback to native implementation for AES-CBC and detect `Cryptodome` in addition to `Crypto`
|
||||
* [hls] Byterange + AES128 is supported by native downloader
|
||||
* [ModifyChapters] Improve sponsor chapter merge algorithm by [nihil-admirari](https://github.com/nihil-admirari)
|
||||
* [ModifyChapters] Minor fixes
|
||||
* [WebVTT] Adjust parser to accommodate PBS subtitles
|
||||
* [utils] Improve `extract_timezone` by [dirkf](https://github.com/dirkf)
|
||||
* [options] Fix `--no-config` and refactor reading of config files
|
||||
* [options] Strip spaces and ignore empty entries in list-like switches
|
||||
* [test/cookies] Improve logging
|
||||
* [build] Automate more of the release process by [animelover1984](https://github.com/animelover1984), [pukkandan](https://github.com/pukkandan)
|
||||
* [build] Fix sha256 by [nihil-admirari](https://github.com/nihil-admirari)
|
||||
* [build] Bring back brew taps by [nao20010128nao](https://github.com/nao20010128nao)
|
||||
* [build] Provide `--onedir` zip for windows by [pukkandan](https://github.com/pukkandan)
|
||||
* [cleanup,docs] Add deprecation warning in docs for some counter intuitive behaviour
|
||||
* [cleanup] Fix line endings for `nebula.py` by [glenn-slayden](https://github.com/glenn-slayden)
|
||||
* [cleanup] Improve `make clean-test` by [sulyi](https://github.com/sulyi)
|
||||
* [cleanup] Misc
|
||||
|
||||
|
||||
### 2021.09.02
|
||||
|
||||
* **Native SponsorBlock** implementation by [nihil-admirari](https://github.com/nihil-admirari), [pukkandan](https://github.com/pukkandan)
|
||||
@@ -30,7 +212,7 @@
|
||||
* The fetched sponsor sections are written to the infojson
|
||||
* Deprecates: `--sponskrub`, `--no-sponskrub`, `--sponskrub-cut`, `--no-sponskrub-cut`, `--sponskrub-force`, `--no-sponskrub-force`, `--sponskrub-location`, `--sponskrub-args`
|
||||
* Split `--embed-chapters` from `--embed-metadata` (it still implies the former by default)
|
||||
* Add option `--remove-chapters` to remove arbitrary chapters by [nihil-admirari](https://github.com/nihil-admirari), pukkandan
|
||||
* Add option `--remove-chapters` to remove arbitrary chapters by [nihil-admirari](https://github.com/nihil-admirari), [pukkandan](https://github.com/pukkandan)
|
||||
* Add option `--force-keyframes-at-cuts` for more accurate cuts when removing and splitting chapters by [nihil-admirari](https://github.com/nihil-admirari)
|
||||
* Let `--match-filter` reject entries early
|
||||
* Makes redundant: `--match-title`, `--reject-title`, `--min-views`, `--max-views`
|
||||
@@ -42,7 +224,6 @@
|
||||
* [downloader/ffmpeg] Experimental support for DASH manifests (including live)
|
||||
* Your ffmpeg must have [this patch](https://github.com/FFmpeg/FFmpeg/commit/3249c757aed678780e22e99a1a49f4672851bca9) applied for YouTube DASH to work
|
||||
* [downloader/ffmpeg] Allow passing custom arguments before `-i`
|
||||
|
||||
* [BannedVideo] Add extractor by [smege1001](https://github.com/smege1001), [blackjack4494](https://github.com/blackjack4494), [pukkandan](https://github.com/pukkandan)
|
||||
* [bilibili] Add category extractor by [animelover1984](https://github.com/animelover1984)
|
||||
* [Epicon] Add extractors by [Ashish0804](https://github.com/Ashish0804)
|
||||
@@ -60,7 +241,6 @@
|
||||
* [Tokentube] Add extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||
* [TV2Hu] Fix `TV2HuIE` and add `TV2HuSeriesIE` by [Ashish0804](https://github.com/Ashish0804)
|
||||
* [voicy] Add extractor by [nao20010128nao](https://github.com/nao20010128nao)
|
||||
|
||||
* [adobepass] Fix Verizon SAML login by [nyuszika7h](https://github.com/nyuszika7h), [ParadoxGBB](https://github.com/ParadoxGBB)
|
||||
* [afreecatv] Fix adult VODs by [wlritchi](https://github.com/wlritchi)
|
||||
* [afreecatv] Tolerate failure to parse date string by [wlritchi](https://github.com/wlritchi)
|
||||
@@ -100,7 +280,6 @@
|
||||
* [youtube] Prefer audio stream that YouTube considers default
|
||||
* [youtube] Remove annotations and deprecate `--write-annotations` by [coletdjnz](https://github.com/coletdjnz)
|
||||
* [Zee5] Fix extractor and add subtitles by [Ashish0804](https://github.com/Ashish0804)
|
||||
|
||||
* [aria2c] Obey `--rate-limit`
|
||||
* [EmbedSubtitle] Continue even if some files are missing
|
||||
* [extractor] Better error message for DRM
|
||||
@@ -185,8 +364,8 @@
|
||||
### 2021.08.02
|
||||
|
||||
* Add logo, banner and donate links
|
||||
* Expand and escape environment variables correctly in output template
|
||||
* Add format types `j` (json), `l` (comma delimited list), `q` (quoted for terminal) in output template
|
||||
* [outtmpl] Expand and escape environment variables
|
||||
* [outtmpl] Add format types `j` (json), `l` (comma delimited list), `q` (quoted for terminal)
|
||||
* [downloader] Allow streaming some unmerged formats to stdout using ffmpeg
|
||||
* [youtube] **Age-gate bypass**
|
||||
* Add `agegate` clients by [pukkandan](https://github.com/pukkandan), [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||
@@ -391,7 +570,7 @@
|
||||
### 2021.06.09
|
||||
|
||||
* Fix bug where `%(field)d` in filename template throws error
|
||||
* Improve offset parsing in outtmpl
|
||||
* [outtmpl] Improve offset parsing
|
||||
* [test] More rigorous tests for `prepare_filename`
|
||||
|
||||
### 2021.06.08
|
||||
|
||||
16
Makefile
16
Makefile
@@ -13,7 +13,9 @@ pypi-files: AUTHORS Changelog.md LICENSE README.md README.txt supportedsites com
|
||||
.PHONY: all clean install test tar pypi-files completions ot offlinetest codetest supportedsites
|
||||
|
||||
clean-test:
|
||||
rm -rf *.dump *.part* *.ytdl *.info.json *.mp4 *.m4a *.flv *.mp3 *.avi *.mkv *.webm *.3gp *.wav *.ape *.swf *.jpg *.png *.frag *.frag.urls *.frag.aria2 test/testdata/player-*.js *.opus *.webp *.ttml *.vtt *.jpeg
|
||||
rm -rf *.3gp *.annotations.xml *.ape *.avi *.description *.dump *.flac *.flv *.frag *.frag.aria2 *.frag.urls \
|
||||
*.info.json *.jpeg *.jpg *.live_chat.json *.m4a *.m4v *.mkv *.mp3 *.mp4 *.ogg *.opus *.part* *.png *.sbv *.srt \
|
||||
*.swf *.swp *.ttml *.vtt *.wav *.webm *.webp *.ytdl test/testdata/player-*.js
|
||||
clean-dist:
|
||||
rm -rf yt-dlp.1.temp.md yt-dlp.1 README.txt MANIFEST build/ dist/ .coverage cover/ yt-dlp.tar.gz completions/ yt_dlp/extractor/lazy_extractors.py *.spec CONTRIBUTING.md.tmp yt-dlp yt-dlp.exe yt_dlp.egg-info/ AUTHORS .mailmap
|
||||
clean-cache:
|
||||
@@ -110,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' \
|
||||
@@ -119,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
|
||||
|
||||
213
README.md
213
README.md
@@ -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
|
||||
* **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, NovaPlay
|
||||
|
||||
* **Fixed/improved extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, akamai, ina, rumble, tennistv, amcnetworks, la7 podcasts, linuxacadamy, nitter, twitcasting, viu, crackle, curiositystream, mediasite, rmcdecouverte, sonyliv, tubi, tenplay, patreon, videa, yahoo, BravoTV, crunchyroll playlist, RTP, viki, Hotstar, vidio, vimeo, mediaset, Mxplayer, nbcolympics, ParamountPlus, Newgrounds, SAML Verizon login, Hungama, afreecatv, aljazeera, ATV, bitchute, camtube, CDA, eroprofile, facebook, HearThisAtIE, iwara, kakao, Motherless, Nova, peertube, pornhub, reddit, tiktok, TV2, TV2Hu, tv5mondeplus, VH1, Viafree, XHamster
|
||||
* **Fixed/improved extractors**: archive.org, roosterteeth.com, skyit, instagram, itv, SouthparkDe, spreaker, Vlive, akamai, ina, rumble, tennistv, amcnetworks, la7 podcasts, linuxacadamy, nitter, twitcasting, viu, crackle, curiositystream, mediasite, rmcdecouverte, sonyliv, tubi, tenplay, patreon, videa, yahoo, BravoTV, crunchyroll playlist, RTP, viki, Hotstar, vidio, vimeo, mediaset, Mxplayer, nbcolympics, ParamountPlus, Newgrounds, SAML Verizon login, Hungama, afreecatv, aljazeera, ATV, bitchute, camtube, CDA, eroprofile, facebook, HearThisAtIE, iwara, kakao, Motherless, Nova, peertube, pornhub, reddit, tiktok, TV2, TV2Hu, tv5mondeplus, VH1, Viafree, XHamster, 9Now, AnimalPlanet, Arte, CBC, Chingari, comedycentral, DIYNetwork, niconico, dw, funimation, globo, HiDive, NDR, Nuvid, Oreilly, pbs, plutotv, reddit, redtube, soundcloud, SpankBang, VrtNU, bbc, Bilibili, LinkedInLearning, parliamentlive, PolskieRadio, Streamable, vidme, francetv
|
||||
|
||||
* **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`
|
||||
@@ -189,13 +193,15 @@ If you have installed using Homebrew, run `brew upgrade yt-dlp/taps/yt-dlp`
|
||||
### DEPENDENCIES
|
||||
Python versions 3.6+ (CPython and PyPy) are supported. Other versions and implementations may or may not work correctly.
|
||||
|
||||
<!-- https://www.microsoft.com/en-us/download/details.aspx?id=26999 -->
|
||||
<!-- Python 3.5+ uses VC++14 and it is already embedded in the binary created
|
||||
<!x-- https://www.microsoft.com/en-us/download/details.aspx?id=26999 --x>
|
||||
On windows, [Microsoft Visual C++ 2010 SP1 Redistributable Package (x86)](https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe) is also necessary to run yt-dlp. You probably already have this, but if the executable throws an error due to missing `MSVCR100.dll` you need to install it manually.
|
||||
-->
|
||||
|
||||
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 +213,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.
|
||||
|
||||
@@ -243,9 +252,12 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
|
||||
-U, --update Update this program to latest version. Make
|
||||
sure that you have sufficient permissions
|
||||
(run with sudo if needed)
|
||||
-i, --ignore-errors Continue on download errors, for example to
|
||||
skip unavailable videos in a playlist
|
||||
(default) (Alias: --no-abort-on-error)
|
||||
-i, --ignore-errors Ignore download and postprocessing errors.
|
||||
The download will be considered successfull
|
||||
even if the postprocessing fails
|
||||
--no-abort-on-error Continue with next video on download
|
||||
errors; e.g. to skip unavailable videos in
|
||||
a playlist (default)
|
||||
--abort-on-error Abort downloading of further videos if an
|
||||
error occurs (Alias: --no-ignore-errors)
|
||||
--dump-user-agent Display the current user-agent and exit
|
||||
@@ -277,7 +289,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
|
||||
@@ -530,10 +543,10 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
|
||||
--cookies-from-browser BROWSER[:PROFILE]
|
||||
Load cookies from a user profile of the
|
||||
given web browser. Currently supported
|
||||
browsers are: brave|chrome|chromium|edge|fi
|
||||
refox|opera|safari|vivaldi. You can specify
|
||||
the user profile name or directory using
|
||||
"BROWSER:PROFILE_NAME" or
|
||||
browsers are: brave, chrome, chromium,
|
||||
edge, firefox, opera, safari, vivaldi. You
|
||||
can specify the user profile name or
|
||||
directory using "BROWSER:PROFILE_NAME" or
|
||||
"BROWSER:PROFILE_PATH". If no profile is
|
||||
given, the most recently accessed one is
|
||||
used
|
||||
@@ -598,7 +611,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)
|
||||
@@ -695,6 +719,9 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
|
||||
out, yt-dlp will ask interactively
|
||||
-2, --twofactor TWOFACTOR Two-factor authentication code
|
||||
-n, --netrc Use .netrc authentication data
|
||||
--netrc-location PATH Location of .netrc authentication data;
|
||||
either the path or its containing
|
||||
directory. Defaults to ~/.netrc
|
||||
--video-password PASSWORD Video password (vimeo, youku)
|
||||
--ap-mso MSO Adobe Pass multiple-system operator (TV
|
||||
provider) identifier, use --ap-list-mso for
|
||||
@@ -831,6 +858,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,
|
||||
@@ -897,7 +938,7 @@ You can configure yt-dlp by placing any supported command line option to a confi
|
||||
* `~/yt-dlp.conf`
|
||||
* `~/yt-dlp.conf.txt`
|
||||
|
||||
Note that `~` points to `C:\Users\<user name>` on windows. Also, `%XDG_CONFIG_HOME%` defaults to `~/.config` if undefined
|
||||
`%XDG_CONFIG_HOME%` defaults to `~/.config` if undefined. On windows, `~` points to %HOME% if present, `%USERPROFILE%` (generally `C:\Users\<user name>`) or `%HOMEDRIVE%%HOMEPATH%`.
|
||||
1. **System Configuration**: `/etc/yt-dlp.conf`
|
||||
|
||||
For example, with the following configuration file yt-dlp will always extract the audio, not copy the mtime, use a proxy and save all videos under `YouTube` directory in your home directory:
|
||||
@@ -923,14 +964,14 @@ You can use `--ignore-config` if you want to disable all configuration files for
|
||||
|
||||
### Authentication with `.netrc` file
|
||||
|
||||
You may also want to configure automatic credentials storage for extractors that support authentication (by providing login and password with `--username` and `--password`) in order not to pass credentials as command line arguments on every yt-dlp execution and prevent tracking plain text passwords in the shell command history. You can achieve this using a [`.netrc` file](https://stackoverflow.com/tags/.netrc/info) on a per extractor basis. For that you will need to create a `.netrc` file in your `$HOME` and restrict permissions to read/write by only you:
|
||||
You may also want to configure automatic credentials storage for extractors that support authentication (by providing login and password with `--username` and `--password`) in order not to pass credentials as command line arguments on every yt-dlp execution and prevent tracking plain text passwords in the shell command history. You can achieve this using a [`.netrc` file](https://stackoverflow.com/tags/.netrc/info) on a per extractor basis. For that you will need to create a `.netrc` file in `--netrc-location` and restrict permissions to read/write by only you:
|
||||
```
|
||||
touch $HOME/.netrc
|
||||
chmod a-rwx,u+rw $HOME/.netrc
|
||||
```
|
||||
After that you can add credentials for an extractor in the following format, where *extractor* is the name of the extractor in lowercase:
|
||||
```
|
||||
machine <extractor> login <login> password <password>
|
||||
machine <extractor> login <username> password <password>
|
||||
```
|
||||
For example:
|
||||
```
|
||||
@@ -939,10 +980,7 @@ machine twitch login my_twitch_account_name password my_twitch_password
|
||||
```
|
||||
To activate authentication with the `.netrc` file you should pass `--netrc` to yt-dlp or place it in the [configuration file](#configuration).
|
||||
|
||||
On Windows you may also need to setup the `%HOME%` environment variable manually. For example:
|
||||
```
|
||||
set HOME=%USERPROFILE%
|
||||
```
|
||||
The default location of the .netrc file is `$HOME` (`~`) in UNIX. On Windows, it is `%HOME%` if present, `%USERPROFILE%` (generally `C:\Users\<user name>`) or `%HOMEDRIVE%%HOMEPATH%`
|
||||
|
||||
# OUTPUT TEMPLATE
|
||||
|
||||
@@ -952,21 +990,23 @@ The `-o` option is used to indicate a template for the output file names while `
|
||||
|
||||
The simplest usage of `-o` is not to set any template arguments when downloading a single file, like in `yt-dlp -o funny_video.flv "https://some/video"` (hard-coding file extension like this is _not_ recommended and could break some post-processing).
|
||||
|
||||
It may however also contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [python string formatting operations](https://docs.python.org/2/library/stdtypes.html#string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations.
|
||||
It may however also contain special sequences that will be replaced when downloading each video. The special sequences may be formatted according to [python string formatting operations](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting). For example, `%(NAME)s` or `%(NAME)05d`. To clarify, that is a percent symbol followed by a name in parentheses, followed by formatting operations.
|
||||
|
||||
The field names themselves (the part inside the parenthesis) can also have some special formatting:
|
||||
1. **Object traversal**: The dictionaries and lists available in metadata can be traversed by using a `.` (dot) separator. You can also do python slicing using `:`. Eg: `%(tags.0)s`, `%(subtitles.en.-1.ext)s`, `%(id.3:7:-1)s`, `%(formats.:.format_id)s`. `%()s` refers to the entire infodict. Note that all the fields that become available using this method are not listed below. Use `-j` to see such fields
|
||||
1. **Addition**: Addition and subtraction of numeric fields can be done using `+` and `-` respectively. Eg: `%(playlist_index+10)03d`, `%(n_entries+1-playlist_index)d`
|
||||
1. **Date/time Formatting**: Date/time fields can be formatted according to [strftime formatting](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) by specifying it separated from the field name using a `>`. Eg: `%(duration>%H-%M-%S)s`, `%(upload_date>%Y-%m-%d)s`, `%(epoch-3600>%H-%M-%S)s`
|
||||
1. **Default**: A default value can be specified for when the field is empty using a `|` seperator. This overrides `--output-na-template`. Eg: `%(uploader|Unknown)s`
|
||||
1. **More Conversions**: In addition to the normal format types `diouxXeEfFgGcrs`, `j`, `l`, `q` can be used for converting to **j**son, a comma seperated **l**ist and a string **q**uoted for the terminal respectively
|
||||
1. **Alternatives**: Alternate fields can be specified seperated with a `,`. Eg: `%(release_date>%Y,upload_date>%Y|Unknown)s`
|
||||
1. **Default**: A literal default value can be specified for when the field is empty using a `|` seperator. This overrides `--output-na-template`. Eg: `%(uploader|Unknown)s`
|
||||
1. **More Conversions**: In addition to the normal format types `diouxXeEfFgGcrs`, `B`, `j`, `l`, `q` can be used for converting to **B**ytes, **j**son, a comma seperated **l**ist (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][|default])[flags][width][.precision][length]type
|
||||
%(name[.keys][addition][>strf][,alternate][|default])[flags][width][.precision][length]type
|
||||
```
|
||||
|
||||
Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation`, `infojson`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video.
|
||||
Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation` (deprecated), `infojson`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`. For example, `-o '%(title)s.%(ext)s' -o 'thumbnail:%(title)s\%(title)s.%(ext)s'` will put the thumbnails in a folder with the same name as the video. 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:
|
||||
|
||||
@@ -980,9 +1020,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
|
||||
@@ -1024,8 +1065,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
|
||||
@@ -1169,7 +1212,11 @@ If you want to download multiple videos and they don't have the same formats ava
|
||||
|
||||
If you want to download several formats of the same video use a comma as a separator, e.g. `-f 22,17,18` will download all these three formats, of course if they are available. Or a more sophisticated example combined with the precedence feature: `-f 136/137/mp4/bestvideo,140/m4a/bestaudio`.
|
||||
|
||||
You can merge the video and audio of multiple formats into a single file using `-f <format1>+<format2>+...` (requires ffmpeg installed), for example `-f bestvideo+bestaudio` will download the best video-only format, the best audio-only format and mux them together with ffmpeg. Unless `--video-multistreams` is used, all formats with a video stream except the first one are ignored. Similarly, unless `--audio-multistreams` is used, all formats with an audio stream except the first one are ignored. For example, `-f bestvideo+best+bestaudio --video-multistreams --audio-multistreams` will download and merge all 3 given formats. The resulting file will have 2 video streams and 2 audio streams. But `-f bestvideo+best+bestaudio --no-video-multistreams` will download and merge only `bestvideo` and `bestaudio`. `best` is ignored since another format containing a video stream (`bestvideo`) has already been selected. The order of the formats is therefore important. `-f best+bestaudio --no-audio-multistreams` will download and merge both formats while `-f bestaudio+best --no-audio-multistreams` will ignore `best` and download only `bestaudio`.
|
||||
You can merge the video and audio of multiple formats into a single file using `-f <format1>+<format2>+...` (requires ffmpeg installed), for example `-f bestvideo+bestaudio` will download the best video-only format, the best audio-only format and mux them together with ffmpeg.
|
||||
|
||||
**Deprecation warning**: Since the *below* described behavior is complex and counter-intuitive, this will be removed and multistreams will be enabled by default in the future. A new operator will be instead added to limit formats to single audio/video
|
||||
|
||||
Unless `--video-multistreams` is used, all formats with a video stream except the first one are ignored. Similarly, unless `--audio-multistreams` is used, all formats with an audio stream except the first one are ignored. For example, `-f bestvideo+best+bestaudio --video-multistreams --audio-multistreams` will download and merge all 3 given formats. The resulting file will have 2 video streams and 2 audio streams. But `-f bestvideo+best+bestaudio --no-video-multistreams` will download and merge only `bestvideo` and `bestaudio`. `best` is ignored since another format containing a video stream (`bestvideo`) has already been selected. The order of the formats is therefore important. `-f best+bestaudio --no-audio-multistreams` will download and merge both formats while `-f bestaudio+best --no-audio-multistreams` will ignore `best` and download only `bestaudio`.
|
||||
|
||||
## Filtering Formats
|
||||
|
||||
@@ -1235,10 +1282,12 @@ The available fields are:
|
||||
- `abr`: Average audio bitrate in KBit/s
|
||||
- `br`: Equivalent to using `tbr,vbr,abr`
|
||||
- `asr`: Audio sample rate in Hz
|
||||
|
||||
**Deprecation warning**: Many of these fields have (currently undocumented) aliases, that may be removed in a future version. It is recommended to use only the documented field names.
|
||||
|
||||
All fields, unless specified otherwise, are sorted in descending order. To reverse this, prefix the field with a `+`. Eg: `+res` prefers format with the smallest resolution. Additionally, you can suffix a preferred value for the fields, separated by a `:`. Eg: `res:720` prefers larger videos, but no larger than 720p and the smallest video if there are no videos less than 720p. For `codec` and `ext`, you can provide two preferred values, the first for video and the second for audio. Eg: `+codec:avc:m4a` (equivalent to `+vcodec:avc,+acodec:m4a`) sets the video codec preference to `h264` > `h265` > `vp9` > `vp9.2` > `av01` > `vp8` > `h263` > `theora` and audio codec preference to `mp4a` > `aac` > `vorbis` > `opus` > `mp3` > `ac3` > `dts`. You can also make the sorting prefer the nearest values to the provided by using `~` as the delimiter. Eg: `filesize~1G` prefers the format with filesize closest to 1 GiB.
|
||||
|
||||
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
|
||||
|
||||
@@ -1436,11 +1485,14 @@ The following extractors use this feature:
|
||||
* **youtube**
|
||||
* `skip`: `hls` or `dash` (or both) to skip download of the respective manifests
|
||||
* `player_client`: Clients to extract video data from. The main clients are `web`, `android`, `ios`, `mweb`. These also have `_music`, `_embedded`, `_agegate`, and `_creator` variants (Eg: `web_embedded`) (`mweb` has only `_agegate`). By default, `android,web` is used, but the agegate and creator variants are added as required for age-gated videos. Similarly the music variants are added for `music.youtube.com` urls. You can also use `all` to use all the clients
|
||||
* `player_skip`: `configs` - skip any requests for client configs and use defaults
|
||||
* `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause some issues. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) for more details
|
||||
* `include_live_dash`: Include live dash formats (These formats don't download properly)
|
||||
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side).
|
||||
* `max_comments`: Maximum amount of comments to download (default all).
|
||||
* `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`
|
||||
@@ -1454,9 +1506,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
|
||||
|
||||
@@ -1559,6 +1696,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]
|
||||
|
||||
40
pyinst.py
40
pyinst.py
@@ -3,7 +3,6 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import sys
|
||||
# import os
|
||||
import platform
|
||||
|
||||
from PyInstaller.utils.hooks import collect_submodules
|
||||
@@ -13,16 +12,21 @@ 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')
|
||||
print('Building %sbit version' % arch)
|
||||
_x86 = '_x86' if arch == '32' else ''
|
||||
|
||||
FILE_DESCRIPTION = 'yt-dlp%s' % (' (32 Bit)' if _x86 else '')
|
||||
# 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']
|
||||
|
||||
# root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
# print('Changing working directory to %s' % root_dir)
|
||||
# os.chdir(root_dir)
|
||||
print(f'Building {arch}bit version with options {opts}')
|
||||
|
||||
FILE_DESCRIPTION = 'yt-dlp%s' % (' (32 Bit)' if _x86 else '')
|
||||
|
||||
exec(compile(open('yt_dlp/version.py').read(), 'yt_dlp/version.py', 'exec'))
|
||||
VERSION = locals()['__version__']
|
||||
@@ -67,16 +71,32 @@ 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([
|
||||
'--name=yt-dlp%s' % _x86,
|
||||
'--onefile',
|
||||
'--icon=devscripts/logo.ico',
|
||||
*[f'--exclude-module={module}' for module in excluded_modules],
|
||||
*[f'--hidden-import={module}' for module in dependancies],
|
||||
'--upx-exclude=vcruntime140.dll',
|
||||
'--noconfirm',
|
||||
*opts,
|
||||
'yt_dlp/__main__.py',
|
||||
])
|
||||
SetVersion('dist/yt-dlp%s.exe' % _x86, VERSION_FILE)
|
||||
SetVersion('dist/%syt-dlp%s.exe' % ('yt-dlp/' if '--onedir' in opts else '', _x86), VERSION_FILE)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
mutagen
|
||||
pycryptodome
|
||||
pycryptodomex
|
||||
websockets
|
||||
|
||||
94
setup.py
94
setup.py
@@ -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',
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Supported sites
|
||||
- **17live**
|
||||
- **17live:clip**
|
||||
- **1tv**: Первый канал
|
||||
- **20min**
|
||||
- **220.ro**
|
||||
@@ -50,6 +52,7 @@
|
||||
- **AmericasTestKitchen**
|
||||
- **AmericasTestKitchenSeason**
|
||||
- **anderetijden**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
|
||||
- **AnimalPlanet**
|
||||
- **AnimeLab**
|
||||
- **AnimeLabShows**
|
||||
- **AnimeOnDemand**
|
||||
@@ -125,6 +128,8 @@
|
||||
- **BilibiliChannel**
|
||||
- **BiliBiliPlayer**
|
||||
- **BiliBiliSearch**: Bilibili video search, "bilisearch" keyword
|
||||
- **BiliIntl**
|
||||
- **BiliIntlSeries**
|
||||
- **BioBioChileTV**
|
||||
- **Biography**
|
||||
- **BIQLE**
|
||||
@@ -152,6 +157,7 @@
|
||||
- **BusinessInsider**
|
||||
- **BuzzFeed**
|
||||
- **BYUtv**
|
||||
- **CAM4**
|
||||
- **Camdemy**
|
||||
- **CamdemyFolder**
|
||||
- **CamModels**
|
||||
@@ -164,10 +170,7 @@
|
||||
- **CarambaTVPage**
|
||||
- **CartoonNetwork**
|
||||
- **cbc.ca**
|
||||
- **cbc.ca:olympics**
|
||||
- **cbc.ca:player**
|
||||
- **cbc.ca:watch**
|
||||
- **cbc.ca:watch:video**
|
||||
- **CBS**
|
||||
- **CBSInteractive**
|
||||
- **CBSLocal**
|
||||
@@ -182,10 +185,13 @@
|
||||
- **CDA**
|
||||
- **CeskaTelevize**
|
||||
- **CeskaTelevizePorady**
|
||||
- **CGTN**
|
||||
- **channel9**: Channel 9
|
||||
- **CharlieRose**
|
||||
- **Chaturbate**
|
||||
- **Chilloutzone**
|
||||
- **Chingari**
|
||||
- **ChingariUser**
|
||||
- **chirbit**
|
||||
- **chirbit:profile**
|
||||
- **cielotv.it**
|
||||
@@ -193,6 +199,7 @@
|
||||
- **Cinemax**
|
||||
- **CiscoLiveSearch**
|
||||
- **CiscoLiveSession**
|
||||
- **ciscowebex**: Cisco Webex
|
||||
- **CJSW**
|
||||
- **cliphunter**
|
||||
- **Clippit**
|
||||
@@ -225,7 +232,6 @@
|
||||
- **CTV**
|
||||
- **CTVNews**
|
||||
- **cu.ntv.co.jp**: Nippon Television Network
|
||||
- **Culturebox**
|
||||
- **CultureUnplugged**
|
||||
- **curiositystream**
|
||||
- **curiositystream:collection**
|
||||
@@ -235,6 +241,8 @@
|
||||
- **dailymotion**
|
||||
- **dailymotion:playlist**
|
||||
- **dailymotion:user**
|
||||
- **damtomo:record**
|
||||
- **damtomo:video**
|
||||
- **daum.net**
|
||||
- **daum.net:clip**
|
||||
- **daum.net:playlist**
|
||||
@@ -258,6 +266,7 @@
|
||||
- **DiscoveryPlusIndiaShow**
|
||||
- **DiscoveryVR**
|
||||
- **Disney**
|
||||
- **DIYNetwork**
|
||||
- **dlive:stream**
|
||||
- **dlive:vod**
|
||||
- **DoodStream**
|
||||
@@ -336,13 +345,10 @@
|
||||
- **foxnews**: Fox News and Fox Business Video
|
||||
- **foxnews:article**
|
||||
- **FoxSports**
|
||||
- **france2.fr:generation-what**
|
||||
- **FranceCulture**
|
||||
- **FranceInter**
|
||||
- **FranceTV**
|
||||
- **FranceTVEmbed**
|
||||
- **francetvinfo.fr**
|
||||
- **FranceTVJeunesse**
|
||||
- **FranceTVSite**
|
||||
- **Freesound**
|
||||
- **freespeech.org**
|
||||
@@ -366,7 +372,11 @@
|
||||
- **Gazeta**
|
||||
- **GDCVault**
|
||||
- **GediDigital**
|
||||
- **gem.cbc.ca**
|
||||
- **gem.cbc.ca:live**
|
||||
- **gem.cbc.ca:playlist**
|
||||
- **generic**: Generic downloader that works on some sites
|
||||
- **Gettr**
|
||||
- **Gfycat**
|
||||
- **GiantBomb**
|
||||
- **Giga**
|
||||
@@ -380,7 +390,9 @@
|
||||
- **google:podcasts**
|
||||
- **google:podcasts:feed**
|
||||
- **GoogleDrive**
|
||||
- **GoPro**
|
||||
- **Goshgay**
|
||||
- **GoToStage**
|
||||
- **GPUTechConf**
|
||||
- **Groupon**
|
||||
- **hbo**
|
||||
@@ -466,6 +478,7 @@
|
||||
- **KinjaEmbed**
|
||||
- **KinoPoisk**
|
||||
- **KonserthusetPlay**
|
||||
- **Koo**
|
||||
- **KrasView**: Красвью
|
||||
- **Ku6**
|
||||
- **KUSI**
|
||||
@@ -539,6 +552,8 @@
|
||||
- **MedalTV**
|
||||
- **media.ccc.de**
|
||||
- **media.ccc.de:lists**
|
||||
- **Mediaite**
|
||||
- **MediaKlikk**
|
||||
- **Medialaan**
|
||||
- **Mediaset**
|
||||
- **Mediasite**
|
||||
@@ -597,6 +612,7 @@
|
||||
- **mtvservices:embedded**
|
||||
- **MTVUutisetArticle**
|
||||
- **MuenchenTV**: münchen.tv
|
||||
- **MuseScore**
|
||||
- **mva**: Microsoft Virtual Academy videos
|
||||
- **mva:course**: Microsoft Virtual Academy courses
|
||||
- **Mwave**
|
||||
@@ -613,6 +629,8 @@
|
||||
- **MyviEmbed**
|
||||
- **MyVisionTV**
|
||||
- **n-tv.de**
|
||||
- **N1Info:article**
|
||||
- **N1InfoAsset**
|
||||
- **natgeo:video**
|
||||
- **NationalGeographicTV**
|
||||
- **Naver**
|
||||
@@ -646,7 +664,8 @@
|
||||
- **NetPlus**
|
||||
- **Netzkino**
|
||||
- **Newgrounds**
|
||||
- **NewgroundsPlaylist**
|
||||
- **Newgrounds:playlist**
|
||||
- **Newgrounds:user**
|
||||
- **Newstube**
|
||||
- **NextMedia**: 蘋果日報
|
||||
- **NextMediaActionNews**: 蘋果日報 - 動新聞
|
||||
@@ -682,6 +701,7 @@
|
||||
- **NosVideo**
|
||||
- **Nova**: TN.cz, Prásk.tv, Nova.cz, Novaplus.cz, FANDA.tv, Krásná.cz and Doma.cz
|
||||
- **NovaEmbed**
|
||||
- **NovaPlay**
|
||||
- **nowness**
|
||||
- **nowness:playlist**
|
||||
- **nowness:series**
|
||||
@@ -707,11 +727,13 @@
|
||||
- **NYTimes**
|
||||
- **NYTimesArticle**
|
||||
- **NYTimesCooking**
|
||||
- **nzherald**
|
||||
- **NZZ**
|
||||
- **ocw.mit.edu**
|
||||
- **OdaTV**
|
||||
- **Odnoklassniki**
|
||||
- **OktoberfestTV**
|
||||
- **OlympicsReplay**
|
||||
- **OnDemandKorea**
|
||||
- **onet.pl**
|
||||
- **onet.tv**
|
||||
@@ -756,6 +778,7 @@
|
||||
- **pbs**: Public Broadcasting Service (PBS) and member stations: PBS: Public Broadcasting Service, APT - Alabama Public Television (WBIQ), GPB/Georgia Public Broadcasting (WGTV), Mississippi Public Broadcasting (WMPN), Nashville Public Television (WNPT), WFSU-TV (WFSU), WSRE (WSRE), WTCI (WTCI), WPBA/Channel 30 (WPBA), Alaska Public Media (KAKM), Arizona PBS (KAET), KNME-TV/Channel 5 (KNME), Vegas PBS (KLVX), AETN/ARKANSAS ETV NETWORK (KETS), KET (WKLE), WKNO/Channel 10 (WKNO), LPB/LOUISIANA PUBLIC BROADCASTING (WLPB), OETA (KETA), Ozarks Public Television (KOZK), WSIU Public Broadcasting (WSIU), KEET TV (KEET), KIXE/Channel 9 (KIXE), KPBS San Diego (KPBS), KQED (KQED), KVIE Public Television (KVIE), PBS SoCal/KOCE (KOCE), ValleyPBS (KVPT), CONNECTICUT PUBLIC TELEVISION (WEDH), KNPB Channel 5 (KNPB), SOPTV (KSYS), Rocky Mountain PBS (KRMA), KENW-TV3 (KENW), KUED Channel 7 (KUED), Wyoming PBS (KCWC), Colorado Public Television / KBDI 12 (KBDI), KBYU-TV (KBYU), Thirteen/WNET New York (WNET), WGBH/Channel 2 (WGBH), WGBY (WGBY), NJTV Public Media NJ (WNJT), WLIW21 (WLIW), mpt/Maryland Public Television (WMPB), WETA Television and Radio (WETA), WHYY (WHYY), PBS 39 (WLVT), WVPT - Your Source for PBS and More! (WVPT), Howard University Television (WHUT), WEDU PBS (WEDU), WGCU Public Media (WGCU), WPBT2 (WPBT), WUCF TV (WUCF), WUFT/Channel 5 (WUFT), WXEL/Channel 42 (WXEL), WLRN/Channel 17 (WLRN), WUSF Public Broadcasting (WUSF), ETV (WRLK), UNC-TV (WUNC), PBS Hawaii - Oceanic Cable Channel 10 (KHET), Idaho Public Television (KAID), KSPS (KSPS), OPB (KOPB), KWSU/Channel 10 & KTNW/Channel 31 (KWSU), WILL-TV (WILL), Network Knowledge - WSEC/Springfield (WSEC), WTTW11 (WTTW), Iowa Public Television/IPTV (KDIN), Nine Network (KETC), PBS39 Fort Wayne (WFWA), WFYI Indianapolis (WFYI), Milwaukee Public Television (WMVS), WNIN (WNIN), WNIT Public Television (WNIT), WPT (WPNE), WVUT/Channel 22 (WVUT), WEIU/Channel 51 (WEIU), WQPT-TV (WQPT), WYCC PBS Chicago (WYCC), WIPB-TV (WIPB), WTIU (WTIU), CET (WCET), ThinkTVNetwork (WPTD), WBGU-TV (WBGU), WGVU TV (WGVU), NET1 (KUON), Pioneer Public Television (KWCM), SDPB Television (KUSD), TPT (KTCA), KSMQ (KSMQ), KPTS/Channel 8 (KPTS), KTWU/Channel 11 (KTWU), East Tennessee PBS (WSJK), WCTE-TV (WCTE), WLJT, Channel 11 (WLJT), WOSU TV (WOSU), WOUB/WOUC (WOUB), WVPB (WVPB), WKYU-PBS (WKYU), KERA 13 (KERA), MPBN (WCBB), Mountain Lake PBS (WCFE), NHPTV (WENH), Vermont PBS (WETK), witf (WITF), WQED Multimedia (WQED), WMHT Educational Telecommunications (WMHT), Q-TV (WDCQ), WTVS Detroit Public TV (WTVS), CMU Public Television (WCMU), WKAR-TV (WKAR), WNMU-TV Public TV 13 (WNMU), WDSE - WRPT (WDSE), WGTE TV (WGTE), Lakeland Public Television (KAWE), KMOS-TV - Channels 6.1, 6.2 and 6.3 (KMOS), MontanaPBS (KUSM), KRWG/Channel 22 (KRWG), KACV (KACV), KCOS/Channel 13 (KCOS), WCNY/Channel 24 (WCNY), WNED (WNED), WPBS (WPBS), WSKG Public TV (WSKG), WXXI (WXXI), WPSU (WPSU), WVIA Public Media Studios (WVIA), WTVI (WTVI), Western Reserve PBS (WNEO), WVIZ/PBS ideastream (WVIZ), KCTS 9 (KCTS), Basin PBS (KPBT), KUHT / Channel 8 (KUHT), KLRN (KLRN), KLRU (KLRU), WTJX Channel 12 (WTJX), WCVE PBS (WCVE), KBTC Public Television (KBTC)
|
||||
- **PearVideo**
|
||||
- **PeerTube**
|
||||
- **PeerTube:Playlist**
|
||||
- **peloton**
|
||||
- **peloton:live**: Peloton Live
|
||||
- **People**
|
||||
@@ -831,6 +854,9 @@
|
||||
- **radiocanada:audiovideo**
|
||||
- **radiofrance**
|
||||
- **RadioJavan**
|
||||
- **radlive**
|
||||
- **radlive:channel**
|
||||
- **radlive:season**
|
||||
- **Rai**
|
||||
- **RaiPlay**
|
||||
- **RaiPlayLive**
|
||||
@@ -881,6 +907,7 @@
|
||||
- **RTVNH**
|
||||
- **RTVS**
|
||||
- **RUHD**
|
||||
- **RumbleChannel**
|
||||
- **RumbleEmbed**
|
||||
- **rutube**: Rutube videos
|
||||
- **rutube:channel**: Rutube channels
|
||||
@@ -955,11 +982,12 @@
|
||||
- **southpark.de**
|
||||
- **southpark.nl**
|
||||
- **southparkstudios.dk**
|
||||
- **SovietsCloset**
|
||||
- **SovietsClosetPlaylist**
|
||||
- **SpankBang**
|
||||
- **SpankBangPlaylist**
|
||||
- **Spankwire**
|
||||
- **Spiegel**
|
||||
- **sport.francetvinfo.fr**
|
||||
- **Sport5**
|
||||
- **SportBox**
|
||||
- **SportDeutschland**
|
||||
@@ -983,6 +1011,7 @@
|
||||
- **StoryFireSeries**
|
||||
- **StoryFireUser**
|
||||
- **Streamable**
|
||||
- **Streamanity**
|
||||
- **streamcloud.eu**
|
||||
- **StreamCZ**
|
||||
- **StreetVoice**
|
||||
@@ -1038,6 +1067,8 @@
|
||||
- **TheScene**
|
||||
- **TheStar**
|
||||
- **TheSun**
|
||||
- **ThetaStream**
|
||||
- **ThetaVideo**
|
||||
- **TheWeatherChannel**
|
||||
- **ThisAmericanLife**
|
||||
- **ThisAV**
|
||||
@@ -1146,6 +1177,7 @@
|
||||
- **Varzesh3**
|
||||
- **Vbox7**
|
||||
- **VeeHD**
|
||||
- **Veo**
|
||||
- **Veoh**
|
||||
- **Vesti**: Вести.Ru
|
||||
- **Vevo**
|
||||
@@ -1174,9 +1206,6 @@
|
||||
- **VidioLive**
|
||||
- **VidioPremier**
|
||||
- **VidLii**
|
||||
- **vidme**
|
||||
- **vidme:user**
|
||||
- **vidme:user:likes**
|
||||
- **vier**: vier.be and vijf.be
|
||||
- **vier:videos**
|
||||
- **viewlift**
|
||||
@@ -1228,6 +1257,7 @@
|
||||
- **VTXTV**
|
||||
- **vube**: Vube.com
|
||||
- **VuClip**
|
||||
- **Vupload**
|
||||
- **VVVVID**
|
||||
- **VVVVIDShow**
|
||||
- **VyboryMos**
|
||||
@@ -1325,6 +1355,8 @@
|
||||
- **ZDFChannel**
|
||||
- **Zee5**
|
||||
- **zee5:series**
|
||||
- **ZenYandex**
|
||||
- **ZenYandexChannel**
|
||||
- **Zhihu**
|
||||
- **zingmp3**: mp3.zing.vn
|
||||
- **zingmp3:album**
|
||||
|
||||
@@ -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,9 +649,11 @@ class TestYoutubeDL(unittest.TestCase):
|
||||
'title2': '%PATH%',
|
||||
'title3': 'foo/bar\\test',
|
||||
'title4': 'foo "bar" test',
|
||||
'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'}]
|
||||
@@ -664,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)):
|
||||
@@ -689,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)
|
||||
@@ -764,9 +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:
|
||||
@@ -788,6 +796,12 @@ class TestYoutubeDL(unittest.TestCase):
|
||||
test('%(formats.0.id.-1+id)f', '1235.000000')
|
||||
test('%(formats.0.id.-1+formats.1.id.-1)d', '3')
|
||||
|
||||
# Alternates
|
||||
test('%(title,id)s', '1234')
|
||||
test('%(width-100,height+20|def)d', '1100')
|
||||
test('%(width-100,height+width|def)s', 'def')
|
||||
test('%(timestamp-x>%H\\,%M\\,%S,timestamp>%H\\,%M\\,%S)s', '12,00,00')
|
||||
|
||||
# Laziness
|
||||
def gen():
|
||||
yield from range(5)
|
||||
@@ -992,6 +1006,7 @@ class TestYoutubeDL(unittest.TestCase):
|
||||
test_selection({'playlist_items': '2-4'}, [2, 3, 4])
|
||||
test_selection({'playlist_items': '2,4'}, [2, 4])
|
||||
test_selection({'playlist_items': '10'}, [])
|
||||
test_selection({'playlist_items': '0'}, [])
|
||||
|
||||
# Tests for https://github.com/ytdl-org/youtube-dl/issues/10591
|
||||
test_selection({'playlist_items': '2-4,3-4,3'}, [2, 3, 4])
|
||||
|
||||
@@ -7,7 +7,19 @@ import sys
|
||||
import unittest
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from yt_dlp.aes import aes_decrypt, aes_encrypt, aes_cbc_decrypt, aes_cbc_encrypt, aes_decrypt_text
|
||||
from yt_dlp.aes import (
|
||||
aes_decrypt,
|
||||
aes_encrypt,
|
||||
aes_cbc_decrypt,
|
||||
aes_cbc_decrypt_bytes,
|
||||
aes_cbc_encrypt,
|
||||
aes_ctr_decrypt,
|
||||
aes_ctr_encrypt,
|
||||
aes_gcm_decrypt_and_verify,
|
||||
aes_gcm_decrypt_and_verify_bytes,
|
||||
aes_decrypt_text
|
||||
)
|
||||
from yt_dlp.compat import compat_pycrypto_AES
|
||||
from yt_dlp.utils import bytes_to_intlist, intlist_to_bytes
|
||||
import base64
|
||||
|
||||
@@ -27,18 +39,43 @@ class TestAES(unittest.TestCase):
|
||||
self.assertEqual(decrypted, msg)
|
||||
|
||||
def test_cbc_decrypt(self):
|
||||
data = bytes_to_intlist(
|
||||
b"\x97\x92+\xe5\x0b\xc3\x18\x91ky9m&\xb3\xb5@\xe6'\xc2\x96.\xc8u\x88\xab9-[\x9e|\xf1\xcd"
|
||||
)
|
||||
decrypted = intlist_to_bytes(aes_cbc_decrypt(data, self.key, self.iv))
|
||||
data = b'\x97\x92+\xe5\x0b\xc3\x18\x91ky9m&\xb3\xb5@\xe6\x27\xc2\x96.\xc8u\x88\xab9-[\x9e|\xf1\xcd'
|
||||
decrypted = intlist_to_bytes(aes_cbc_decrypt(bytes_to_intlist(data), self.key, self.iv))
|
||||
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||
if compat_pycrypto_AES:
|
||||
decrypted = aes_cbc_decrypt_bytes(data, intlist_to_bytes(self.key), intlist_to_bytes(self.iv))
|
||||
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||
|
||||
def test_cbc_encrypt(self):
|
||||
data = bytes_to_intlist(self.secret_msg)
|
||||
encrypted = intlist_to_bytes(aes_cbc_encrypt(data, self.key, self.iv))
|
||||
self.assertEqual(
|
||||
encrypted,
|
||||
b"\x97\x92+\xe5\x0b\xc3\x18\x91ky9m&\xb3\xb5@\xe6'\xc2\x96.\xc8u\x88\xab9-[\x9e|\xf1\xcd")
|
||||
b'\x97\x92+\xe5\x0b\xc3\x18\x91ky9m&\xb3\xb5@\xe6\'\xc2\x96.\xc8u\x88\xab9-[\x9e|\xf1\xcd')
|
||||
|
||||
def test_ctr_decrypt(self):
|
||||
data = bytes_to_intlist(b'\x03\xc7\xdd\xd4\x8e\xb3\xbc\x1a*O\xdc1\x12+8Aio\xd1z\xb5#\xaf\x08')
|
||||
decrypted = intlist_to_bytes(aes_ctr_decrypt(data, self.key, self.iv))
|
||||
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||
|
||||
def test_ctr_encrypt(self):
|
||||
data = bytes_to_intlist(self.secret_msg)
|
||||
encrypted = intlist_to_bytes(aes_ctr_encrypt(data, self.key, self.iv))
|
||||
self.assertEqual(
|
||||
encrypted,
|
||||
b'\x03\xc7\xdd\xd4\x8e\xb3\xbc\x1a*O\xdc1\x12+8Aio\xd1z\xb5#\xaf\x08')
|
||||
|
||||
def test_gcm_decrypt(self):
|
||||
data = b'\x159Y\xcf5eud\x90\x9c\x85&]\x14\x1d\x0f.\x08\xb4T\xe4/\x17\xbd'
|
||||
authentication_tag = b'\xe8&I\x80rI\x07\x9d}YWuU@:e'
|
||||
|
||||
decrypted = intlist_to_bytes(aes_gcm_decrypt_and_verify(
|
||||
bytes_to_intlist(data), self.key, bytes_to_intlist(authentication_tag), self.iv[:12]))
|
||||
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||
if compat_pycrypto_AES:
|
||||
decrypted = aes_gcm_decrypt_and_verify_bytes(
|
||||
data, intlist_to_bytes(self.key), authentication_tag, intlist_to_bytes(self.iv[:12]))
|
||||
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||
|
||||
def test_decrypt_text(self):
|
||||
password = intlist_to_bytes(self.key).decode('utf-8')
|
||||
|
||||
@@ -3,16 +3,28 @@ from datetime import datetime, timezone
|
||||
|
||||
from yt_dlp import cookies
|
||||
from yt_dlp.cookies import (
|
||||
CRYPTO_AVAILABLE,
|
||||
LinuxChromeCookieDecryptor,
|
||||
MacChromeCookieDecryptor,
|
||||
WindowsChromeCookieDecryptor,
|
||||
YDLLogger,
|
||||
parse_safari_cookies,
|
||||
pbkdf2_sha1,
|
||||
)
|
||||
|
||||
|
||||
class Logger:
|
||||
def debug(self, message):
|
||||
print(f'[verbose] {message}')
|
||||
|
||||
def info(self, message):
|
||||
print(message)
|
||||
|
||||
def warning(self, message, only_once=False):
|
||||
self.error(message)
|
||||
|
||||
def error(self, message):
|
||||
raise Exception(message)
|
||||
|
||||
|
||||
class MonkeyPatch:
|
||||
def __init__(self, module, temporary_values):
|
||||
self._module = module
|
||||
@@ -42,7 +54,7 @@ class TestCookies(unittest.TestCase):
|
||||
with MonkeyPatch(cookies, {'_get_linux_keyring_password': lambda *args, **kwargs: b''}):
|
||||
encrypted_value = b'v10\xccW%\xcd\xe6\xe6\x9fM" \xa7\xb0\xca\xe4\x07\xd6'
|
||||
value = 'USD'
|
||||
decryptor = LinuxChromeCookieDecryptor('Chrome', YDLLogger())
|
||||
decryptor = LinuxChromeCookieDecryptor('Chrome', Logger())
|
||||
self.assertEqual(decryptor.decrypt(encrypted_value), value)
|
||||
|
||||
def test_chrome_cookie_decryptor_linux_v11(self):
|
||||
@@ -50,24 +62,23 @@ class TestCookies(unittest.TestCase):
|
||||
'KEYRING_AVAILABLE': True}):
|
||||
encrypted_value = b'v11#\x81\x10>`w\x8f)\xc0\xb2\xc1\r\xf4\x1al\xdd\x93\xfd\xf8\xf8N\xf2\xa9\x83\xf1\xe9o\x0elVQd'
|
||||
value = 'tz=Europe.London'
|
||||
decryptor = LinuxChromeCookieDecryptor('Chrome', YDLLogger())
|
||||
decryptor = LinuxChromeCookieDecryptor('Chrome', Logger())
|
||||
self.assertEqual(decryptor.decrypt(encrypted_value), value)
|
||||
|
||||
@unittest.skipIf(not CRYPTO_AVAILABLE, 'cryptography library not available')
|
||||
def test_chrome_cookie_decryptor_windows_v10(self):
|
||||
with MonkeyPatch(cookies, {
|
||||
'_get_windows_v10_key': lambda *args, **kwargs: b'Y\xef\xad\xad\xeerp\xf0Y\xe6\x9b\x12\xc2<z\x16]\n\xbb\xb8\xcb\xd7\x9bA\xc3\x14e\x99{\xd6\xf4&'
|
||||
}):
|
||||
encrypted_value = b'v10T\xb8\xf3\xb8\x01\xa7TtcV\xfc\x88\xb8\xb8\xef\x05\xb5\xfd\x18\xc90\x009\xab\xb1\x893\x85)\x87\xe1\xa9-\xa3\xad='
|
||||
value = '32101439'
|
||||
decryptor = WindowsChromeCookieDecryptor('', YDLLogger())
|
||||
decryptor = WindowsChromeCookieDecryptor('', Logger())
|
||||
self.assertEqual(decryptor.decrypt(encrypted_value), value)
|
||||
|
||||
def test_chrome_cookie_decryptor_mac_v10(self):
|
||||
with MonkeyPatch(cookies, {'_get_mac_keyring_password': lambda *args, **kwargs: b'6eIDUdtKAacvlHwBVwvg/Q=='}):
|
||||
encrypted_value = b'v10\xb3\xbe\xad\xa1[\x9fC\xa1\x98\xe0\x9a\x01\xd9\xcf\xbfc'
|
||||
value = '2021-06-01-22'
|
||||
decryptor = MacChromeCookieDecryptor('', YDLLogger())
|
||||
decryptor = MacChromeCookieDecryptor('', Logger())
|
||||
self.assertEqual(decryptor.decrypt(encrypted_value), value)
|
||||
|
||||
def test_safari_cookie_parsing(self):
|
||||
|
||||
@@ -461,11 +461,23 @@ class TestModifyChaptersPP(unittest.TestCase):
|
||||
self._remove_marked_arrange_sponsors_test_impl(
|
||||
chapters, self._chapters([2, 2.5], ['c1', 'c3']), cuts)
|
||||
|
||||
def test_remove_marked_arrange_sponsors_SingleTinyChapterIsPreserved(self):
|
||||
cuts = [self._chapter(0.5, 2, remove=True)]
|
||||
chapters = self._chapters([2], ['c']) + cuts
|
||||
self._remove_marked_arrange_sponsors_test_impl(
|
||||
chapters, self._chapters([0.5], ['c']), cuts)
|
||||
|
||||
def test_remove_marked_arrange_sponsors_TinyChapterAtTheStartPrependedToTheNext(self):
|
||||
cuts = [self._chapter(0.5, 2, remove=True)]
|
||||
chapters = self._chapters([2, 4], ['c1', 'c2']) + cuts
|
||||
self._remove_marked_arrange_sponsors_test_impl(
|
||||
chapters, self._chapters([2.5], ['c2']), cuts)
|
||||
|
||||
def test_remove_marked_arrange_sponsors_TinyChaptersResultingFromSponsorOverlapAreIgnored(self):
|
||||
chapters = self._chapters([1, 3, 4], ['c1', 'c2', 'c3']) + [
|
||||
self._sponsor_chapter(1.5, 2.5, 'sponsor')]
|
||||
self._remove_marked_arrange_sponsors_test_impl(
|
||||
chapters, self._chapters([1.5, 3, 4], ['c1', '[SponsorBlock]: Sponsor', 'c3']), [])
|
||||
chapters, self._chapters([1.5, 2.5, 4], ['c1', '[SponsorBlock]: Sponsor', 'c3']), [])
|
||||
|
||||
def test_remove_marked_arrange_sponsors_TinySponsorsOverlapsAreIgnored(self):
|
||||
chapters = self._chapters([2, 3, 5], ['c1', 'c2', 'c3']) + [
|
||||
@@ -476,6 +488,26 @@ class TestModifyChaptersPP(unittest.TestCase):
|
||||
chapters, self._chapters([1, 3, 4, 5], [
|
||||
'c1', '[SponsorBlock]: Sponsor', '[SponsorBlock]: Unpaid/Self Promotion', 'c3']), [])
|
||||
|
||||
def test_remove_marked_arrange_sponsors_TinySponsorsPrependedToTheNextSponsor(self):
|
||||
chapters = self._chapters([4], ['c']) + [
|
||||
self._sponsor_chapter(1.5, 2, 'sponsor'),
|
||||
self._sponsor_chapter(2, 4, 'selfpromo')
|
||||
]
|
||||
self._remove_marked_arrange_sponsors_test_impl(
|
||||
chapters, self._chapters([1.5, 4], ['c', '[SponsorBlock]: Unpaid/Self Promotion']), [])
|
||||
|
||||
def test_remove_marked_arrange_sponsors_SmallestSponsorInTheOverlapGetsNamed(self):
|
||||
self._pp._sponsorblock_chapter_title = '[SponsorBlock]: %(name)s'
|
||||
chapters = self._chapters([10], ['c']) + [
|
||||
self._sponsor_chapter(2, 8, 'sponsor'),
|
||||
self._sponsor_chapter(4, 6, 'selfpromo')
|
||||
]
|
||||
self._remove_marked_arrange_sponsors_test_impl(
|
||||
chapters, self._chapters([2, 4, 6, 8, 10], [
|
||||
'c', '[SponsorBlock]: Sponsor', '[SponsorBlock]: Unpaid/Self Promotion',
|
||||
'[SponsorBlock]: Sponsor', 'c'
|
||||
]), [])
|
||||
|
||||
def test_make_concat_opts_CommonCase(self):
|
||||
sponsor_chapters = [self._chapter(1, 2, 's1'), self._chapter(10, 20, 's2')]
|
||||
expected = '''ffconcat version 1.0
|
||||
|
||||
@@ -19,6 +19,7 @@ from yt_dlp.extractor import (
|
||||
CeskaTelevizeIE,
|
||||
LyndaIE,
|
||||
NPOIE,
|
||||
PBSIE,
|
||||
ComedyCentralIE,
|
||||
NRKTVIE,
|
||||
RaiPlayIE,
|
||||
@@ -372,5 +373,42 @@ class TestDemocracynowSubtitles(BaseTestSubtitles):
|
||||
self.assertEqual(md5(subtitles['en']), 'acaca989e24a9e45a6719c9b3d60815c')
|
||||
|
||||
|
||||
@is_download_test
|
||||
class TestPBSSubtitles(BaseTestSubtitles):
|
||||
url = 'https://www.pbs.org/video/how-fantasy-reflects-our-world-picecq/'
|
||||
IE = PBSIE
|
||||
|
||||
def test_allsubtitles(self):
|
||||
self.DL.params['writesubtitles'] = True
|
||||
self.DL.params['allsubtitles'] = True
|
||||
subtitles = self.getSubtitles()
|
||||
self.assertEqual(set(subtitles.keys()), set(['en']))
|
||||
|
||||
def test_subtitles_dfxp_format(self):
|
||||
self.DL.params['writesubtitles'] = True
|
||||
self.DL.params['subtitlesformat'] = 'dfxp'
|
||||
subtitles = self.getSubtitles()
|
||||
self.assertIn(md5(subtitles['en']), ['643b034254cdc3768ff1e750b6b5873b'])
|
||||
|
||||
def test_subtitles_vtt_format(self):
|
||||
self.DL.params['writesubtitles'] = True
|
||||
self.DL.params['subtitlesformat'] = 'vtt'
|
||||
subtitles = self.getSubtitles()
|
||||
self.assertIn(
|
||||
md5(subtitles['en']), ['937a05711555b165d4c55a9667017045', 'f49ea998d6824d94959c8152a368ff73'])
|
||||
|
||||
def test_subtitles_srt_format(self):
|
||||
self.DL.params['writesubtitles'] = True
|
||||
self.DL.params['subtitlesformat'] = 'srt'
|
||||
subtitles = self.getSubtitles()
|
||||
self.assertIn(md5(subtitles['en']), ['2082c21b43759d9bf172931b2f2ca371'])
|
||||
|
||||
def test_subtitles_sami_format(self):
|
||||
self.DL.params['writesubtitles'] = True
|
||||
self.DL.params['subtitlesformat'] = 'sami'
|
||||
subtitles = self.getSubtitles()
|
||||
self.assertIn(md5(subtitles['en']), ['4256b16ac7da6a6780fafd04294e85cd'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -248,7 +248,7 @@ def _real_main(argv=None):
|
||||
if opts.cookiesfrombrowser is not None:
|
||||
opts.cookiesfrombrowser = [
|
||||
part.strip() or None for part in opts.cookiesfrombrowser.split(':', 1)]
|
||||
if opts.cookiesfrombrowser[0] not in SUPPORTED_BROWSERS:
|
||||
if opts.cookiesfrombrowser[0].lower() not in SUPPORTED_BROWSERS:
|
||||
parser.error('unsupported browser specified for cookies')
|
||||
|
||||
if opts.date is not None:
|
||||
@@ -279,7 +279,7 @@ def _real_main(argv=None):
|
||||
setattr(opts, opt_name, default)
|
||||
return None
|
||||
|
||||
set_default_compat('abort-on-error', 'ignoreerrors')
|
||||
set_default_compat('abort-on-error', 'ignoreerrors', 'only_download')
|
||||
set_default_compat('no-playlist-metafiles', 'allow_playlist_files')
|
||||
set_default_compat('no-clean-infojson', 'clean_infojson')
|
||||
if 'format-sort' in compat_opts:
|
||||
@@ -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',
|
||||
@@ -575,6 +580,7 @@ def _real_main(argv=None):
|
||||
|
||||
ydl_opts = {
|
||||
'usenetrc': opts.usenetrc,
|
||||
'netrc_location': opts.netrc_location,
|
||||
'username': opts.username,
|
||||
'password': opts.password,
|
||||
'twofactor': opts.twofactor,
|
||||
@@ -630,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,
|
||||
@@ -728,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:
|
||||
|
||||
269
yt_dlp/aes.py
269
yt_dlp/aes.py
@@ -2,36 +2,68 @@ from __future__ import unicode_literals
|
||||
|
||||
from math import ceil
|
||||
|
||||
from .compat import compat_b64decode
|
||||
from .compat import compat_b64decode, compat_pycrypto_AES
|
||||
from .utils import bytes_to_intlist, intlist_to_bytes
|
||||
|
||||
|
||||
if compat_pycrypto_AES:
|
||||
def aes_cbc_decrypt_bytes(data, key, iv):
|
||||
""" Decrypt bytes with AES-CBC using pycryptodome """
|
||||
return compat_pycrypto_AES.new(key, compat_pycrypto_AES.MODE_CBC, iv).decrypt(data)
|
||||
|
||||
def aes_gcm_decrypt_and_verify_bytes(data, key, tag, nonce):
|
||||
""" Decrypt bytes with AES-GCM using pycryptodome """
|
||||
return compat_pycrypto_AES.new(key, compat_pycrypto_AES.MODE_GCM, nonce).decrypt_and_verify(data, tag)
|
||||
|
||||
else:
|
||||
def aes_cbc_decrypt_bytes(data, key, iv):
|
||||
""" Decrypt bytes with AES-CBC using native implementation since pycryptodome is unavailable """
|
||||
return intlist_to_bytes(aes_cbc_decrypt(*map(bytes_to_intlist, (data, key, iv))))
|
||||
|
||||
def aes_gcm_decrypt_and_verify_bytes(data, key, tag, nonce):
|
||||
""" Decrypt bytes with AES-GCM using native implementation since pycryptodome is unavailable """
|
||||
return intlist_to_bytes(aes_gcm_decrypt_and_verify(*map(bytes_to_intlist, (data, key, tag, nonce))))
|
||||
|
||||
|
||||
BLOCK_SIZE_BYTES = 16
|
||||
|
||||
|
||||
def aes_ctr_decrypt(data, key, counter):
|
||||
def aes_ctr_decrypt(data, key, iv):
|
||||
"""
|
||||
Decrypt with aes in counter mode
|
||||
|
||||
@param {int[]} data cipher
|
||||
@param {int[]} key 16/24/32-Byte cipher key
|
||||
@param {instance} counter Instance whose next_value function (@returns {int[]} 16-Byte block)
|
||||
returns the next counter block
|
||||
@param {int[]} iv 16-Byte initialization vector
|
||||
@returns {int[]} decrypted data
|
||||
"""
|
||||
return aes_ctr_encrypt(data, key, iv)
|
||||
|
||||
|
||||
def aes_ctr_encrypt(data, key, iv):
|
||||
"""
|
||||
Encrypt with aes in counter mode
|
||||
|
||||
@param {int[]} data cleartext
|
||||
@param {int[]} key 16/24/32-Byte cipher key
|
||||
@param {int[]} iv 16-Byte initialization vector
|
||||
@returns {int[]} encrypted data
|
||||
"""
|
||||
expanded_key = key_expansion(key)
|
||||
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||
counter = iter_vector(iv)
|
||||
|
||||
decrypted_data = []
|
||||
encrypted_data = []
|
||||
for i in range(block_count):
|
||||
counter_block = counter.next_value()
|
||||
counter_block = next(counter)
|
||||
block = data[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES]
|
||||
block += [0] * (BLOCK_SIZE_BYTES - len(block))
|
||||
|
||||
cipher_counter_block = aes_encrypt(counter_block, expanded_key)
|
||||
decrypted_data += xor(block, cipher_counter_block)
|
||||
decrypted_data = decrypted_data[:len(data)]
|
||||
encrypted_data += xor(block, cipher_counter_block)
|
||||
encrypted_data = encrypted_data[:len(data)]
|
||||
|
||||
return decrypted_data
|
||||
return encrypted_data
|
||||
|
||||
|
||||
def aes_cbc_decrypt(data, key, iv):
|
||||
@@ -88,39 +120,47 @@ def aes_cbc_encrypt(data, key, iv):
|
||||
return encrypted_data
|
||||
|
||||
|
||||
def key_expansion(data):
|
||||
def aes_gcm_decrypt_and_verify(data, key, tag, nonce):
|
||||
"""
|
||||
Generate key schedule
|
||||
Decrypt with aes in GBM mode and checks authenticity using tag
|
||||
|
||||
@param {int[]} data 16/24/32-Byte cipher key
|
||||
@returns {int[]} 176/208/240-Byte expanded key
|
||||
@param {int[]} data cipher
|
||||
@param {int[]} key 16-Byte cipher key
|
||||
@param {int[]} tag authentication tag
|
||||
@param {int[]} nonce IV (recommended 12-Byte)
|
||||
@returns {int[]} decrypted data
|
||||
"""
|
||||
data = data[:] # copy
|
||||
rcon_iteration = 1
|
||||
key_size_bytes = len(data)
|
||||
expanded_key_size_bytes = (key_size_bytes // 4 + 7) * BLOCK_SIZE_BYTES
|
||||
|
||||
while len(data) < expanded_key_size_bytes:
|
||||
temp = data[-4:]
|
||||
temp = key_schedule_core(temp, rcon_iteration)
|
||||
rcon_iteration += 1
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
# XXX: check aes, gcm param
|
||||
|
||||
for _ in range(3):
|
||||
temp = data[-4:]
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
hash_subkey = aes_encrypt([0] * BLOCK_SIZE_BYTES, key_expansion(key))
|
||||
|
||||
if key_size_bytes == 32:
|
||||
temp = data[-4:]
|
||||
temp = sub_bytes(temp)
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
if len(nonce) == 12:
|
||||
j0 = nonce + [0, 0, 0, 1]
|
||||
else:
|
||||
fill = (BLOCK_SIZE_BYTES - (len(nonce) % BLOCK_SIZE_BYTES)) % BLOCK_SIZE_BYTES + 8
|
||||
ghash_in = nonce + [0] * fill + bytes_to_intlist((8 * len(nonce)).to_bytes(8, 'big'))
|
||||
j0 = ghash(hash_subkey, ghash_in)
|
||||
|
||||
for _ in range(3 if key_size_bytes == 32 else 2 if key_size_bytes == 24 else 0):
|
||||
temp = data[-4:]
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
data = data[:expanded_key_size_bytes]
|
||||
# TODO: add nonce support to aes_ctr_decrypt
|
||||
|
||||
return data
|
||||
# nonce_ctr = j0[:12]
|
||||
iv_ctr = inc(j0)
|
||||
|
||||
decrypted_data = aes_ctr_decrypt(data, key, iv_ctr + [0] * (BLOCK_SIZE_BYTES - len(iv_ctr)))
|
||||
pad_len = len(data) // 16 * 16
|
||||
s_tag = ghash(
|
||||
hash_subkey,
|
||||
data
|
||||
+ [0] * (BLOCK_SIZE_BYTES - len(data) + pad_len) # pad
|
||||
+ bytes_to_intlist((0 * 8).to_bytes(8, 'big') # length of associated data
|
||||
+ ((len(data) * 8).to_bytes(8, 'big'))) # length of data
|
||||
)
|
||||
|
||||
if tag != aes_ctr_encrypt(s_tag, key, j0):
|
||||
raise ValueError("Mismatching authentication tag")
|
||||
|
||||
return decrypted_data
|
||||
|
||||
|
||||
def aes_encrypt(data, expanded_key):
|
||||
@@ -138,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
|
||||
@@ -157,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])
|
||||
@@ -189,15 +229,7 @@ def aes_decrypt_text(data, password, key_size_bytes):
|
||||
nonce = data[:NONCE_LENGTH_BYTES]
|
||||
cipher = data[NONCE_LENGTH_BYTES:]
|
||||
|
||||
class Counter(object):
|
||||
__value = nonce + [0] * (BLOCK_SIZE_BYTES - NONCE_LENGTH_BYTES)
|
||||
|
||||
def next_value(self):
|
||||
temp = self.__value
|
||||
self.__value = inc(self.__value)
|
||||
return temp
|
||||
|
||||
decrypted_data = aes_ctr_decrypt(cipher, key, Counter())
|
||||
decrypted_data = aes_ctr_decrypt(cipher, key, nonce + [0] * (BLOCK_SIZE_BYTES - NONCE_LENGTH_BYTES))
|
||||
plaintext = intlist_to_bytes(decrypted_data)
|
||||
|
||||
return plaintext
|
||||
@@ -278,6 +310,47 @@ RIJNDAEL_LOG_TABLE = (0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1a, 0xc6, 0x4b, 0xc7
|
||||
0x67, 0x4a, 0xed, 0xde, 0xc5, 0x31, 0xfe, 0x18, 0x0d, 0x63, 0x8c, 0x80, 0xc0, 0xf7, 0x70, 0x07)
|
||||
|
||||
|
||||
def key_expansion(data):
|
||||
"""
|
||||
Generate key schedule
|
||||
|
||||
@param {int[]} data 16/24/32-Byte cipher key
|
||||
@returns {int[]} 176/208/240-Byte expanded key
|
||||
"""
|
||||
data = data[:] # copy
|
||||
rcon_iteration = 1
|
||||
key_size_bytes = len(data)
|
||||
expanded_key_size_bytes = (key_size_bytes // 4 + 7) * BLOCK_SIZE_BYTES
|
||||
|
||||
while len(data) < expanded_key_size_bytes:
|
||||
temp = data[-4:]
|
||||
temp = key_schedule_core(temp, rcon_iteration)
|
||||
rcon_iteration += 1
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
|
||||
for _ in range(3):
|
||||
temp = data[-4:]
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
|
||||
if key_size_bytes == 32:
|
||||
temp = data[-4:]
|
||||
temp = sub_bytes(temp)
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
|
||||
for _ in range(3 if key_size_bytes == 32 else 2 if key_size_bytes == 24 else 0):
|
||||
temp = data[-4:]
|
||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
||||
data = data[:expanded_key_size_bytes]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def iter_vector(iv):
|
||||
while True:
|
||||
yield iv
|
||||
iv = inc(iv)
|
||||
|
||||
|
||||
def sub_bytes(data):
|
||||
return [SBOX[x] for x in data]
|
||||
|
||||
@@ -302,48 +375,36 @@ 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):
|
||||
return [data[((column - row) & 0b11) * 4 + row] for column in range(4) for row in range(4)]
|
||||
|
||||
|
||||
def shift_block(data):
|
||||
data_shifted = []
|
||||
for column in range(4):
|
||||
for row in range(4):
|
||||
data_shifted.append(data[((column - row) & 0b11) * 4 + row])
|
||||
|
||||
bit = 0
|
||||
for n in data:
|
||||
if bit:
|
||||
n |= 0x100
|
||||
bit = n & 1
|
||||
n >>= 1
|
||||
data_shifted.append(n)
|
||||
|
||||
return data_shifted
|
||||
|
||||
|
||||
@@ -358,4 +419,50 @@ def inc(data):
|
||||
return data
|
||||
|
||||
|
||||
__all__ = ['aes_encrypt', 'key_expansion', 'aes_ctr_decrypt', 'aes_cbc_decrypt', 'aes_decrypt_text']
|
||||
def block_product(block_x, block_y):
|
||||
# NIST SP 800-38D, Algorithm 1
|
||||
|
||||
if len(block_x) != BLOCK_SIZE_BYTES or len(block_y) != BLOCK_SIZE_BYTES:
|
||||
raise ValueError("Length of blocks need to be %d bytes" % BLOCK_SIZE_BYTES)
|
||||
|
||||
block_r = [0xE1] + [0] * (BLOCK_SIZE_BYTES - 1)
|
||||
block_v = block_y[:]
|
||||
block_z = [0] * BLOCK_SIZE_BYTES
|
||||
|
||||
for i in block_x:
|
||||
for bit in range(7, -1, -1):
|
||||
if i & (1 << bit):
|
||||
block_z = xor(block_z, block_v)
|
||||
|
||||
do_xor = block_v[-1] & 1
|
||||
block_v = shift_block(block_v)
|
||||
if do_xor:
|
||||
block_v = xor(block_v, block_r)
|
||||
|
||||
return block_z
|
||||
|
||||
|
||||
def ghash(subkey, data):
|
||||
# NIST SP 800-38D, Algorithm 2
|
||||
|
||||
if len(data) % BLOCK_SIZE_BYTES:
|
||||
raise ValueError("Length of data should be %d bytes" % BLOCK_SIZE_BYTES)
|
||||
|
||||
last_y = [0] * BLOCK_SIZE_BYTES
|
||||
for i in range(0, len(data), BLOCK_SIZE_BYTES):
|
||||
block = data[i : i + BLOCK_SIZE_BYTES] # noqa: E203
|
||||
last_y = block_product(xor(last_y, block), subkey)
|
||||
|
||||
return last_y
|
||||
|
||||
|
||||
__all__ = [
|
||||
'aes_ctr_decrypt',
|
||||
'aes_cbc_decrypt',
|
||||
'aes_cbc_decrypt_bytes',
|
||||
'aes_decrypt_text',
|
||||
'aes_encrypt',
|
||||
'aes_gcm_decrypt_and_verify',
|
||||
'aes_gcm_decrypt_and_verify_bytes',
|
||||
'key_expansion'
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -130,6 +132,39 @@ except AttributeError:
|
||||
asyncio.run = compat_asyncio_run
|
||||
|
||||
|
||||
# Python 3.8+ does not honor %HOME% on windows, but this breaks compatibility with youtube-dl
|
||||
# See https://github.com/yt-dlp/yt-dlp/issues/792
|
||||
# https://docs.python.org/3/library/os.path.html#os.path.expanduser
|
||||
if compat_os_name in ('nt', 'ce') and 'HOME' in os.environ:
|
||||
_userhome = os.environ['HOME']
|
||||
|
||||
def compat_expanduser(path):
|
||||
if not path.startswith('~'):
|
||||
return path
|
||||
i = path.replace('\\', '/', 1).find('/') # ~user
|
||||
if i < 0:
|
||||
i = len(path)
|
||||
userhome = os.path.join(os.path.dirname(_userhome), path[1:i]) if i > 1 else _userhome
|
||||
return userhome + path[i:]
|
||||
else:
|
||||
compat_expanduser = os.path.expanduser
|
||||
|
||||
|
||||
try:
|
||||
from Cryptodome.Cipher import AES as compat_pycrypto_AES
|
||||
except ImportError:
|
||||
try:
|
||||
from Crypto.Cipher import AES as compat_pycrypto_AES
|
||||
except ImportError:
|
||||
compat_pycrypto_AES = None
|
||||
|
||||
|
||||
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
|
||||
@@ -152,7 +187,6 @@ compat_cookies = http.cookies
|
||||
compat_cookies_SimpleCookie = compat_cookies.SimpleCookie
|
||||
compat_etree_Element = etree.Element
|
||||
compat_etree_register_namespace = etree.register_namespace
|
||||
compat_expanduser = os.path.expanduser
|
||||
compat_get_terminal_size = shutil.get_terminal_size
|
||||
compat_getenv = os.getenv
|
||||
compat_getpass = getpass.getpass
|
||||
@@ -224,6 +258,7 @@ __all__ = [
|
||||
'compat_os_name',
|
||||
'compat_parse_qs',
|
||||
'compat_print',
|
||||
'compat_pycrypto_AES',
|
||||
'compat_realpath',
|
||||
'compat_setenv',
|
||||
'compat_shlex_quote',
|
||||
@@ -252,5 +287,6 @@ __all__ = [
|
||||
'compat_xml_parse_error',
|
||||
'compat_xpath',
|
||||
'compat_zip',
|
||||
'windows_enable_vt_mode',
|
||||
'workaround_optparse_bug9161',
|
||||
]
|
||||
|
||||
@@ -9,16 +9,14 @@ import tempfile
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from hashlib import pbkdf2_hmac
|
||||
|
||||
from yt_dlp.aes import aes_cbc_decrypt
|
||||
from yt_dlp.compat import (
|
||||
from .aes import aes_cbc_decrypt_bytes, aes_gcm_decrypt_and_verify_bytes
|
||||
from .compat import (
|
||||
compat_b64decode,
|
||||
compat_cookiejar_Cookie,
|
||||
)
|
||||
from yt_dlp.utils import (
|
||||
from .utils import (
|
||||
bug_reports_message,
|
||||
bytes_to_intlist,
|
||||
expand_path,
|
||||
intlist_to_bytes,
|
||||
process_communicate_or_kill,
|
||||
YoutubeDLCookieJar,
|
||||
)
|
||||
@@ -32,12 +30,6 @@ except ImportError:
|
||||
SQLITE_AVAILABLE = False
|
||||
|
||||
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
CRYPTO_AVAILABLE = True
|
||||
except ImportError:
|
||||
CRYPTO_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import keyring
|
||||
KEYRING_AVAILABLE = True
|
||||
@@ -123,7 +115,7 @@ def _extract_firefox_cookies(profile, logger):
|
||||
cookie_database_path = _find_most_recently_used_file(search_root, 'cookies.sqlite')
|
||||
if cookie_database_path is None:
|
||||
raise FileNotFoundError('could not find firefox cookies database in {}'.format(search_root))
|
||||
logger.debug('extracting from: "{}"'.format(cookie_database_path))
|
||||
logger.debug('Extracting cookies from: "{}"'.format(cookie_database_path))
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='youtube_dl') as tmpdir:
|
||||
cursor = None
|
||||
@@ -240,7 +232,7 @@ def _extract_chrome_cookies(browser_name, profile, logger):
|
||||
cookie_database_path = _find_most_recently_used_file(search_root, 'Cookies')
|
||||
if cookie_database_path is None:
|
||||
raise FileNotFoundError('could not find {} cookies database in "{}"'.format(browser_name, search_root))
|
||||
logger.debug('extracting from: "{}"'.format(cookie_database_path))
|
||||
logger.debug('Extracting cookies from: "{}"'.format(cookie_database_path))
|
||||
|
||||
decryptor = get_cookie_decryptor(config['browser_dir'], config['keyring_name'], logger)
|
||||
|
||||
@@ -361,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
|
||||
@@ -400,11 +392,6 @@ class WindowsChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||
if self._v10_key is None:
|
||||
self._logger.warning('cannot decrypt v10 cookies: no key found', only_once=True)
|
||||
return None
|
||||
elif not CRYPTO_AVAILABLE:
|
||||
self._logger.warning('cannot decrypt cookie as the `pycryptodome` module is not installed. '
|
||||
'Please install by running `python3 -m pip install pycryptodome`',
|
||||
only_once=True)
|
||||
return None
|
||||
|
||||
# https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/os_crypt_win.cc
|
||||
# kNonceLength
|
||||
@@ -559,7 +546,7 @@ def _parse_safari_cookies_record(data, jar, logger):
|
||||
p.skip_to(value_offset)
|
||||
value = p.read_cstring()
|
||||
except UnicodeDecodeError:
|
||||
logger.warning('failed to parse cookie because UTF-8 decoding failed')
|
||||
logger.warning('failed to parse 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')
|
||||
@@ -605,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'
|
||||
@@ -618,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
|
||||
|
||||
|
||||
@@ -648,29 +640,26 @@ def pbkdf2_sha1(password, salt, iterations, key_length):
|
||||
|
||||
|
||||
def _decrypt_aes_cbc(ciphertext, key, logger, initialization_vector=b' ' * 16):
|
||||
plaintext = aes_cbc_decrypt(bytes_to_intlist(ciphertext),
|
||||
bytes_to_intlist(key),
|
||||
bytes_to_intlist(initialization_vector))
|
||||
plaintext = aes_cbc_decrypt_bytes(ciphertext, key, initialization_vector)
|
||||
padding_length = plaintext[-1]
|
||||
try:
|
||||
return intlist_to_bytes(plaintext[:-padding_length]).decode('utf-8')
|
||||
return plaintext[:-padding_length].decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
logger.warning('failed to decrypt cookie because UTF-8 decoding failed. Possibly the key is wrong?')
|
||||
logger.warning('failed to decrypt cookie (AES-CBC) because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
|
||||
return None
|
||||
|
||||
|
||||
def _decrypt_aes_gcm(ciphertext, key, nonce, authentication_tag, logger):
|
||||
cipher = AES.new(key, AES.MODE_GCM, nonce)
|
||||
try:
|
||||
plaintext = cipher.decrypt_and_verify(ciphertext, authentication_tag)
|
||||
plaintext = aes_gcm_decrypt_and_verify_bytes(ciphertext, key, authentication_tag, nonce)
|
||||
except ValueError:
|
||||
logger.warning('failed to decrypt cookie because the MAC check failed. Possibly the key is wrong?')
|
||||
logger.warning('failed to decrypt cookie (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?')
|
||||
logger.warning('failed to decrypt cookie (AES-GCM) because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
|
||||
return None
|
||||
|
||||
|
||||
@@ -698,7 +687,7 @@ def _decrypt_windows_dpapi(ciphertext, logger):
|
||||
ctypes.byref(blob_out) # pDataOut
|
||||
)
|
||||
if not ret:
|
||||
logger.warning('failed to decrypt with DPAPI')
|
||||
logger.warning('failed to decrypt with DPAPI', only_once=True)
|
||||
return None
|
||||
|
||||
result = ctypes.string_at(blob_out.pbData, blob_out.cbData)
|
||||
@@ -748,6 +737,7 @@ def _is_path(value):
|
||||
|
||||
|
||||
def _parse_browser_specification(browser_name, profile=None):
|
||||
browser_name = browser_name.lower()
|
||||
if browser_name not in SUPPORTED_BROWSERS:
|
||||
raise ValueError(f'unsupported browser: "{browser_name}"')
|
||||
if profile is not None and _is_path(profile):
|
||||
|
||||
@@ -3,11 +3,9 @@ from __future__ import division, unicode_literals
|
||||
import copy
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import random
|
||||
|
||||
from ..compat import compat_os_name
|
||||
from ..utils import (
|
||||
decodeArgument,
|
||||
encodeFilename,
|
||||
@@ -16,6 +14,12 @@ from ..utils import (
|
||||
shell_quote,
|
||||
timeconvert,
|
||||
)
|
||||
from ..minicurses import (
|
||||
MultilineLogger,
|
||||
MultilinePrinter,
|
||||
QuietMultilinePrinter,
|
||||
BreaklineStatusPrinter
|
||||
)
|
||||
|
||||
|
||||
class FileDownloader(object):
|
||||
@@ -39,8 +43,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.
|
||||
@@ -56,6 +58,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.
|
||||
"""
|
||||
@@ -68,6 +71,7 @@ class FileDownloader(object):
|
||||
self.ydl = ydl
|
||||
self._progress_hooks = []
|
||||
self.params = params
|
||||
self._prepare_multiline_status()
|
||||
self.add_progress_hook(self.report_progress)
|
||||
|
||||
@staticmethod
|
||||
@@ -236,39 +240,46 @@ class FileDownloader(object):
|
||||
"""Report destination filename."""
|
||||
self.to_screen('[download] Destination: ' + filename)
|
||||
|
||||
def _report_progress_status(self, msg, is_last_line=False):
|
||||
fullmsg = '[download] ' + msg
|
||||
if self.params.get('progress_with_newline', False):
|
||||
self.to_screen(fullmsg)
|
||||
def _prepare_multiline_status(self, lines=1):
|
||||
if self.params.get('noprogress'):
|
||||
self._multiline = QuietMultilinePrinter()
|
||||
elif self.ydl.params.get('logger'):
|
||||
self._multiline = MultilineLogger(self.ydl.params['logger'], lines)
|
||||
elif self.params.get('progress_with_newline'):
|
||||
self._multiline = BreaklineStatusPrinter(self.ydl._screen_file, lines)
|
||||
else:
|
||||
if compat_os_name == 'nt':
|
||||
prev_len = getattr(self, '_report_progress_prev_line_length',
|
||||
0)
|
||||
if prev_len > len(fullmsg):
|
||||
fullmsg += ' ' * (prev_len - len(fullmsg))
|
||||
self._report_progress_prev_line_length = len(fullmsg)
|
||||
clear_line = '\r'
|
||||
else:
|
||||
clear_line = ('\r\x1b[K' if sys.stderr.isatty() else '\r')
|
||||
self.to_screen(clear_line + fullmsg, skip_eol=not is_last_line)
|
||||
self.to_console_title('yt-dlp ' + msg)
|
||||
self._multiline = MultilinePrinter(self.ydl._screen_file, lines, not self.params.get('quiet'))
|
||||
|
||||
def _finish_multiline_status(self):
|
||||
self._multiline.end()
|
||||
|
||||
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)
|
||||
|
||||
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':
|
||||
@@ -310,8 +321,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)
|
||||
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."""
|
||||
@@ -383,7 +394,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."""
|
||||
|
||||
@@ -6,13 +6,7 @@ import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
can_decrypt_frag = True
|
||||
except ImportError:
|
||||
can_decrypt_frag = False
|
||||
|
||||
from .common import FileDownloader
|
||||
from .fragment import FragmentFD
|
||||
from ..compat import (
|
||||
compat_setenv,
|
||||
compat_str,
|
||||
@@ -29,12 +23,11 @@ from ..utils import (
|
||||
check_executable,
|
||||
is_outdated_version,
|
||||
process_communicate_or_kill,
|
||||
sanitized_Request,
|
||||
sanitize_open,
|
||||
)
|
||||
|
||||
|
||||
class ExternalFD(FileDownloader):
|
||||
class ExternalFD(FragmentFD):
|
||||
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps')
|
||||
can_download_to_stdout = False
|
||||
|
||||
@@ -146,6 +139,7 @@ class ExternalFD(FileDownloader):
|
||||
self.report_error('Giving up after %s fragment retries' % fragment_retries)
|
||||
return -1
|
||||
|
||||
decrypt_fragment = self.decrypter(info_dict)
|
||||
dest, _ = sanitize_open(tmpfilename, 'wb')
|
||||
for frag_index, fragment in enumerate(info_dict['fragments']):
|
||||
fragment_filename = '%s-Frag%d' % (tmpfilename, frag_index)
|
||||
@@ -157,22 +151,7 @@ class ExternalFD(FileDownloader):
|
||||
continue
|
||||
self.report_error('Unable to open fragment %d' % frag_index)
|
||||
return -1
|
||||
decrypt_info = fragment.get('decrypt_info')
|
||||
if decrypt_info:
|
||||
if decrypt_info['METHOD'] == 'AES-128':
|
||||
iv = decrypt_info.get('IV')
|
||||
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
|
||||
self._prepare_url(info_dict, info_dict.get('_decryption_key_url') or decrypt_info['URI'])).read()
|
||||
encrypted_data = src.read()
|
||||
decrypted_data = AES.new(
|
||||
decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(encrypted_data)
|
||||
dest.write(decrypted_data)
|
||||
else:
|
||||
fragment_data = src.read()
|
||||
dest.write(fragment_data)
|
||||
else:
|
||||
fragment_data = src.read()
|
||||
dest.write(fragment_data)
|
||||
dest.write(decrypt_fragment(fragment, src.read()))
|
||||
src.close()
|
||||
if not self.params.get('keep_fragments', False):
|
||||
os.remove(encodeFilename(fragment_filename))
|
||||
@@ -186,10 +165,6 @@ class ExternalFD(FileDownloader):
|
||||
self.to_stderr(stderr.decode('utf-8', 'replace'))
|
||||
return p.returncode
|
||||
|
||||
def _prepare_url(self, info_dict, url):
|
||||
headers = info_dict.get('http_headers')
|
||||
return sanitized_Request(url, None, headers) if headers else url
|
||||
|
||||
|
||||
class CurlFD(ExternalFD):
|
||||
AVAILABLE_OPT = '-V'
|
||||
@@ -357,7 +332,7 @@ class FFmpegFD(ExternalFD):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def can_merge_formats(cls, info_dict, params={}):
|
||||
def can_merge_formats(cls, info_dict, params):
|
||||
return (
|
||||
info_dict.get('requested_formats')
|
||||
and info_dict.get('protocol')
|
||||
@@ -523,7 +498,7 @@ class AVconvFD(FFmpegFD):
|
||||
_BY_NAME = dict(
|
||||
(klass.get_basename(), klass)
|
||||
for name, klass in globals().items()
|
||||
if name.endswith('FD') and name != 'ExternalFD'
|
||||
if name.endswith('FD') and name not in ('ExternalFD', 'FragmentFD')
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3,12 +3,7 @@ from __future__ import division, unicode_literals
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
can_decrypt_frag = True
|
||||
except ImportError:
|
||||
can_decrypt_frag = False
|
||||
from math import ceil
|
||||
|
||||
try:
|
||||
import concurrent.futures
|
||||
@@ -18,6 +13,7 @@ except ImportError:
|
||||
|
||||
from .common import FileDownloader
|
||||
from .http import HttpFD
|
||||
from ..aes import aes_cbc_decrypt_bytes
|
||||
from ..compat import (
|
||||
compat_urllib_error,
|
||||
compat_struct_pack,
|
||||
@@ -125,6 +121,7 @@ class FragmentFD(FileDownloader):
|
||||
'url': frag_url,
|
||||
'http_headers': headers or info_dict.get('http_headers'),
|
||||
'request_data': request_data,
|
||||
'ctx_id': ctx.get('ctx_id'),
|
||||
}
|
||||
success = ctx['dl'].download(fragment_filename, fragment_info_dict)
|
||||
if not success:
|
||||
@@ -224,6 +221,7 @@ class FragmentFD(FileDownloader):
|
||||
def _start_frag_download(self, ctx, info_dict):
|
||||
resume_len = ctx['complete_frags_downloaded_bytes']
|
||||
total_frags = ctx['total_frags']
|
||||
ctx_id = ctx.get('ctx_id')
|
||||
# This dict stores the download progress, it's updated by the progress
|
||||
# hook
|
||||
state = {
|
||||
@@ -247,6 +245,12 @@ class FragmentFD(FileDownloader):
|
||||
if s['status'] not in ('downloading', 'finished'):
|
||||
return
|
||||
|
||||
if ctx_id is not None and s.get('ctx_id') != ctx_id:
|
||||
return
|
||||
|
||||
state['max_progress'] = ctx.get('max_progress')
|
||||
state['progress_idx'] = ctx.get('progress_idx')
|
||||
|
||||
time_now = time.time()
|
||||
state['elapsed'] = time_now - start
|
||||
frag_total_bytes = s.get('total_bytes') or 0
|
||||
@@ -306,6 +310,9 @@ class FragmentFD(FileDownloader):
|
||||
'filename': ctx['filename'],
|
||||
'status': 'finished',
|
||||
'elapsed': elapsed,
|
||||
'ctx_id': ctx.get('ctx_id'),
|
||||
'max_progress': ctx.get('max_progress'),
|
||||
'progress_idx': ctx.get('progress_idx'),
|
||||
}, info_dict)
|
||||
|
||||
def _prepare_external_frag_download(self, ctx):
|
||||
@@ -329,7 +336,66 @@ class FragmentFD(FileDownloader):
|
||||
'fragment_index': 0,
|
||||
})
|
||||
|
||||
def download_and_append_fragments(self, ctx, fragments, info_dict, *, pack_func=None, finish_func=None):
|
||||
def decrypter(self, info_dict):
|
||||
_key_cache = {}
|
||||
|
||||
def _get_key(url):
|
||||
if url not in _key_cache:
|
||||
_key_cache[url] = self.ydl.urlopen(self._prepare_url(info_dict, url)).read()
|
||||
return _key_cache[url]
|
||||
|
||||
def decrypt_fragment(fragment, frag_content):
|
||||
decrypt_info = fragment.get('decrypt_info')
|
||||
if not decrypt_info or decrypt_info['METHOD'] != 'AES-128':
|
||||
return frag_content
|
||||
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', fragment['media_sequence'])
|
||||
decrypt_info['KEY'] = decrypt_info.get('KEY') or _get_key(info_dict.get('_decryption_key_url') or decrypt_info['URI'])
|
||||
# Don't decrypt the content in tests since the data is explicitly truncated and it's not to a valid block
|
||||
# size (see https://github.com/ytdl-org/youtube-dl/pull/27660). Tests only care that the correct data downloaded,
|
||||
# not what it decrypts to.
|
||||
if self.params.get('test', False):
|
||||
return frag_content
|
||||
decrypted_data = aes_cbc_decrypt_bytes(frag_content, decrypt_info['KEY'], iv)
|
||||
return decrypted_data[:-decrypted_data[-1]]
|
||||
|
||||
return decrypt_fragment
|
||||
|
||||
def download_and_append_fragments_multiple(self, *args, pack_func=None, finish_func=None):
|
||||
'''
|
||||
@params (ctx1, fragments1, info_dict1), (ctx2, fragments2, info_dict2), ...
|
||||
all args must be either tuple or list
|
||||
'''
|
||||
max_progress = len(args)
|
||||
if max_progress == 1:
|
||||
return self.download_and_append_fragments(*args[0], pack_func=pack_func, finish_func=finish_func)
|
||||
max_workers = self.params.get('concurrent_fragment_downloads', max_progress)
|
||||
self._prepare_multiline_status(max_progress)
|
||||
|
||||
def thread_func(idx, ctx, fragments, info_dict, tpe):
|
||||
ctx['max_progress'] = max_progress
|
||||
ctx['progress_idx'] = idx
|
||||
return self.download_and_append_fragments(ctx, fragments, info_dict, pack_func=pack_func, finish_func=finish_func, tpe=tpe)
|
||||
|
||||
class FTPE(concurrent.futures.ThreadPoolExecutor):
|
||||
# has to stop this or it's going to wait on the worker thread itself
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
pass
|
||||
|
||||
spins = []
|
||||
for idx, (ctx, fragments, info_dict) in enumerate(args):
|
||||
tpe = FTPE(ceil(max_workers / max_progress))
|
||||
job = tpe.submit(thread_func, idx, ctx, fragments, info_dict, tpe)
|
||||
spins.append((tpe, job))
|
||||
|
||||
result = True
|
||||
for tpe, job in spins:
|
||||
try:
|
||||
result = result and job.result()
|
||||
finally:
|
||||
tpe.shutdown(wait=True)
|
||||
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)
|
||||
is_fatal = (lambda idx: idx == 0) if self.params.get('skip_unavailable_fragments', True) else (lambda _: True)
|
||||
if not pack_func:
|
||||
@@ -337,7 +403,7 @@ class FragmentFD(FileDownloader):
|
||||
|
||||
def download_fragment(fragment, ctx):
|
||||
frag_index = ctx['fragment_index'] = fragment['frag_index']
|
||||
headers = info_dict.get('http_headers', {})
|
||||
headers = info_dict.get('http_headers', {}).copy()
|
||||
byte_range = fragment.get('byte_range')
|
||||
if byte_range:
|
||||
headers['Range'] = 'bytes=%d-%d' % (byte_range['start'], byte_range['end'] - 1)
|
||||
@@ -374,20 +440,6 @@ class FragmentFD(FileDownloader):
|
||||
return False, frag_index
|
||||
return frag_content, frag_index
|
||||
|
||||
def decrypt_fragment(fragment, frag_content):
|
||||
decrypt_info = fragment.get('decrypt_info')
|
||||
if not decrypt_info or decrypt_info['METHOD'] != 'AES-128':
|
||||
return frag_content
|
||||
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', fragment['media_sequence'])
|
||||
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
|
||||
self._prepare_url(info_dict, info_dict.get('_decryption_key_url') or decrypt_info['URI'])).read()
|
||||
# Don't decrypt the content in tests since the data is explicitly truncated and it's not to a valid block
|
||||
# size (see https://github.com/ytdl-org/youtube-dl/pull/27660). Tests only care that the correct data downloaded,
|
||||
# not what it decrypts to.
|
||||
if self.params.get('test', False):
|
||||
return frag_content
|
||||
return AES.new(decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(frag_content)
|
||||
|
||||
def append_fragment(frag_content, frag_index, ctx):
|
||||
if not frag_content:
|
||||
if not is_fatal(frag_index - 1):
|
||||
@@ -401,6 +453,8 @@ class FragmentFD(FileDownloader):
|
||||
self._append_fragment(ctx, pack_func(frag_content, frag_index))
|
||||
return True
|
||||
|
||||
decrypt_fragment = self.decrypter(info_dict)
|
||||
|
||||
max_workers = self.params.get('concurrent_fragment_downloads', 1)
|
||||
if can_threaded_download and max_workers > 1:
|
||||
|
||||
@@ -410,7 +464,7 @@ class FragmentFD(FileDownloader):
|
||||
return fragment, frag_content, frag_index, ctx_copy.get('fragment_filename_sanitized')
|
||||
|
||||
self.report_warning('The download speed shown is only of one thread. This is a known issue and patches are welcome')
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
|
||||
with tpe or concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
|
||||
for fragment, frag_content, frag_index, frag_filename in pool.map(_download_fragment, fragments):
|
||||
ctx['fragment_filename_sanitized'] = frag_filename
|
||||
ctx['fragment_index'] = frag_index
|
||||
|
||||
@@ -5,10 +5,11 @@ import io
|
||||
import binascii
|
||||
|
||||
from ..downloader import get_suitable_downloader
|
||||
from .fragment import FragmentFD, can_decrypt_frag
|
||||
from .fragment import FragmentFD
|
||||
from .external import FFmpegFD
|
||||
|
||||
from ..compat import (
|
||||
compat_pycrypto_AES,
|
||||
compat_urlparse,
|
||||
)
|
||||
from ..utils import (
|
||||
@@ -29,7 +30,7 @@ class HlsFD(FragmentFD):
|
||||
FD_NAME = 'hlsnative'
|
||||
|
||||
@staticmethod
|
||||
def can_download(manifest, info_dict, allow_unplayable_formats=False, with_crypto=can_decrypt_frag):
|
||||
def can_download(manifest, info_dict, allow_unplayable_formats=False):
|
||||
UNSUPPORTED_FEATURES = [
|
||||
# r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [2]
|
||||
|
||||
@@ -56,9 +57,6 @@ class HlsFD(FragmentFD):
|
||||
|
||||
def check_results():
|
||||
yield not info_dict.get('is_live')
|
||||
is_aes128_enc = '#EXT-X-KEY:METHOD=AES-128' in manifest
|
||||
yield with_crypto or not is_aes128_enc
|
||||
yield not (is_aes128_enc and r'#EXT-X-BYTERANGE' in manifest)
|
||||
for feature in UNSUPPORTED_FEATURES:
|
||||
yield not re.search(feature, manifest)
|
||||
return all(check_results())
|
||||
@@ -71,16 +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
|
||||
if self.can_download(s, info_dict, with_crypto=True):
|
||||
self.report_warning('pycryptodome is needed to download this file natively')
|
||||
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:
|
||||
@@ -172,6 +174,7 @@ class HlsFD(FragmentFD):
|
||||
'byte_range': byte_range,
|
||||
'media_sequence': media_sequence,
|
||||
})
|
||||
media_sequence += 1
|
||||
|
||||
elif line.startswith('#EXT-X-MAP'):
|
||||
if format_index and discontinuity_count != format_index:
|
||||
@@ -196,6 +199,7 @@ class HlsFD(FragmentFD):
|
||||
'byte_range': byte_range,
|
||||
'media_sequence': media_sequence
|
||||
})
|
||||
media_sequence += 1
|
||||
|
||||
if map_info.get('BYTERANGE'):
|
||||
splitted_byte_range = map_info.get('BYTERANGE').split('@')
|
||||
@@ -235,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
|
||||
@@ -57,6 +58,7 @@ class HttpFD(FileDownloader):
|
||||
ctx.block_size = self.params.get('buffersize', 1024)
|
||||
ctx.start_time = time.time()
|
||||
ctx.chunk_size = None
|
||||
throttle_start = None
|
||||
|
||||
if self.params.get('continuedl', True):
|
||||
# Establish possible resume length
|
||||
@@ -196,6 +198,7 @@ class HttpFD(FileDownloader):
|
||||
raise RetryDownload(err)
|
||||
|
||||
def download():
|
||||
nonlocal throttle_start
|
||||
data_len = ctx.data.info().get('Content-length', None)
|
||||
|
||||
# Range HTTP header may be ignored/unsupported by a webserver
|
||||
@@ -224,7 +227,6 @@ class HttpFD(FileDownloader):
|
||||
# measure time over whole while-loop, so slow_down() and best_block_size() work together properly
|
||||
now = None # needed for slow_down() in the first loop run
|
||||
before = start # start measuring
|
||||
throttle_start = None
|
||||
|
||||
def retry(e):
|
||||
to_stdout = ctx.tmpfilename == '-'
|
||||
@@ -310,6 +312,7 @@ class HttpFD(FileDownloader):
|
||||
'eta': eta,
|
||||
'speed': speed,
|
||||
'elapsed': now - ctx.start_time,
|
||||
'ctx_id': info_dict.get('ctx_id'),
|
||||
}, info_dict)
|
||||
|
||||
if data_len is not None and byte_counter == data_len:
|
||||
@@ -324,7 +327,7 @@ class HttpFD(FileDownloader):
|
||||
if ctx.stream is not None and ctx.tmpfilename != '-':
|
||||
ctx.stream.close()
|
||||
raise ThrottledDownload()
|
||||
else:
|
||||
elif speed:
|
||||
throttle_start = None
|
||||
|
||||
if not is_test and ctx.chunk_size and ctx.data_len is not None and byte_counter < ctx.data_len:
|
||||
@@ -357,6 +360,7 @@ class HttpFD(FileDownloader):
|
||||
'filename': ctx.filename,
|
||||
'status': 'finished',
|
||||
'elapsed': time.time() - ctx.start_time,
|
||||
'ctx_id': info_dict.get('ctx_id'),
|
||||
}, info_dict)
|
||||
|
||||
return True
|
||||
|
||||
@@ -6,7 +6,7 @@ import threading
|
||||
from .common import FileDownloader
|
||||
from ..downloader import get_suitable_downloader
|
||||
from ..extractor.niconico import NiconicoIE
|
||||
from ..compat import compat_urllib_request
|
||||
from ..utils import sanitized_Request
|
||||
|
||||
|
||||
class NiconicoDmcFD(FileDownloader):
|
||||
@@ -29,9 +29,11 @@ class NiconicoDmcFD(FileDownloader):
|
||||
heartbeat_data = heartbeat_info_dict['data'].encode()
|
||||
heartbeat_interval = heartbeat_info_dict.get('interval', 30)
|
||||
|
||||
request = sanitized_Request(heartbeat_url, heartbeat_data)
|
||||
|
||||
def heartbeat():
|
||||
try:
|
||||
compat_urllib_request.urlopen(url=heartbeat_url, data=heartbeat_data)
|
||||
self.ydl.urlopen(request).read()
|
||||
except Exception:
|
||||
self.to_screen('[%s] Heartbeat failed' % self.FD_NAME)
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ class YoutubeLiveChatFD(FragmentFD):
|
||||
request_data['currentPlayerState'] = {'playerOffsetMs': str(max(offset - 5000, 0))}
|
||||
if click_tracking_params:
|
||||
request_data['context']['clickTracking'] = {'clickTrackingParams': click_tracking_params}
|
||||
headers = ie.generate_api_headers(ytcfg, visitor_data=visitor_data)
|
||||
headers = ie.generate_api_headers(ytcfg=ytcfg, visitor_data=visitor_data)
|
||||
headers.update({'content-type': 'application/json'})
|
||||
fragment_request_data = json.dumps(request_data, ensure_ascii=False).encode('utf-8') + b'\n'
|
||||
success, continuation_id, offset, click_tracking_params = download_and_parse_fragment(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -174,7 +174,7 @@ class ArteTVIE(ArteTVBaseIE):
|
||||
return {
|
||||
'id': player_info.get('VID') or video_id,
|
||||
'title': title,
|
||||
'description': player_info.get('VDE'),
|
||||
'description': player_info.get('VDE') or player_info.get('V7T'),
|
||||
'upload_date': unified_strdate(upload_date_str),
|
||||
'thumbnail': player_info.get('programImage') or player_info.get('VTU', {}).get('IUR'),
|
||||
'formats': formats,
|
||||
|
||||
@@ -1,74 +1,106 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
determine_ext,
|
||||
dict_get,
|
||||
int_or_none,
|
||||
unescapeHTML,
|
||||
float_or_none,
|
||||
jwt_encode_hs256,
|
||||
try_get,
|
||||
)
|
||||
|
||||
|
||||
class ATVAtIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?atv\.at/(?:[^/]+/){2}(?P<id>[dv]\d+)'
|
||||
_VALID_URL = r'https?://(?:www\.)?atv\.at/tv/(?:[^/]+/){2,3}(?P<id>.*)'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://www.atv.at/bauer-sucht-frau-die-zweite-chance/folge-1/d3390693/',
|
||||
'md5': 'c471605591009dfb6e6c54f7e62e2807',
|
||||
'url': 'https://www.atv.at/tv/bauer-sucht-frau/staffel-18/bauer-sucht-frau/bauer-sucht-frau-staffel-18-folge-3-die-hofwochen',
|
||||
'md5': '3c3b4aaca9f63e32b35e04a9c2515903',
|
||||
'info_dict': {
|
||||
'id': '3390684',
|
||||
'id': 'v-ce9cgn1e70n5-1',
|
||||
'ext': 'mp4',
|
||||
'title': 'Bauer sucht Frau - Die zweite Chance Folge 1',
|
||||
'title': 'Bauer sucht Frau - Staffel 18 Folge 3 - Die Hofwochen',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.atv.at/bauer-sucht-frau-staffel-17/fuenfte-eventfolge/d3339537/',
|
||||
'url': 'https://www.atv.at/tv/bauer-sucht-frau/staffel-18/episode-01/bauer-sucht-frau-staffel-18-vorstellungsfolge-1',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _process_source_entry(self, source, part_id):
|
||||
source_url = source.get('url')
|
||||
if not source_url:
|
||||
return
|
||||
if determine_ext(source_url) == 'm3u8':
|
||||
return self._extract_m3u8_formats(
|
||||
source_url, part_id, 'mp4', 'm3u8_native',
|
||||
m3u8_id='hls', fatal=False)
|
||||
else:
|
||||
return [{
|
||||
'url': source_url,
|
||||
}]
|
||||
# extracted from bootstrap.js function (search for e.encryption_key and use your browser's debugger)
|
||||
_ACCESS_ID = 'x_atv'
|
||||
_ENCRYPTION_KEY = 'Hohnaekeishoogh2omaeghooquooshia'
|
||||
|
||||
def _process_entry(self, entry):
|
||||
part_id = entry.get('id')
|
||||
if not part_id:
|
||||
return
|
||||
def _extract_video_info(self, url, content, video):
|
||||
clip_id = content.get('splitId', content['id'])
|
||||
formats = []
|
||||
for source in entry.get('sources', []):
|
||||
formats.extend(self._process_source_entry(source, part_id) or [])
|
||||
|
||||
clip_urls = video['urls']
|
||||
for protocol, variant in clip_urls.items():
|
||||
source_url = try_get(variant, lambda x: x['clear']['url'])
|
||||
if not source_url:
|
||||
continue
|
||||
if protocol == 'dash':
|
||||
formats.extend(self._extract_mpd_formats(
|
||||
source_url, clip_id, mpd_id=protocol, fatal=False))
|
||||
elif protocol == 'hls':
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
source_url, clip_id, 'mp4', 'm3u8_native',
|
||||
m3u8_id=protocol, fatal=False))
|
||||
else:
|
||||
formats.append({
|
||||
'url': source_url,
|
||||
'format_id': protocol,
|
||||
})
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': part_id,
|
||||
'title': entry.get('title'),
|
||||
'duration': int_or_none(entry.get('duration')),
|
||||
'formats': formats
|
||||
'id': clip_id,
|
||||
'title': content.get('title'),
|
||||
'duration': float_or_none(content.get('duration')),
|
||||
'series': content.get('tvShowTitle'),
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
video_data = self._parse_json(unescapeHTML(self._search_regex(
|
||||
r'var\splaylist\s*=\s*(?P<json>\[.*\]);',
|
||||
webpage, 'player data', group='json')),
|
||||
display_id)
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
json_data = self._parse_json(
|
||||
self._search_regex(r'<script id="state" type="text/plain">(.*)</script>', webpage, 'json_data'),
|
||||
video_id=video_id)
|
||||
|
||||
first_video = video_data[0]
|
||||
video_id = first_video['id']
|
||||
video_title = dict_get(first_video, ('tvShowTitle', 'title'))
|
||||
video_title = json_data['views']['default']['page']['title']
|
||||
contentResource = json_data['views']['default']['page']['contentResource']
|
||||
content_id = contentResource[0]['id']
|
||||
content_ids = [{'id': id, 'subclip_start': content['start'], 'subclip_end': content['end']}
|
||||
for id, content in enumerate(contentResource)]
|
||||
|
||||
time_of_request = datetime.datetime.now()
|
||||
not_before = time_of_request - datetime.timedelta(minutes=5)
|
||||
expire = time_of_request + datetime.timedelta(minutes=5)
|
||||
payload = {
|
||||
'content_ids': {
|
||||
content_id: content_ids,
|
||||
},
|
||||
'secure_delivery': True,
|
||||
'iat': int(time_of_request.timestamp()),
|
||||
'nbf': int(not_before.timestamp()),
|
||||
'exp': int(expire.timestamp()),
|
||||
}
|
||||
jwt_token = jwt_encode_hs256(payload, self._ENCRYPTION_KEY, headers={'kid': self._ACCESS_ID})
|
||||
videos = self._download_json(
|
||||
'https://vas-v4.p7s1video.net/4.0/getsources',
|
||||
content_id, 'Downloading videos JSON', query={
|
||||
'token': jwt_token.decode('utf-8')
|
||||
})
|
||||
|
||||
video_id, videos_data = list(videos['data'].items())[0]
|
||||
entries = [
|
||||
self._extract_video_info(url, contentResource[video['id']], video)
|
||||
for video in videos_data]
|
||||
|
||||
return {
|
||||
'_type': 'multi_video',
|
||||
'id': video_id,
|
||||
'title': video_title,
|
||||
'entries': (self._process_entry(entry) for entry in video_data),
|
||||
'entries': entries,
|
||||
}
|
||||
|
||||
@@ -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,7 +22,9 @@ from ..utils import (
|
||||
parse_iso8601,
|
||||
try_get,
|
||||
smuggle_url,
|
||||
srt_subtitles_timecode,
|
||||
str_or_none,
|
||||
str_to_int,
|
||||
strip_jsonp,
|
||||
unified_timestamp,
|
||||
unsmuggle_url,
|
||||
@@ -622,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)
|
||||
@@ -774,3 +776,152 @@ class BiliBiliPlayerIE(InfoExtractor):
|
||||
return self.url_result(
|
||||
'http://www.bilibili.tv/video/av%s/' % video_id,
|
||||
ie=BiliBiliIE.ie_key(), video_id=video_id)
|
||||
|
||||
|
||||
class BiliIntlBaseIE(InfoExtractor):
|
||||
_API_URL = 'https://api.bili{}/intl/gateway{}'
|
||||
|
||||
def _call_api(self, type, endpoint, id):
|
||||
return self._download_json(self._API_URL.format(type, endpoint), id)['data']
|
||||
|
||||
def 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 = {}
|
||||
for sub in sub_json.get('subtitles', []):
|
||||
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({
|
||||
'ext': 'srt',
|
||||
'data': self.json2srt(sub_data)
|
||||
})
|
||||
return subtitles
|
||||
|
||||
def _get_formats(self, type, ep_id):
|
||||
video_json = self._call_api(type, f'/web/playurl?ep_id={ep_id}&platform=web', ep_id)
|
||||
if not video_json:
|
||||
self.raise_login_required(method='cookies')
|
||||
video_json = video_json['playurl']
|
||||
formats = []
|
||||
for vid in video_json.get('video', []):
|
||||
video_res = vid.get('video_resource') or {}
|
||||
video_info = vid.get('stream_info') or {}
|
||||
if not video_res.get('url'):
|
||||
continue
|
||||
formats.append({
|
||||
'url': video_res['url'],
|
||||
'ext': 'mp4',
|
||||
'format_note': video_info.get('desc_words'),
|
||||
'width': video_res.get('width'),
|
||||
'height': video_res.get('height'),
|
||||
'vbr': video_res.get('bandwidth'),
|
||||
'acodec': 'none',
|
||||
'vcodec': video_res.get('codecs'),
|
||||
'filesize': video_res.get('size'),
|
||||
})
|
||||
for aud in video_json.get('audio_resource', []):
|
||||
if not aud.get('url'):
|
||||
continue
|
||||
formats.append({
|
||||
'url': aud['url'],
|
||||
'ext': 'mp4',
|
||||
'abr': aud.get('bandwidth'),
|
||||
'acodec': aud.get('codecs'),
|
||||
'vcodec': 'none',
|
||||
'filesize': aud.get('size'),
|
||||
})
|
||||
|
||||
self._sort_formats(formats)
|
||||
return formats
|
||||
|
||||
def _extract_ep_info(self, type, episode_data, ep_id):
|
||||
return {
|
||||
'id': ep_id,
|
||||
'title': episode_data.get('long_title') or episode_data['title'],
|
||||
'thumbnail': episode_data.get('cover'),
|
||||
'episode_number': str_to_int(episode_data.get('title')),
|
||||
'formats': self._get_formats(type, ep_id),
|
||||
'subtitles': self._get_subtitles(type, ep_id),
|
||||
'extractor_key': BiliIntlIE.ie_key(),
|
||||
}
|
||||
|
||||
|
||||
class BiliIntlIE(BiliIntlBaseIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?bili(?P<type>bili\.tv|intl.com)/(?:[a-z]{2}/)?play/(?P<season_id>\d+)/(?P<id>\d+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.bilibili.tv/en/play/34613/341736',
|
||||
'info_dict': {
|
||||
'id': '341736',
|
||||
'ext': 'mp4',
|
||||
'title': 'The First Night',
|
||||
'thumbnail': 'https://i0.hdslb.com/bfs/intl/management/91e30e5521235d9b163339a26a0b030ebda54310.png',
|
||||
'episode_number': 2,
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.biliintl.com/en/play/34613/341736',
|
||||
'info_dict': {
|
||||
'id': '341736',
|
||||
'ext': 'mp4',
|
||||
'title': 'The First Night',
|
||||
'thumbnail': 'https://i0.hdslb.com/bfs/intl/management/91e30e5521235d9b163339a26a0b030ebda54310.png',
|
||||
'episode_number': 2,
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
type, season_id, id = self._match_valid_url(url).groups()
|
||||
data_json = self._call_api(type, f'/web/view/ogv_collection?season_id={season_id}', id)
|
||||
episode_data = next(
|
||||
episode for episode in data_json.get('episodes', [])
|
||||
if str(episode.get('ep_id')) == id)
|
||||
return self._extract_ep_info(type, episode_data, id)
|
||||
|
||||
|
||||
class BiliIntlSeriesIE(BiliIntlBaseIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?bili(?P<type>bili\.tv|intl.com)/(?:[a-z]{2}/)?play/(?P<id>\d+)$'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.bilibili.tv/en/play/34613',
|
||||
'playlist_mincount': 15,
|
||||
'info_dict': {
|
||||
'id': '34613',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
'format': 'bv',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.biliintl.com/en/play/34613',
|
||||
'playlist_mincount': 15,
|
||||
'info_dict': {
|
||||
'id': '34613',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
'format': 'bv',
|
||||
},
|
||||
}]
|
||||
|
||||
def _entries(self, id, type):
|
||||
data_json = self._call_api(type, f'/web/view/ogv_collection?season_id={id}', id)
|
||||
for episode in data_json.get('episodes', []):
|
||||
episode_id = str(episode.get('ep_id'))
|
||||
yield self._extract_ep_info(type, episode, episode_id)
|
||||
|
||||
def _real_extract(self, url):
|
||||
type, id = self._match_valid_url(url).groups()
|
||||
return self.playlist_result(self._entries(id, type), playlist_id=id)
|
||||
|
||||
@@ -472,7 +472,7 @@ class BrightcoveNewIE(AdobePassIE):
|
||||
title = json_data['name'].strip()
|
||||
|
||||
num_drm_sources = 0
|
||||
formats = []
|
||||
formats, subtitles = [], {}
|
||||
sources = json_data.get('sources') or []
|
||||
for source in sources:
|
||||
container = source.get('container')
|
||||
@@ -488,12 +488,16 @@ class BrightcoveNewIE(AdobePassIE):
|
||||
elif ext == 'm3u8' or container == 'M2TS':
|
||||
if not src:
|
||||
continue
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
src, video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False))
|
||||
f, subs = self._extract_m3u8_formats_and_subtitles(
|
||||
src, video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False)
|
||||
formats.extend(f)
|
||||
subtitles = self._merge_subtitles(subtitles, subs)
|
||||
elif ext == 'mpd':
|
||||
if not src:
|
||||
continue
|
||||
formats.extend(self._extract_mpd_formats(src, video_id, 'dash', fatal=False))
|
||||
f, subs = self._extract_mpd_formats_and_subtitles(src, video_id, 'dash', fatal=False)
|
||||
formats.extend(f)
|
||||
subtitles = self._merge_subtitles(subtitles, subs)
|
||||
else:
|
||||
streaming_src = source.get('streaming_src')
|
||||
stream_name, app_name = source.get('stream_name'), source.get('app_name')
|
||||
@@ -556,7 +560,6 @@ class BrightcoveNewIE(AdobePassIE):
|
||||
for f in formats:
|
||||
f.setdefault('http_headers', {}).update(headers)
|
||||
|
||||
subtitles = {}
|
||||
for text_track in json_data.get('text_tracks', []):
|
||||
if text_track.get('kind') != 'captions':
|
||||
continue
|
||||
|
||||
32
yt_dlp/extractor/cam4.py
Normal file
32
yt_dlp/extractor/cam4.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
|
||||
|
||||
class CAM4IE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:[^/]+\.)?cam4\.com/(?P<id>[a-z0-9_]+)'
|
||||
_TEST = {
|
||||
'url': 'https://www.cam4.com/foxynesss',
|
||||
'info_dict': {
|
||||
'id': 'foxynesss',
|
||||
'ext': 'mp4',
|
||||
'title': 're:^foxynesss [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
||||
'age_limit': 18,
|
||||
}
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
channel_id = self._match_id(url)
|
||||
m3u8_playlist = self._download_json('https://www.cam4.com/rest/v1.0/profile/{}/streamInfo'.format(channel_id), channel_id).get('cdnURL')
|
||||
|
||||
formats = self._extract_m3u8_formats(m3u8_playlist, channel_id, 'mp4', m3u8_id='hls', live=True)
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': channel_id,
|
||||
'title': self._live_title(channel_id),
|
||||
'is_live': True,
|
||||
'age_limit': 18,
|
||||
'formats': formats,
|
||||
}
|
||||
@@ -286,6 +286,9 @@ class VrtNUIE(GigyaBaseIE):
|
||||
'targetEnv': 'jssdk',
|
||||
}))
|
||||
|
||||
if auth_info.get('errorDetails'):
|
||||
raise ExtractorError('Unable to login: VrtNU said: ' + auth_info.get('errorDetails'), expected=True)
|
||||
|
||||
# Sometimes authentication fails for no good reason, retry
|
||||
login_attempt = 1
|
||||
while login_attempt <= 3:
|
||||
|
||||
@@ -1,30 +1,18 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import (
|
||||
compat_str,
|
||||
compat_HTTPError,
|
||||
)
|
||||
from ..utils import (
|
||||
js_to_json,
|
||||
smuggle_url,
|
||||
try_get,
|
||||
xpath_text,
|
||||
xpath_element,
|
||||
xpath_with_ns,
|
||||
find_xpath_attr,
|
||||
orderedSet,
|
||||
parse_duration,
|
||||
parse_iso8601,
|
||||
parse_age_limit,
|
||||
strip_or_none,
|
||||
int_or_none,
|
||||
ExtractorError,
|
||||
)
|
||||
|
||||
@@ -59,6 +47,7 @@ class CBCIE(InfoExtractor):
|
||||
'uploader': 'CBCC-NEW',
|
||||
'timestamp': 1382717907,
|
||||
},
|
||||
'skip': 'No longer available',
|
||||
}, {
|
||||
# with clipId, feed only available via tpfeed.cbc.ca
|
||||
'url': 'http://www.cbc.ca/archives/entry/1978-robin-williams-freestyles-on-90-minutes-live',
|
||||
@@ -209,289 +198,228 @@ class CBCPlayerIE(InfoExtractor):
|
||||
}
|
||||
|
||||
|
||||
class CBCWatchBaseIE(InfoExtractor):
|
||||
_device_id = None
|
||||
_device_token = None
|
||||
_API_BASE_URL = 'https://api-cbc.cloud.clearleap.com/cloffice/client/'
|
||||
_NS_MAP = {
|
||||
'media': 'http://search.yahoo.com/mrss/',
|
||||
'clearleap': 'http://www.clearleap.com/namespace/clearleap/1.0/',
|
||||
}
|
||||
_GEO_COUNTRIES = ['CA']
|
||||
_LOGIN_URL = 'https://api.loginradius.com/identity/v2/auth/login'
|
||||
_TOKEN_URL = 'https://cloud-api.loginradius.com/sso/jwt/api/token'
|
||||
_API_KEY = '3f4beddd-2061-49b0-ae80-6f1f2ed65b37'
|
||||
_NETRC_MACHINE = 'cbcwatch'
|
||||
|
||||
def _signature(self, email, password):
|
||||
data = json.dumps({
|
||||
'email': email,
|
||||
'password': password,
|
||||
}).encode()
|
||||
headers = {'content-type': 'application/json'}
|
||||
query = {'apikey': self._API_KEY}
|
||||
resp = self._download_json(self._LOGIN_URL, None, data=data, headers=headers, query=query)
|
||||
access_token = resp['access_token']
|
||||
|
||||
# token
|
||||
query = {
|
||||
'access_token': access_token,
|
||||
'apikey': self._API_KEY,
|
||||
'jwtapp': 'jwt',
|
||||
}
|
||||
resp = self._download_json(self._TOKEN_URL, None, headers=headers, query=query)
|
||||
return resp['signature']
|
||||
|
||||
def _call_api(self, path, video_id):
|
||||
url = path if path.startswith('http') else self._API_BASE_URL + path
|
||||
for _ in range(2):
|
||||
try:
|
||||
result = self._download_xml(url, video_id, headers={
|
||||
'X-Clearleap-DeviceId': self._device_id,
|
||||
'X-Clearleap-DeviceToken': self._device_token,
|
||||
})
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
|
||||
# Device token has expired, re-acquiring device token
|
||||
self._register_device()
|
||||
continue
|
||||
raise
|
||||
error_message = xpath_text(result, 'userMessage') or xpath_text(result, 'systemMessage')
|
||||
if error_message:
|
||||
raise ExtractorError('%s said: %s' % (self.IE_NAME, error_message))
|
||||
return result
|
||||
|
||||
def _real_initialize(self):
|
||||
if self._valid_device_token():
|
||||
return
|
||||
device = self._downloader.cache.load(
|
||||
'cbcwatch', self._cache_device_key()) or {}
|
||||
self._device_id, self._device_token = device.get('id'), device.get('token')
|
||||
if self._valid_device_token():
|
||||
return
|
||||
self._register_device()
|
||||
|
||||
def _valid_device_token(self):
|
||||
return self._device_id and self._device_token
|
||||
|
||||
def _cache_device_key(self):
|
||||
email, _ = self._get_login_info()
|
||||
return '%s_device' % hashlib.sha256(email.encode()).hexdigest() if email else 'device'
|
||||
|
||||
def _register_device(self):
|
||||
result = self._download_xml(
|
||||
self._API_BASE_URL + 'device/register',
|
||||
None, 'Acquiring device token',
|
||||
data=b'<device><type>web</type></device>')
|
||||
self._device_id = xpath_text(result, 'deviceId', fatal=True)
|
||||
email, password = self._get_login_info()
|
||||
if email and password:
|
||||
signature = self._signature(email, password)
|
||||
data = '<login><token>{0}</token><device><deviceId>{1}</deviceId><type>web</type></device></login>'.format(
|
||||
escape(signature), escape(self._device_id)).encode()
|
||||
url = self._API_BASE_URL + 'device/login'
|
||||
result = self._download_xml(
|
||||
url, None, data=data,
|
||||
headers={'content-type': 'application/xml'})
|
||||
self._device_token = xpath_text(result, 'token', fatal=True)
|
||||
else:
|
||||
self._device_token = xpath_text(result, 'deviceToken', fatal=True)
|
||||
self._downloader.cache.store(
|
||||
'cbcwatch', self._cache_device_key(), {
|
||||
'id': self._device_id,
|
||||
'token': self._device_token,
|
||||
})
|
||||
|
||||
def _parse_rss_feed(self, rss):
|
||||
channel = xpath_element(rss, 'channel', fatal=True)
|
||||
|
||||
def _add_ns(path):
|
||||
return xpath_with_ns(path, self._NS_MAP)
|
||||
|
||||
entries = []
|
||||
for item in channel.findall('item'):
|
||||
guid = xpath_text(item, 'guid', fatal=True)
|
||||
title = xpath_text(item, 'title', fatal=True)
|
||||
|
||||
media_group = xpath_element(item, _add_ns('media:group'), fatal=True)
|
||||
content = xpath_element(media_group, _add_ns('media:content'), fatal=True)
|
||||
content_url = content.attrib['url']
|
||||
|
||||
thumbnails = []
|
||||
for thumbnail in media_group.findall(_add_ns('media:thumbnail')):
|
||||
thumbnail_url = thumbnail.get('url')
|
||||
if not thumbnail_url:
|
||||
continue
|
||||
thumbnails.append({
|
||||
'id': thumbnail.get('profile'),
|
||||
'url': thumbnail_url,
|
||||
'width': int_or_none(thumbnail.get('width')),
|
||||
'height': int_or_none(thumbnail.get('height')),
|
||||
})
|
||||
|
||||
timestamp = None
|
||||
release_date = find_xpath_attr(
|
||||
item, _add_ns('media:credit'), 'role', 'releaseDate')
|
||||
if release_date is not None:
|
||||
timestamp = parse_iso8601(release_date.text)
|
||||
|
||||
entries.append({
|
||||
'_type': 'url_transparent',
|
||||
'url': content_url,
|
||||
'id': guid,
|
||||
'title': title,
|
||||
'description': xpath_text(item, 'description'),
|
||||
'timestamp': timestamp,
|
||||
'duration': int_or_none(content.get('duration')),
|
||||
'age_limit': parse_age_limit(xpath_text(item, _add_ns('media:rating'))),
|
||||
'episode': xpath_text(item, _add_ns('clearleap:episode')),
|
||||
'episode_number': int_or_none(xpath_text(item, _add_ns('clearleap:episodeInSeason'))),
|
||||
'series': xpath_text(item, _add_ns('clearleap:series')),
|
||||
'season_number': int_or_none(xpath_text(item, _add_ns('clearleap:season'))),
|
||||
'thumbnails': thumbnails,
|
||||
'ie_key': 'CBCWatchVideo',
|
||||
})
|
||||
|
||||
return self.playlist_result(
|
||||
entries, xpath_text(channel, 'guid'),
|
||||
xpath_text(channel, 'title'),
|
||||
xpath_text(channel, 'description'))
|
||||
|
||||
|
||||
class CBCWatchVideoIE(CBCWatchBaseIE):
|
||||
IE_NAME = 'cbc.ca:watch:video'
|
||||
_VALID_URL = r'https?://api-cbc\.cloud\.clearleap\.com/cloffice/client/web/play/?\?.*?\bcontentId=(?P<id>[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})'
|
||||
_TEST = {
|
||||
# geo-restricted to Canada, bypassable
|
||||
'url': 'https://api-cbc.cloud.clearleap.com/cloffice/client/web/play/?contentId=3c84472a-1eea-4dee-9267-2655d5055dcf&categoryId=ebc258f5-ee40-4cca-b66b-ba6bd55b7235',
|
||||
'only_matching': True,
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
result = self._call_api(url, video_id)
|
||||
|
||||
m3u8_url = xpath_text(result, 'url', fatal=True)
|
||||
formats = self._extract_m3u8_formats(re.sub(r'/([^/]+)/[^/?]+\.m3u8', r'/\1/\1.m3u8', m3u8_url), video_id, 'mp4', fatal=False)
|
||||
if len(formats) < 2:
|
||||
formats = self._extract_m3u8_formats(m3u8_url, video_id, 'mp4')
|
||||
for f in formats:
|
||||
format_id = f.get('format_id')
|
||||
if format_id.startswith('AAC'):
|
||||
f['acodec'] = 'aac'
|
||||
elif format_id.startswith('AC3'):
|
||||
f['acodec'] = 'ac-3'
|
||||
self._sort_formats(formats)
|
||||
|
||||
info = {
|
||||
'id': video_id,
|
||||
'title': video_id,
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
rss = xpath_element(result, 'rss')
|
||||
if rss:
|
||||
info.update(self._parse_rss_feed(rss)['entries'][0])
|
||||
del info['url']
|
||||
del info['_type']
|
||||
del info['ie_key']
|
||||
return info
|
||||
|
||||
|
||||
class CBCWatchIE(CBCWatchBaseIE):
|
||||
IE_NAME = 'cbc.ca:watch'
|
||||
_VALID_URL = r'https?://(?:gem|watch)\.cbc\.ca/(?:[^/]+/)+(?P<id>[0-9a-f-]+)'
|
||||
class CBCGemIE(InfoExtractor):
|
||||
IE_NAME = 'gem.cbc.ca'
|
||||
_VALID_URL = r'https?://gem\.cbc\.ca/media/(?P<id>[0-9a-z-]+/s[0-9]+[a-z][0-9]+)'
|
||||
_TESTS = [{
|
||||
# geo-restricted to Canada, bypassable
|
||||
'url': 'http://watch.cbc.ca/doc-zone/season-6/customer-disservice/38e815a-009e3ab12e4',
|
||||
# This is a normal, public, TV show video
|
||||
'url': 'https://gem.cbc.ca/media/schitts-creek/s06e01',
|
||||
'md5': '93dbb31c74a8e45b378cf13bd3f6f11e',
|
||||
'info_dict': {
|
||||
'id': '9673749a-5e77-484c-8b62-a1092a6b5168',
|
||||
'id': 'schitts-creek/s06e01',
|
||||
'ext': 'mp4',
|
||||
'title': 'Customer (Dis)Service',
|
||||
'description': 'md5:8bdd6913a0fe03d4b2a17ebe169c7c87',
|
||||
'upload_date': '20160219',
|
||||
'timestamp': 1455840000,
|
||||
},
|
||||
'params': {
|
||||
# m3u8 download
|
||||
'skip_download': True,
|
||||
'format': 'bestvideo',
|
||||
'title': 'Smoke Signals',
|
||||
'description': 'md5:929868d20021c924020641769eb3e7f1',
|
||||
'thumbnail': 'https://images.radio-canada.ca/v1/synps-cbc/episode/perso/cbc_schitts_creek_season_06e01_thumbnail_v01.jpg?im=Resize=(Size)',
|
||||
'duration': 1314,
|
||||
'categories': ['comedy'],
|
||||
'series': 'Schitt\'s Creek',
|
||||
'season': 'Season 6',
|
||||
'season_number': 6,
|
||||
'episode': 'Smoke Signals',
|
||||
'episode_number': 1,
|
||||
'episode_id': 'schitts-creek/s06e01',
|
||||
},
|
||||
'params': {'format': 'bv'},
|
||||
'skip': 'Geo-restricted to Canada',
|
||||
}, {
|
||||
# geo-restricted to Canada, bypassable
|
||||
'url': 'http://watch.cbc.ca/arthur/all/1ed4b385-cd84-49cf-95f0-80f004680057',
|
||||
# This video requires an account in the browser, but works fine in yt-dlp
|
||||
'url': 'https://gem.cbc.ca/media/schitts-creek/s01e01',
|
||||
'md5': '297a9600f554f2258aed01514226a697',
|
||||
'info_dict': {
|
||||
'id': '1ed4b385-cd84-49cf-95f0-80f004680057',
|
||||
'title': 'Arthur',
|
||||
'description': 'Arthur, the sweetest 8-year-old aardvark, and his pals solve all kinds of problems with humour, kindness and teamwork.',
|
||||
'id': 'schitts-creek/s01e01',
|
||||
'ext': 'mp4',
|
||||
'title': 'The Cup Runneth Over',
|
||||
'description': 'md5:9bca14ea49ab808097530eb05a29e797',
|
||||
'thumbnail': 'https://images.radio-canada.ca/v1/synps-cbc/episode/perso/cbc_schitts_creek_season_01e01_thumbnail_v01.jpg?im=Resize=(Size)',
|
||||
'series': 'Schitt\'s Creek',
|
||||
'season_number': 1,
|
||||
'season': 'Season 1',
|
||||
'episode_number': 1,
|
||||
'episode': 'The Cup Runneth Over',
|
||||
'episode_id': 'schitts-creek/s01e01',
|
||||
'duration': 1309,
|
||||
'categories': ['comedy'],
|
||||
},
|
||||
'playlist_mincount': 30,
|
||||
}, {
|
||||
'url': 'https://gem.cbc.ca/media/this-hour-has-22-minutes/season-26/episode-20/38e815a-0108c6c6a42',
|
||||
'only_matching': True,
|
||||
'params': {'format': 'bv'},
|
||||
'skip': 'Geo-restricted to Canada',
|
||||
}]
|
||||
_API_BASE = 'https://services.radio-canada.ca/ott/cbc-api/v2/assets/'
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
rss = self._call_api('web/browse/' + video_id, video_id)
|
||||
return self._parse_rss_feed(rss)
|
||||
video_info = self._download_json(self._API_BASE + video_id, video_id)
|
||||
|
||||
|
||||
class CBCOlympicsIE(InfoExtractor):
|
||||
IE_NAME = 'cbc.ca:olympics'
|
||||
_VALID_URL = r'https?://olympics\.cbc\.ca/video/[^/]+/(?P<id>[^/?#]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://olympics.cbc.ca/video/whats-on-tv/olympic-morning-featuring-the-opening-ceremony/',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
video_id = self._hidden_inputs(webpage)['videoId']
|
||||
video_doc = self._download_xml(
|
||||
'https://olympics.cbc.ca/videodata/%s.xml' % video_id, video_id)
|
||||
title = xpath_text(video_doc, 'title', fatal=True)
|
||||
is_live = xpath_text(video_doc, 'kind') == 'Live'
|
||||
if is_live:
|
||||
title = self._live_title(title)
|
||||
|
||||
formats = []
|
||||
for video_source in video_doc.findall('videoSources/videoSource'):
|
||||
uri = xpath_text(video_source, 'uri')
|
||||
if not uri:
|
||||
continue
|
||||
tokenize = self._download_json(
|
||||
'https://olympics.cbc.ca/api/api-akamai/tokenize',
|
||||
video_id, data=json.dumps({
|
||||
'VideoSource': uri,
|
||||
}).encode(), headers={
|
||||
'Content-Type': 'application/json',
|
||||
'Referer': url,
|
||||
# d3.VideoPlayer._init in https://olympics.cbc.ca/components/script/base.js
|
||||
'Cookie': '_dvp=TK:C0ObxjerU', # AKAMAI CDN cookie
|
||||
}, fatal=False)
|
||||
if not tokenize:
|
||||
continue
|
||||
content_url = tokenize['ContentUrl']
|
||||
video_source_format = video_source.get('format')
|
||||
if video_source_format == 'IIS':
|
||||
formats.extend(self._extract_ism_formats(
|
||||
content_url, video_id, ism_id=video_source_format, fatal=False))
|
||||
last_error = None
|
||||
attempt = -1
|
||||
retries = self.get_param('extractor_retries', 15)
|
||||
while attempt < retries:
|
||||
attempt += 1
|
||||
if last_error:
|
||||
self.report_warning('%s. Retrying ...' % last_error)
|
||||
m3u8_info = self._download_json(
|
||||
video_info['playSession']['url'], video_id,
|
||||
note='Downloading JSON metadata%s' % f' (attempt {attempt})')
|
||||
m3u8_url = m3u8_info.get('url')
|
||||
if m3u8_url:
|
||||
break
|
||||
elif m3u8_info.get('errorCode') == 1:
|
||||
self.raise_geo_restricted(countries=['CA'])
|
||||
else:
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
content_url, video_id, 'mp4',
|
||||
'm3u8' if is_live else 'm3u8_native',
|
||||
m3u8_id=video_source_format, fatal=False))
|
||||
last_error = f'{self.IE_NAME} said: {m3u8_info.get("errorCode")} - {m3u8_info.get("message")}'
|
||||
# 35 means media unavailable, but retries work
|
||||
if m3u8_info.get('errorCode') != 35 or attempt >= retries:
|
||||
raise ExtractorError(last_error)
|
||||
|
||||
formats = self._extract_m3u8_formats(m3u8_url, video_id, m3u8_id='hls')
|
||||
self._remove_duplicate_formats(formats)
|
||||
|
||||
for i, format in enumerate(formats):
|
||||
if format.get('vcodec') == 'none':
|
||||
if format.get('ext') is None:
|
||||
format['ext'] = 'm4a'
|
||||
if format.get('acodec') is None:
|
||||
format['acodec'] = 'mp4a.40.2'
|
||||
|
||||
# Put described audio at the beginning of the list, so that it
|
||||
# isn't chosen by default, as most people won't want it.
|
||||
if 'descriptive' in format['format_id'].lower():
|
||||
format['preference'] = -2
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'display_id': display_id,
|
||||
'title': title,
|
||||
'description': xpath_text(video_doc, 'description'),
|
||||
'thumbnail': xpath_text(video_doc, 'thumbnailUrl'),
|
||||
'duration': parse_duration(xpath_text(video_doc, 'duration')),
|
||||
'title': video_info['title'],
|
||||
'description': video_info.get('description'),
|
||||
'thumbnail': video_info.get('image'),
|
||||
'series': video_info.get('series'),
|
||||
'season_number': video_info.get('season'),
|
||||
'season': f'Season {video_info.get("season")}',
|
||||
'episode_number': video_info.get('episode'),
|
||||
'episode': video_info.get('title'),
|
||||
'episode_id': video_id,
|
||||
'duration': video_info.get('duration'),
|
||||
'categories': [video_info.get('category')],
|
||||
'formats': formats,
|
||||
'is_live': is_live,
|
||||
'release_timestamp': video_info.get('airDate'),
|
||||
'timestamp': video_info.get('availableDate'),
|
||||
}
|
||||
|
||||
|
||||
class CBCGemPlaylistIE(InfoExtractor):
|
||||
IE_NAME = 'gem.cbc.ca:playlist'
|
||||
_VALID_URL = r'https?://gem\.cbc\.ca/media/(?P<id>(?P<show>[0-9a-z-]+)/s(?P<season>[0-9]+))/?(?:[?#]|$)'
|
||||
_TESTS = [{
|
||||
# TV show playlist, all public videos
|
||||
'url': 'https://gem.cbc.ca/media/schitts-creek/s06',
|
||||
'playlist_count': 16,
|
||||
'info_dict': {
|
||||
'id': 'schitts-creek/s06',
|
||||
'title': 'Season 6',
|
||||
'description': 'md5:6a92104a56cbeb5818cc47884d4326a2',
|
||||
},
|
||||
}]
|
||||
_API_BASE = 'https://services.radio-canada.ca/ott/cbc-api/v2/shows/'
|
||||
|
||||
def _real_extract(self, url):
|
||||
match = self._match_valid_url(url)
|
||||
season_id = match.group('id')
|
||||
show = match.group('show')
|
||||
show_info = self._download_json(self._API_BASE + show, season_id)
|
||||
season = int(match.group('season'))
|
||||
season_info = try_get(show_info, lambda x: x['seasons'][season - 1])
|
||||
|
||||
if season_info is None:
|
||||
raise ExtractorError(f'Couldn\'t find season {season} of {show}')
|
||||
|
||||
episodes = []
|
||||
for episode in season_info['assets']:
|
||||
episodes.append({
|
||||
'_type': 'url_transparent',
|
||||
'ie_key': 'CBCGem',
|
||||
'url': 'https://gem.cbc.ca/media/' + episode['id'],
|
||||
'id': episode['id'],
|
||||
'title': episode.get('title'),
|
||||
'description': episode.get('description'),
|
||||
'thumbnail': episode.get('image'),
|
||||
'series': episode.get('series'),
|
||||
'season_number': episode.get('season'),
|
||||
'season': season_info['title'],
|
||||
'season_id': season_info.get('id'),
|
||||
'episode_number': episode.get('episode'),
|
||||
'episode': episode.get('title'),
|
||||
'episode_id': episode['id'],
|
||||
'duration': episode.get('duration'),
|
||||
'categories': [episode.get('category')],
|
||||
})
|
||||
|
||||
thumbnail = None
|
||||
tn_uri = season_info.get('image')
|
||||
# the-national was observed to use a "data:image/png;base64"
|
||||
# URI for their 'image' value. The image was 1x1, and is
|
||||
# probably just a placeholder, so it is ignored.
|
||||
if tn_uri is not None and not tn_uri.startswith('data:'):
|
||||
thumbnail = tn_uri
|
||||
|
||||
return {
|
||||
'_type': 'playlist',
|
||||
'entries': episodes,
|
||||
'id': season_id,
|
||||
'title': season_info['title'],
|
||||
'description': season_info.get('description'),
|
||||
'thumbnail': thumbnail,
|
||||
'series': show_info.get('title'),
|
||||
'season_number': season_info.get('season'),
|
||||
'season': season_info['title'],
|
||||
}
|
||||
|
||||
|
||||
class CBCGemLiveIE(InfoExtractor):
|
||||
IE_NAME = 'gem.cbc.ca:live'
|
||||
_VALID_URL = r'https?://gem\.cbc\.ca/live/(?P<id>[0-9]{12})'
|
||||
_TEST = {
|
||||
'url': 'https://gem.cbc.ca/live/920604739687',
|
||||
'info_dict': {
|
||||
'title': 'Ottawa',
|
||||
'description': 'The live TV channel and local programming from Ottawa',
|
||||
'thumbnail': 'https://thumbnails.cbc.ca/maven_legacy/thumbnails/CBC_OTT_VMS/Live_Channel_Static_Images/Ottawa_2880x1620.jpg',
|
||||
'is_live': True,
|
||||
'id': 'AyqZwxRqh8EH',
|
||||
'ext': 'mp4',
|
||||
'timestamp': 1492106160,
|
||||
'upload_date': '20170413',
|
||||
'uploader': 'CBCC-NEW',
|
||||
},
|
||||
'skip': 'Live might have ended',
|
||||
}
|
||||
|
||||
# It's unclear where the chars at the end come from, but they appear to be
|
||||
# constant. Might need updating in the future.
|
||||
_API = 'https://tpfeed.cbc.ca/f/ExhSPC/t_t3UKJR6MAT'
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
live_info = self._download_json(self._API, video_id)['entries']
|
||||
|
||||
video_info = None
|
||||
for stream in live_info:
|
||||
if stream.get('guid') == video_id:
|
||||
video_info = stream
|
||||
|
||||
if video_info is None:
|
||||
raise ExtractorError(
|
||||
'Couldn\'t find video metadata, maybe this livestream is now offline',
|
||||
expected=True)
|
||||
|
||||
return {
|
||||
'_type': 'url_transparent',
|
||||
'ie_key': 'ThePlatform',
|
||||
'url': video_info['content'][0]['url'],
|
||||
'id': video_id,
|
||||
'title': video_info.get('title'),
|
||||
'description': video_info.get('description'),
|
||||
'tags': try_get(video_info, lambda x: x['keywords'].split(', ')),
|
||||
'thumbnail': video_info.get('cbc$staticImage'),
|
||||
'is_live': True,
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@ class CBSIE(CBSBaseIE):
|
||||
title = xpath_text(video_data, 'videoTitle', 'title') or xpath_text(video_data, 'videotitle', 'title')
|
||||
|
||||
asset_types = {}
|
||||
has_drm = False
|
||||
for item in items_data.findall('.//item'):
|
||||
asset_type = xpath_text(item, 'assetType')
|
||||
query = {
|
||||
@@ -144,6 +145,8 @@ class CBSIE(CBSBaseIE):
|
||||
if asset_type in asset_types:
|
||||
continue
|
||||
elif any(excluded in asset_type for excluded in ('HLS_FPS', 'DASH_CENC', 'OnceURL')):
|
||||
if 'DASH_CENC' in asset_type:
|
||||
has_drm = True
|
||||
continue
|
||||
if asset_type.startswith('HLS') or 'StreamPack' in asset_type:
|
||||
query['formats'] = 'MPEG4,M3U'
|
||||
@@ -151,6 +154,9 @@ class CBSIE(CBSBaseIE):
|
||||
query['formats'] = 'MPEG4,FLV'
|
||||
asset_types[asset_type] = query
|
||||
|
||||
if not asset_types and has_drm:
|
||||
self.report_drm(content_id)
|
||||
|
||||
return self._extract_common_video_info(content_id, asset_types, mpx_acc, extra_info={
|
||||
'title': title,
|
||||
'series': xpath_text(video_data, 'seriesTitle'),
|
||||
|
||||
64
yt_dlp/extractor/cgtn.py
Normal file
64
yt_dlp/extractor/cgtn.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
try_get,
|
||||
unified_timestamp,
|
||||
)
|
||||
|
||||
|
||||
class CGTNIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://news\.cgtn\.com/news/[0-9]{4}-[0-9]{2}-[0-9]{2}/[a-zA-Z0-9-]+-(?P<id>[a-zA-Z0-9-]+)/index\.html'
|
||||
_TESTS = [
|
||||
{
|
||||
'url': 'https://news.cgtn.com/news/2021-03-09/Up-and-Out-of-Poverty-Ep-1-A-solemn-promise-YuOUaOzGQU/index.html',
|
||||
'info_dict': {
|
||||
'id': 'YuOUaOzGQU',
|
||||
'ext': 'mp4',
|
||||
'title': 'Up and Out of Poverty Ep. 1: A solemn promise',
|
||||
'thumbnail': r're:^https?://.*\.jpg$',
|
||||
'timestamp': 1615295940,
|
||||
'upload_date': '20210309',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True
|
||||
}
|
||||
}, {
|
||||
'url': 'https://news.cgtn.com/news/2021-06-06/China-Indonesia-vow-to-further-deepen-maritime-cooperation-10REvJCewCY/index.html',
|
||||
'info_dict': {
|
||||
'id': '10REvJCewCY',
|
||||
'ext': 'mp4',
|
||||
'title': 'China, Indonesia vow to further deepen maritime cooperation',
|
||||
'thumbnail': r're:^https?://.*\.png$',
|
||||
'description': 'China and Indonesia vowed to upgrade their cooperation into the maritime sector and also for political security, economy, and cultural and people-to-people exchanges.',
|
||||
'author': 'CGTN',
|
||||
'category': 'China',
|
||||
'timestamp': 1622950200,
|
||||
'upload_date': '20210606',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': False
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
download_url = self._html_search_regex(r'data-video ="(?P<url>.+m3u8)"', webpage, 'download_url')
|
||||
datetime_str = self._html_search_regex(r'<span class="date">\s*(.+?)\s*</span>', webpage, 'datetime_str', fatal=False)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': self._og_search_title(webpage),
|
||||
'description': self._og_search_description(webpage, default=None),
|
||||
'thumbnail': self._og_search_thumbnail(webpage),
|
||||
'formats': self._extract_m3u8_formats(download_url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls'),
|
||||
'category': self._html_search_regex(r'<span class="section">\s*(.+?)\s*</span>',
|
||||
webpage, 'category', fatal=False),
|
||||
'author': self._html_search_regex(r'<div class="news-author-name">\s*(.+?)\s*</div>',
|
||||
webpage, 'author', default=None, fatal=False),
|
||||
'timestamp': try_get(unified_timestamp(datetime_str), lambda x: x - 8 * 3600),
|
||||
}
|
||||
209
yt_dlp/extractor/chingari.py
Normal file
209
yt_dlp/extractor/chingari.py
Normal file
@@ -0,0 +1,209 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import itertools
|
||||
import json
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_urllib_parse_unquote_plus
|
||||
from ..utils import (
|
||||
clean_html,
|
||||
ExtractorError,
|
||||
int_or_none,
|
||||
str_to_int,
|
||||
url_or_none,
|
||||
)
|
||||
|
||||
|
||||
class ChingariBaseIE(InfoExtractor):
|
||||
def _get_post(self, id, post_data):
|
||||
media_data = post_data['mediaLocation']
|
||||
base_url = media_data['base']
|
||||
author_data = post_data.get('authorData', {})
|
||||
song_data = post_data.get('song', {}) # revist this in future for differentiating b/w 'art' and 'author'
|
||||
|
||||
formats = [{
|
||||
'format_id': frmt,
|
||||
'width': str_to_int(frmt[1:]),
|
||||
'url': base_url + frmt_path,
|
||||
} for frmt, frmt_path in media_data.get('transcoded', {}).items()]
|
||||
|
||||
if media_data.get('path'):
|
||||
formats.append({
|
||||
'format_id': 'original',
|
||||
'format_note': 'Direct video.',
|
||||
'url': base_url + '/apipublic' + media_data['path'],
|
||||
'quality': 10,
|
||||
})
|
||||
self._sort_formats(formats)
|
||||
timestamp = str_to_int(post_data.get('created_at'))
|
||||
if timestamp:
|
||||
timestamp = int_or_none(timestamp, 1000)
|
||||
|
||||
thumbnail, uploader_url = None, None
|
||||
if media_data.get('thumbnail'):
|
||||
thumbnail = base_url + media_data.get('thumbnail')
|
||||
if author_data.get('username'):
|
||||
uploader_url = 'https://chingari.io/' + author_data.get('username')
|
||||
|
||||
return {
|
||||
'id': id,
|
||||
'title': compat_urllib_parse_unquote_plus(clean_html(post_data.get('caption'))),
|
||||
'description': compat_urllib_parse_unquote_plus(clean_html(post_data.get('caption'))),
|
||||
'duration': media_data.get('duration'),
|
||||
'thumbnail': url_or_none(thumbnail),
|
||||
'like_count': post_data.get('likeCount'),
|
||||
'view_count': post_data.get('viewsCount'),
|
||||
'comment_count': post_data.get('commentCount'),
|
||||
'repost_count': post_data.get('shareCount'),
|
||||
'timestamp': timestamp,
|
||||
'uploader_id': post_data.get('userId') or author_data.get('_id'),
|
||||
'uploader': author_data.get('name'),
|
||||
'uploader_url': url_or_none(uploader_url),
|
||||
'track': song_data.get('title'),
|
||||
'artist': song_data.get('author'),
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
|
||||
class ChingariIE(ChingariBaseIE):
|
||||
_VALID_URL = r'(?:https?://)(?:www\.)?chingari\.io/share/post\?id=(?P<id>[^&/#?]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://chingari.io/share/post?id=612f8f4ce1dc57090e8a7beb',
|
||||
'info_dict': {
|
||||
'id': '612f8f4ce1dc57090e8a7beb',
|
||||
'ext': 'mp4',
|
||||
'title': 'Happy birthday Srila Prabhupada',
|
||||
'description': 'md5:c7080ebfdfeb06016e638c286d6bc3fa',
|
||||
'duration': 0,
|
||||
'thumbnail': 'https://media.chingari.io/uploads/c41d30e2-06b6-4e3b-9b4b-edbb929cec06-1630506826911/thumbnail/198f993f-ce87-4623-82c6-cd071bd6d4f4-1630506828016.jpg',
|
||||
'like_count': int,
|
||||
'view_count': int,
|
||||
'comment_count': int,
|
||||
'repost_count': int,
|
||||
'timestamp': 1630506828,
|
||||
'upload_date': '20210901',
|
||||
'uploader_id': '5f0403982c8bd344f4813f8c',
|
||||
'uploader': 'ISKCON,Inc.',
|
||||
'uploader_url': 'https://chingari.io/iskcon,inc',
|
||||
'track': None,
|
||||
'artist': None,
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
id = self._match_id(url)
|
||||
post_json = self._download_json(f'https://api.chingari.io/post/post_details/{id}', id)
|
||||
if post_json['code'] != 200:
|
||||
raise ExtractorError(post_json['message'], expected=True)
|
||||
post_data = post_json['data']
|
||||
return self._get_post(id, post_data)
|
||||
|
||||
|
||||
class ChingariUserIE(ChingariBaseIE):
|
||||
_VALID_URL = r'(?:https?://)(?:www\.)?chingari\.io/(?!share/post)(?P<id>[^/?]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://chingari.io/dada1023',
|
||||
'playlist_mincount': 3,
|
||||
'info_dict': {
|
||||
'id': 'dada1023',
|
||||
},
|
||||
'entries': [{
|
||||
'url': 'https://chingari.io/share/post?id=614781f3ade60b3a0bfff42a',
|
||||
'info_dict': {
|
||||
'id': '614781f3ade60b3a0bfff42a',
|
||||
'ext': 'mp4',
|
||||
'title': '#chingaribappa ',
|
||||
'description': 'md5:d1df21d84088770468fa63afe3b17857',
|
||||
'duration': 7,
|
||||
'thumbnail': 'https://media.chingari.io/uploads/346d86d4-abb2-474e-a164-ffccf2bbcb72-1632076273717/thumbnail/b0b3aac2-2b86-4dd1-909d-9ed6e57cf77c-1632076275552.jpg',
|
||||
'like_count': int,
|
||||
'view_count': int,
|
||||
'comment_count': int,
|
||||
'repost_count': int,
|
||||
'timestamp': 1632076275,
|
||||
'upload_date': '20210919',
|
||||
'uploader_id': '5efc4b12cca35c3d1794c2d3',
|
||||
'uploader': 'dada (girish) dhawale',
|
||||
'uploader_url': 'https://chingari.io/dada1023',
|
||||
'track': None,
|
||||
'artist': None
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, {
|
||||
'url': 'https://chingari.io/share/post?id=6146b132bcbf860959e12cba',
|
||||
'info_dict': {
|
||||
'id': '6146b132bcbf860959e12cba',
|
||||
'ext': 'mp4',
|
||||
'title': 'Tactor harvesting',
|
||||
'description': 'md5:8403f12dce68828b77ecee7eb7e887b7',
|
||||
'duration': 59.3,
|
||||
'thumbnail': 'https://media.chingari.io/uploads/b353ca70-7a87-400d-93a6-fa561afaec86-1632022814584/thumbnail/c09302e3-2043-41b1-a2fe-77d97e5bd676-1632022834260.jpg',
|
||||
'like_count': int,
|
||||
'view_count': int,
|
||||
'comment_count': int,
|
||||
'repost_count': int,
|
||||
'timestamp': 1632022834,
|
||||
'upload_date': '20210919',
|
||||
'uploader_id': '5efc4b12cca35c3d1794c2d3',
|
||||
'uploader': 'dada (girish) dhawale',
|
||||
'uploader_url': 'https://chingari.io/dada1023',
|
||||
'track': None,
|
||||
'artist': None
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, {
|
||||
'url': 'https://chingari.io/share/post?id=6145651b74cb030a64c40b82',
|
||||
'info_dict': {
|
||||
'id': '6145651b74cb030a64c40b82',
|
||||
'ext': 'mp4',
|
||||
'title': '#odiabhajan ',
|
||||
'description': 'md5:687ea36835b9276cf2af90f25e7654cb',
|
||||
'duration': 56.67,
|
||||
'thumbnail': 'https://media.chingari.io/uploads/6cbf216b-babc-4cce-87fe-ceaac8d706ac-1631937782708/thumbnail/8855754f-6669-48ce-b269-8cc0699ed6da-1631937819522.jpg',
|
||||
'like_count': int,
|
||||
'view_count': int,
|
||||
'comment_count': int,
|
||||
'repost_count': int,
|
||||
'timestamp': 1631937819,
|
||||
'upload_date': '20210918',
|
||||
'uploader_id': '5efc4b12cca35c3d1794c2d3',
|
||||
'uploader': 'dada (girish) dhawale',
|
||||
'uploader_url': 'https://chingari.io/dada1023',
|
||||
'track': None,
|
||||
'artist': None
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}],
|
||||
}, {
|
||||
'url': 'https://chingari.io/iskcon%2Cinc',
|
||||
'playlist_mincount': 1025,
|
||||
'info_dict': {
|
||||
'id': 'iskcon%2Cinc',
|
||||
},
|
||||
}]
|
||||
|
||||
def _entries(self, id):
|
||||
skip = 0
|
||||
has_more = True
|
||||
for page in itertools.count():
|
||||
posts = self._download_json('https://api.chingari.io/users/getPosts', id,
|
||||
data=json.dumps({'userId': id, 'ownerId': id, 'skip': skip, 'limit': 20}).encode(),
|
||||
headers={'content-type': 'application/json;charset=UTF-8'},
|
||||
note='Downloading page %s' % page)
|
||||
for post in posts.get('data', []):
|
||||
post_data = post['post']
|
||||
yield self._get_post(post_data['_id'], post_data)
|
||||
skip += 20
|
||||
has_more = posts['hasMoreData']
|
||||
if not has_more:
|
||||
break
|
||||
|
||||
def _real_extract(self, url):
|
||||
alt_id = self._match_id(url)
|
||||
post_json = self._download_json(f'https://api.chingari.io/user/{alt_id}', alt_id)
|
||||
if post_json['code'] != 200:
|
||||
raise ExtractorError(post_json['message'], expected=True)
|
||||
id = post_json['data']['_id']
|
||||
return self.playlist_result(self._entries(id), playlist_id=alt_id)
|
||||
90
yt_dlp/extractor/ciscowebex.py
Normal file
90
yt_dlp/extractor/ciscowebex.py
Normal file
@@ -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,
|
||||
}
|
||||
@@ -4,7 +4,7 @@ from .mtv import MTVServicesInfoExtractor
|
||||
|
||||
|
||||
class ComedyCentralIE(MTVServicesInfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?cc\.com/(?:episodes|video(?:-clips)?)/(?P<id>[0-9a-z]{6})'
|
||||
_VALID_URL = r'https?://(?:www\.)?cc\.com/(?:episodes|video(?:-clips)?|collection-playlist)/(?P<id>[0-9a-z]{6})'
|
||||
_FEED_URL = 'http://comedycentral.com/feeds/mrss/'
|
||||
|
||||
_TESTS = [{
|
||||
@@ -24,6 +24,9 @@ class ComedyCentralIE(MTVServicesInfoExtractor):
|
||||
}, {
|
||||
'url': 'https://www.cc.com/video/k3sdvm/the-daily-show-with-jon-stewart-exclusive-the-fourth-estate',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://www.cc.com/collection-playlist/cosnej/stand-up-specials/t6vtjb',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from ..compat import (
|
||||
compat_cookies_SimpleCookie,
|
||||
compat_etree_Element,
|
||||
compat_etree_fromstring,
|
||||
compat_expanduser,
|
||||
compat_getpass,
|
||||
compat_http_client,
|
||||
compat_os_name,
|
||||
@@ -405,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
|
||||
@@ -420,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.
|
||||
"""
|
||||
|
||||
@@ -788,9 +793,10 @@ class InfoExtractor(object):
|
||||
self._downloader.to_screen(dump)
|
||||
if self.get_param('write_pages', False):
|
||||
basen = '%s_%s' % (video_id, urlh.geturl())
|
||||
if len(basen) > 240:
|
||||
trim_length = self.get_param('trim_file_name') or 240
|
||||
if len(basen) > trim_length:
|
||||
h = '___' + hashlib.md5(basen.encode('utf-8')).hexdigest()
|
||||
basen = basen[:240 - len(h)] + h
|
||||
basen = basen[:trim_length - len(h)] + h
|
||||
raw_filename = basen + '.dump'
|
||||
filename = sanitize_filename(raw_filename, restricted=True)
|
||||
self.to_screen('Saving request to ' + filename)
|
||||
@@ -1128,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:
|
||||
@@ -1166,7 +1169,10 @@ class InfoExtractor(object):
|
||||
|
||||
if self.get_param('usenetrc', False):
|
||||
try:
|
||||
info = netrc.netrc().authenticators(netrc_machine)
|
||||
netrc_file = compat_expanduser(self.get_param('netrc_location') or '~')
|
||||
if os.path.isdir(netrc_file):
|
||||
netrc_file = os.path.join(netrc_file, '.netrc')
|
||||
info = netrc.netrc(file=netrc_file).authenticators(netrc_machine)
|
||||
if info is not None:
|
||||
username = info[0]
|
||||
password = info[2]
|
||||
@@ -1672,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,
|
||||
@@ -1756,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)
|
||||
@@ -1960,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(
|
||||
@@ -2214,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:
|
||||
@@ -2245,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):
|
||||
@@ -2318,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'))
|
||||
@@ -2399,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'):
|
||||
@@ -2471,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(
|
||||
@@ -2498,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(
|
||||
@@ -2618,8 +2656,10 @@ class InfoExtractor(object):
|
||||
base_url = base_url_e.text + base_url
|
||||
if re.match(r'^https?://', base_url):
|
||||
break
|
||||
if mpd_base_url and not re.match(r'^https?://', base_url):
|
||||
if not mpd_base_url.endswith('/') and not base_url.startswith('/'):
|
||||
if mpd_base_url and base_url.startswith('/'):
|
||||
base_url = compat_urlparse.urljoin(mpd_base_url, base_url)
|
||||
elif mpd_base_url and not re.match(r'^https?://', base_url):
|
||||
if not mpd_base_url.endswith('/'):
|
||||
mpd_base_url += '/'
|
||||
base_url = mpd_base_url + base_url
|
||||
representation_id = representation_attrib.get('id')
|
||||
@@ -2827,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={}):
|
||||
@@ -3090,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={}):
|
||||
@@ -3496,9 +3530,11 @@ class InfoExtractor(object):
|
||||
raise NotImplementedError('This method must be implemented by subclasses')
|
||||
|
||||
def mark_watched(self, *args, **kwargs):
|
||||
if (self.get_param('mark_watched', False)
|
||||
and (self._get_login_info()[0] is not None
|
||||
or self.get_param('cookiefile') is not None)):
|
||||
if not self.get_param('mark_watched', False):
|
||||
return
|
||||
if (self._get_login_info()[0] is not None
|
||||
or self.get_param('cookiefile')
|
||||
or self.get_param('cookiesfrombrowser')):
|
||||
self._mark_watched(*args, **kwargs)
|
||||
|
||||
def _mark_watched(self, *args, **kwargs):
|
||||
|
||||
113
yt_dlp/extractor/damtomo.py
Normal file
113
yt_dlp/extractor/damtomo.py
Normal file
@@ -0,0 +1,113 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import ExtractorError, clean_html, int_or_none, try_get, unified_strdate
|
||||
from ..compat import compat_str
|
||||
|
||||
|
||||
class DamtomoBaseIE(InfoExtractor):
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage, handle = self._download_webpage_handle(self._WEBPAGE_URL_TMPL % video_id, video_id, encoding='sjis')
|
||||
|
||||
if handle.url == 'https://www.clubdam.com/sorry/':
|
||||
raise ExtractorError('You are rate-limited. Try again later.', expected=True)
|
||||
if '<h2>予期せぬエラーが発生しました。</h2>' in webpage:
|
||||
raise ExtractorError('There is an error on server-side. Try again later.', expected=True)
|
||||
|
||||
description = self._search_regex(r'(?m)<div id="public_comment">\s*<p>\s*([^<]*?)\s*</p>', webpage, 'description', default=None)
|
||||
uploader_id = self._search_regex(r'<a href="https://www\.clubdam\.com/app/damtomo/member/info/Profile\.do\?damtomoId=([^"]+)"', webpage, 'uploader_id', default=None)
|
||||
|
||||
data_dict = {
|
||||
mobj.group('class'): re.sub(r'\s+', ' ', clean_html(mobj.group('value')))
|
||||
for mobj in re.finditer(r'(?s)<(p|div)\s+class="(?P<class>[^" ]+?)">(?P<value>.+?)</\1>', webpage)}
|
||||
|
||||
# since videos do not have title, give the name of song instead
|
||||
data_dict['user_name'] = re.sub(r'\s*さん\s*$', '', data_dict['user_name'])
|
||||
title = data_dict.get('song_title')
|
||||
|
||||
stream_tree = self._download_xml(
|
||||
self._DKML_XML_URL % video_id, video_id, note='Requesting stream information', encoding='sjis',
|
||||
# doing this has no problem since there is no character outside ASCII,
|
||||
# and never likely to happen in the future
|
||||
transform_source=lambda x: re.sub(r'\s*encoding="[^"]+?"', '', x))
|
||||
m3u8_url = try_get(stream_tree, lambda x: x.find(
|
||||
'.//d:streamingUrl', {'d': self._DKML_XML_NS}).text.strip(), compat_str)
|
||||
if not m3u8_url:
|
||||
raise ExtractorError('Failed to obtain m3u8 URL')
|
||||
formats = self._extract_m3u8_formats(m3u8_url, video_id, ext='mp4')
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'uploader_id': uploader_id,
|
||||
'description': description,
|
||||
'uploader': data_dict.get('user_name'),
|
||||
'upload_date': unified_strdate(self._search_regex(r'(\d{4}/\d{2}/\d{2})', data_dict.get('date'), 'upload_date', default=None)),
|
||||
'view_count': int_or_none(self._search_regex(r'(\d+)', data_dict['audience'], 'view_count', default=None)),
|
||||
'like_count': int_or_none(self._search_regex(r'(\d+)', data_dict['nice'], 'like_count', default=None)),
|
||||
'track': title,
|
||||
'artist': data_dict.get('song_artist'),
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
|
||||
class DamtomoVideoIE(DamtomoBaseIE):
|
||||
IE_NAME = 'damtomo:video'
|
||||
_VALID_URL = r'https?://(?:www\.)?clubdam\.com/app/damtomo/(?:SP/)?karaokeMovie/StreamingDkm\.do\?karaokeMovieId=(?P<id>\d+)'
|
||||
_WEBPAGE_URL_TMPL = 'https://www.clubdam.com/app/damtomo/karaokeMovie/StreamingDkm.do?karaokeMovieId=%s'
|
||||
_DKML_XML_URL = 'https://www.clubdam.com/app/damtomo/karaokeMovie/GetStreamingDkmUrlXML.do?movieSelectFlg=2&karaokeMovieId=%s'
|
||||
_DKML_XML_NS = 'https://www.clubdam.com/app/damtomo/karaokeMovie/GetStreamingDkmUrlXML'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.clubdam.com/app/damtomo/karaokeMovie/StreamingDkm.do?karaokeMovieId=2414316',
|
||||
'info_dict': {
|
||||
'id': '2414316',
|
||||
'title': 'Get Wild',
|
||||
'uploader': 'Kドロン',
|
||||
'uploader_id': 'ODk5NTQwMzQ',
|
||||
'track': 'Get Wild',
|
||||
'artist': 'TM NETWORK(TMN)',
|
||||
'upload_date': '20201226',
|
||||
}
|
||||
}]
|
||||
|
||||
|
||||
class DamtomoRecordIE(DamtomoBaseIE):
|
||||
IE_NAME = 'damtomo:record'
|
||||
_VALID_URL = r'https?://(?:www\.)?clubdam\.com/app/damtomo/(?:SP/)?karaokePost/StreamingKrk\.do\?karaokeContributeId=(?P<id>\d+)'
|
||||
_WEBPAGE_URL_TMPL = 'https://www.clubdam.com/app/damtomo/karaokePost/StreamingKrk.do?karaokeContributeId=%s'
|
||||
_DKML_XML_URL = 'https://www.clubdam.com/app/damtomo/karaokePost/GetStreamingKrkUrlXML.do?karaokeContributeId=%s'
|
||||
_DKML_XML_NS = 'https://www.clubdam.com/app/damtomo/karaokePost/GetStreamingKrkUrlXML'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.clubdam.com/app/damtomo/karaokePost/StreamingKrk.do?karaokeContributeId=27376862',
|
||||
'info_dict': {
|
||||
'id': '27376862',
|
||||
'title': 'イカSUMMER [良音]',
|
||||
'description': None,
|
||||
'uploader': 'NANA',
|
||||
'uploader_id': 'MzAyMDExNTY',
|
||||
'upload_date': '20210721',
|
||||
'view_count': 4,
|
||||
'like_count': 1,
|
||||
'track': 'イカSUMMER [良音]',
|
||||
'artist': 'ORANGE RANGE',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.clubdam.com/app/damtomo/karaokePost/StreamingKrk.do?karaokeContributeId=27489418',
|
||||
'info_dict': {
|
||||
'id': '27489418',
|
||||
'title': '心みだれて〜say it with flowers〜(生音)',
|
||||
'uploader_id': 'NjI1MjI2MjU',
|
||||
'description': 'やっぱりキーを下げて正解だった感じ。リベンジ成功ということで。',
|
||||
'uploader': '箱の「中の人」',
|
||||
'upload_date': '20210815',
|
||||
'view_count': 5,
|
||||
'like_count': 3,
|
||||
'track': '心みだれて〜say it with flowers〜(生音)',
|
||||
'artist': '小林明子',
|
||||
}
|
||||
}]
|
||||
@@ -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),
|
||||
}
|
||||
@@ -349,7 +349,7 @@ class DiscoveryPlusIE(DPlayIE):
|
||||
_API_URL = 'us1-prod-direct.discoveryplus.com'
|
||||
|
||||
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
||||
headers['x-disco-client'] = f'WEB:UNKNOWN:{self._PRODUCT}:15.0.0'
|
||||
headers['x-disco-client'] = f'WEB:UNKNOWN:{self._PRODUCT}:25.2.6'
|
||||
|
||||
def _download_video_playback_info(self, disco_base, video_id, headers):
|
||||
return self._download_json(
|
||||
@@ -389,3 +389,43 @@ class ScienceChannelIE(DiscoveryPlusIE):
|
||||
|
||||
_PRODUCT = 'sci'
|
||||
_API_URL = 'us1-prod-direct.sciencechannel.com'
|
||||
|
||||
|
||||
class DIYNetworkIE(DiscoveryPlusIE):
|
||||
_VALID_URL = r'https?://(?:watch\.)?diynetwork\.com/video' + DPlayIE._PATH_REGEX
|
||||
_TESTS = [{
|
||||
'url': 'https://watch.diynetwork.com/video/pool-kings-diy-network/bringing-beach-life-to-texas',
|
||||
'info_dict': {
|
||||
'id': '2309730',
|
||||
'display_id': 'pool-kings-diy-network/bringing-beach-life-to-texas',
|
||||
'ext': 'mp4',
|
||||
'title': 'Bringing Beach Life to Texas',
|
||||
'description': 'The Pool Kings give a family a day at the beach in their own backyard.',
|
||||
'season_number': 10,
|
||||
'episode_number': 2,
|
||||
},
|
||||
'skip': 'Available for Premium users',
|
||||
}]
|
||||
|
||||
_PRODUCT = 'diy'
|
||||
_API_URL = 'us1-prod-direct.watch.diynetwork.com'
|
||||
|
||||
|
||||
class AnimalPlanetIE(DiscoveryPlusIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?animalplanet\.com/video' + DPlayIE._PATH_REGEX
|
||||
_TESTS = [{
|
||||
'url': 'https://www.animalplanet.com/video/north-woods-law-animal-planet/squirrel-showdown',
|
||||
'info_dict': {
|
||||
'id': '3338923',
|
||||
'display_id': 'north-woods-law-animal-planet/squirrel-showdown',
|
||||
'ext': 'mp4',
|
||||
'title': 'Squirrel Showdown',
|
||||
'description': 'A woman is suspected of being in possession of flying squirrel kits.',
|
||||
'season_number': 16,
|
||||
'episode_number': 11,
|
||||
},
|
||||
'skip': 'Available for Premium users',
|
||||
}]
|
||||
|
||||
_PRODUCT = 'apl'
|
||||
_API_URL = 'us1-prod-direct.animalplanet.com'
|
||||
|
||||
@@ -5,6 +5,7 @@ from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
int_or_none,
|
||||
unified_strdate,
|
||||
url_or_none,
|
||||
)
|
||||
from ..compat import compat_urlparse
|
||||
|
||||
@@ -15,13 +16,13 @@ class DWIE(InfoExtractor):
|
||||
_TESTS = [{
|
||||
# video
|
||||
'url': 'http://www.dw.com/en/intelligent-light/av-19112290',
|
||||
'md5': '7372046e1815c5a534b43f3c3c36e6e9',
|
||||
'md5': 'fb9dfd9520811d3ece80f04befd73428',
|
||||
'info_dict': {
|
||||
'id': '19112290',
|
||||
'ext': 'mp4',
|
||||
'title': 'Intelligent light',
|
||||
'description': 'md5:90e00d5881719f2a6a5827cb74985af1',
|
||||
'upload_date': '20160311',
|
||||
'upload_date': '20160605',
|
||||
}
|
||||
}, {
|
||||
# audio
|
||||
@@ -55,15 +56,16 @@ class DWIE(InfoExtractor):
|
||||
title = hidden_inputs['media_title']
|
||||
media_id = hidden_inputs.get('media_id') or media_id
|
||||
|
||||
if hidden_inputs.get('player_type') == 'video' and hidden_inputs.get('stream_file') == '1':
|
||||
direct_url = url_or_none(hidden_inputs.get('file_name'))
|
||||
if direct_url:
|
||||
formats = [{'url': hidden_inputs['file_name']}]
|
||||
else:
|
||||
formats = self._extract_smil_formats(
|
||||
'http://www.dw.com/smil/v-%s' % media_id, media_id,
|
||||
transform_source=lambda s: s.replace(
|
||||
'rtmp://tv-od.dw.de/flash/',
|
||||
'http://tv-download.dw.de/dwtv_video/flv/'))
|
||||
self._sort_formats(formats)
|
||||
else:
|
||||
formats = [{'url': hidden_inputs['file_name']}]
|
||||
self._sort_formats(formats)
|
||||
|
||||
upload_date = hidden_inputs.get('display_date')
|
||||
if not upload_date:
|
||||
|
||||
@@ -147,6 +147,8 @@ from .bilibili import (
|
||||
BilibiliAudioAlbumIE,
|
||||
BiliBiliPlayerIE,
|
||||
BilibiliChannelIE,
|
||||
BiliIntlIE,
|
||||
BiliIntlSeriesIE,
|
||||
)
|
||||
from .biobiochiletv import BioBioChileTVIE
|
||||
from .bitchute import (
|
||||
@@ -183,6 +185,7 @@ from .businessinsider import BusinessInsiderIE
|
||||
from .buzzfeed import BuzzFeedIE
|
||||
from .byutv import BYUtvIE
|
||||
from .c56 import C56IE
|
||||
from .cam4 import CAM4IE
|
||||
from .camdemy import (
|
||||
CamdemyIE,
|
||||
CamdemyFolderIE
|
||||
@@ -205,9 +208,9 @@ from .cartoonnetwork import CartoonNetworkIE
|
||||
from .cbc import (
|
||||
CBCIE,
|
||||
CBCPlayerIE,
|
||||
CBCWatchVideoIE,
|
||||
CBCWatchIE,
|
||||
CBCOlympicsIE,
|
||||
CBCGemIE,
|
||||
CBCGemPlaylistIE,
|
||||
CBCGemLiveIE,
|
||||
)
|
||||
from .cbs import CBSIE
|
||||
from .cbslocal import (
|
||||
@@ -236,10 +239,15 @@ from .ceskatelevize import (
|
||||
CeskaTelevizeIE,
|
||||
CeskaTelevizePoradyIE,
|
||||
)
|
||||
from .cgtn import CGTNIE
|
||||
from .channel9 import Channel9IE
|
||||
from .charlierose import CharlieRoseIE
|
||||
from .chaturbate import ChaturbateIE
|
||||
from .chilloutzone import ChilloutzoneIE
|
||||
from .chingari import (
|
||||
ChingariIE,
|
||||
ChingariUserIE,
|
||||
)
|
||||
from .chirbit import (
|
||||
ChirbitIE,
|
||||
ChirbitProfileIE,
|
||||
@@ -250,6 +258,7 @@ from .ciscolive import (
|
||||
CiscoLiveSessionIE,
|
||||
CiscoLiveSearchIE,
|
||||
)
|
||||
from .ciscowebex import CiscoWebexIE
|
||||
from .cjsw import CJSWIE
|
||||
from .cliphunter import CliphunterIE
|
||||
from .clippit import ClippitIE
|
||||
@@ -307,6 +316,10 @@ from .dailymotion import (
|
||||
DailymotionPlaylistIE,
|
||||
DailymotionUserIE,
|
||||
)
|
||||
from .damtomo import (
|
||||
DamtomoRecordIE,
|
||||
DamtomoVideoIE,
|
||||
)
|
||||
from .daum import (
|
||||
DaumIE,
|
||||
DaumClipIE,
|
||||
@@ -328,7 +341,6 @@ from .discoveryplusindia import (
|
||||
DiscoveryPlusIndiaShowIE,
|
||||
)
|
||||
from .dotsub import DotsubIE
|
||||
from .douyin import DouyinIE
|
||||
from .douyutv import (
|
||||
DouyuShowIE,
|
||||
DouyuTVIE,
|
||||
@@ -337,7 +349,9 @@ from .dplay import (
|
||||
DPlayIE,
|
||||
DiscoveryPlusIE,
|
||||
HGTVDeIE,
|
||||
ScienceChannelIE
|
||||
ScienceChannelIE,
|
||||
DIYNetworkIE,
|
||||
AnimalPlanetIE
|
||||
)
|
||||
from .dreisat import DreiSatIE
|
||||
from .drbonanza import DRBonanzaIE
|
||||
@@ -460,12 +474,7 @@ from .franceinter import FranceInterIE
|
||||
from .francetv import (
|
||||
FranceTVIE,
|
||||
FranceTVSiteIE,
|
||||
FranceTVEmbedIE,
|
||||
FranceTVInfoIE,
|
||||
FranceTVInfoSportIE,
|
||||
FranceTVJeunesseIE,
|
||||
GenerationWhatIE,
|
||||
CultureboxIE,
|
||||
)
|
||||
from .freesound import FreesoundIE
|
||||
from .freespeech import FreespeechIE
|
||||
@@ -493,6 +502,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
|
||||
@@ -510,7 +520,9 @@ from .googlepodcasts import (
|
||||
GooglePodcastsFeedIE,
|
||||
)
|
||||
from .googlesearch import GoogleSearchIE
|
||||
from .gopro import GoProIE
|
||||
from .goshgay import GoshgayIE
|
||||
from .gotostage import GoToStageIE
|
||||
from .gputechconf import GPUTechConfIE
|
||||
from .groupon import GrouponIE
|
||||
from .hbo import HBOIE
|
||||
@@ -547,6 +559,10 @@ from .hungama import (
|
||||
HungamaAlbumPlaylistIE,
|
||||
)
|
||||
from .hypem import HypemIE
|
||||
from .ichinanalive import (
|
||||
IchinanaLiveIE,
|
||||
IchinanaLiveClipIE,
|
||||
)
|
||||
from .ign import (
|
||||
IGNIE,
|
||||
IGNVideoIE,
|
||||
@@ -613,6 +629,7 @@ from .kickstarter import KickStarterIE
|
||||
from .kinja import KinjaEmbedIE
|
||||
from .kinopoisk import KinoPoiskIE
|
||||
from .konserthusetplay import KonserthusetPlayIE
|
||||
from .koo import KooIE
|
||||
from .krasview import KrasViewIE
|
||||
from .ku6 import Ku6IE
|
||||
from .kusi import KUSIIE
|
||||
@@ -721,6 +738,8 @@ from .massengeschmacktv import MassengeschmackTVIE
|
||||
from .matchtv import MatchTVIE
|
||||
from .mdr import MDRIE
|
||||
from .medaltv import MedalTVIE
|
||||
from .mediaite import MediaiteIE
|
||||
from .mediaklikk import MediaKlikkIE
|
||||
from .mediaset import MediasetIE
|
||||
from .mediasite import (
|
||||
MediasiteIE,
|
||||
@@ -796,6 +815,7 @@ from .mtv import (
|
||||
MTVItaliaProgrammaIE,
|
||||
)
|
||||
from .muenchentv import MuenchenTVIE
|
||||
from .musescore import MuseScoreIE
|
||||
from .mwave import MwaveIE, MwaveMeetGreetIE
|
||||
from .mxplayer import (
|
||||
MxplayerIE,
|
||||
@@ -810,6 +830,7 @@ from .myvi import (
|
||||
)
|
||||
from .myvideoge import MyVideoGeIE
|
||||
from .myvidster import MyVidsterIE
|
||||
from .n1 import N1InfoIIE, N1InfoAssetIE
|
||||
from .nationalgeographic import (
|
||||
NationalGeographicVideoIE,
|
||||
NationalGeographicTVIE,
|
||||
@@ -858,6 +879,7 @@ from .neteasemusic import (
|
||||
from .newgrounds import (
|
||||
NewgroundsIE,
|
||||
NewgroundsPlaylistIE,
|
||||
NewgroundsUserIE,
|
||||
)
|
||||
from .newstube import NewstubeIE
|
||||
from .nextmedia import (
|
||||
@@ -911,6 +933,7 @@ from .nova import (
|
||||
NovaEmbedIE,
|
||||
NovaIE,
|
||||
)
|
||||
from .novaplay import NovaPlayIE
|
||||
from .nowness import (
|
||||
NownessIE,
|
||||
NownessPlaylistIE,
|
||||
@@ -951,10 +974,12 @@ from .nytimes import (
|
||||
NYTimesCookingIE,
|
||||
)
|
||||
from .nuvid import NuvidIE
|
||||
from .nzherald import NZHeraldIE
|
||||
from .nzz import NZZIE
|
||||
from .odatv import OdaTVIE
|
||||
from .odnoklassniki import OdnoklassnikiIE
|
||||
from .oktoberfesttv import OktoberfestTVIE
|
||||
from .olympics import OlympicsReplayIE
|
||||
from .ondemandkorea import OnDemandKoreaIE
|
||||
from .onet import (
|
||||
OnetIE,
|
||||
@@ -1012,7 +1037,10 @@ from .patreon import (
|
||||
)
|
||||
from .pbs import PBSIE
|
||||
from .pearvideo import PearVideoIE
|
||||
from .peertube import PeerTubeIE
|
||||
from .peertube import (
|
||||
PeerTubeIE,
|
||||
PeerTubePlaylistIE,
|
||||
)
|
||||
from .peloton import (
|
||||
PelotonIE,
|
||||
PelotonLiveIE
|
||||
@@ -1107,6 +1135,11 @@ from .radiode import RadioDeIE
|
||||
from .radiojavan import RadioJavanIE
|
||||
from .radiobremen import RadioBremenIE
|
||||
from .radiofrance import RadioFranceIE
|
||||
from .radlive import (
|
||||
RadLiveIE,
|
||||
RadLiveChannelIE,
|
||||
RadLiveSeasonIE,
|
||||
)
|
||||
from .rai import (
|
||||
RaiPlayIE,
|
||||
RaiPlayLiveIE,
|
||||
@@ -1170,7 +1203,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,
|
||||
@@ -1278,6 +1314,10 @@ from .southpark import (
|
||||
SouthParkEsIE,
|
||||
SouthParkNlIE
|
||||
)
|
||||
from .sovietscloset import (
|
||||
SovietsClosetIE,
|
||||
SovietsClosetPlaylistIE
|
||||
)
|
||||
from .spankbang import (
|
||||
SpankBangIE,
|
||||
SpankBangPlaylistIE,
|
||||
@@ -1321,6 +1361,7 @@ from .storyfire import (
|
||||
StoryFireSeriesIE,
|
||||
)
|
||||
from .streamable import StreamableIE
|
||||
from .streamanity import StreamanityIE
|
||||
from .streamcloud import StreamcloudIE
|
||||
from .streamcz import StreamCZIE
|
||||
from .streetvoice import StreetVoiceIE
|
||||
@@ -1389,6 +1430,10 @@ from .theplatform import (
|
||||
from .thescene import TheSceneIE
|
||||
from .thestar import TheStarIE
|
||||
from .thesun import TheSunIE
|
||||
from .theta import (
|
||||
ThetaVideoIE,
|
||||
ThetaStreamIE,
|
||||
)
|
||||
from .theweatherchannel import TheWeatherChannelIE
|
||||
from .thisamericanlife import ThisAmericanLifeIE
|
||||
from .thisav import ThisAVIE
|
||||
@@ -1397,6 +1442,7 @@ from .threeqsdn import ThreeQSDNIE
|
||||
from .tiktok import (
|
||||
TikTokIE,
|
||||
TikTokUserIE,
|
||||
DouyinIE,
|
||||
)
|
||||
from .tinypic import TinyPicIE
|
||||
from .tmz import TMZIE
|
||||
@@ -1560,6 +1606,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 (
|
||||
@@ -1594,11 +1641,6 @@ from .vidio import (
|
||||
VidioLiveIE
|
||||
)
|
||||
from .vidlii import VidLiiIE
|
||||
from .vidme import (
|
||||
VidmeIE,
|
||||
VidmeUserIE,
|
||||
VidmeUserLikesIE,
|
||||
)
|
||||
from .vier import VierIE, VierVideosIE
|
||||
from .viewlift import (
|
||||
ViewLiftIE,
|
||||
@@ -1669,6 +1711,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,
|
||||
@@ -1755,7 +1798,11 @@ from .yandexmusic import (
|
||||
YandexMusicArtistTracksIE,
|
||||
YandexMusicArtistAlbumsIE,
|
||||
)
|
||||
from .yandexvideo import YandexVideoIE
|
||||
from .yandexvideo import (
|
||||
YandexVideoIE,
|
||||
ZenYandexIE,
|
||||
ZenYandexChannelIE,
|
||||
)
|
||||
from .yapfiles import YapFilesIE
|
||||
from .yesjapan import YesJapanIE
|
||||
from .yinyuetai import YinYueTaiIE
|
||||
@@ -1775,6 +1822,7 @@ from .yourporn import YourPornIE
|
||||
from .yourupload import YourUploadIE
|
||||
from .youtube import (
|
||||
YoutubeIE,
|
||||
YoutubeClipIE,
|
||||
YoutubeFavouritesIE,
|
||||
YoutubeHistoryIE,
|
||||
YoutubeTabIE,
|
||||
|
||||
@@ -173,7 +173,7 @@ class FancodeLiveIE(FancodeVodIE):
|
||||
|
||||
match_info = try_get(info_json, lambda x: x['data']['match'])
|
||||
|
||||
if match_info.get('status') != "LIVE":
|
||||
if match_info.get('streamingStatus') != "STARTED":
|
||||
raise ExtractorError('The stream can\'t be accessed', expected=True)
|
||||
self._check_login_required(match_info.get('isUserEntitled'), True) # all live streams are premium only
|
||||
|
||||
|
||||
@@ -4,19 +4,12 @@ from __future__ import unicode_literals
|
||||
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import (
|
||||
compat_str,
|
||||
)
|
||||
from ..utils import (
|
||||
clean_html,
|
||||
determine_ext,
|
||||
ExtractorError,
|
||||
int_or_none,
|
||||
parse_duration,
|
||||
format_field,
|
||||
parse_iso8601,
|
||||
parse_qs,
|
||||
try_get,
|
||||
url_or_none,
|
||||
urljoin,
|
||||
)
|
||||
from .dailymotion import DailymotionIE
|
||||
|
||||
@@ -89,97 +82,81 @@ class FranceTVIE(InfoExtractor):
|
||||
# Videos are identified by idDiffusion so catalogue part is optional.
|
||||
# However when provided, some extra formats may be returned so we pass
|
||||
# it if available.
|
||||
info = self._download_json(
|
||||
'https://sivideo.webservices.francetelevisions.fr/tools/getInfosOeuvre/v2/',
|
||||
video_id, 'Downloading video JSON', query={
|
||||
'idDiffusion': video_id,
|
||||
'catalogue': catalogue or '',
|
||||
})
|
||||
|
||||
if info.get('status') == 'NOK':
|
||||
raise ExtractorError(
|
||||
'%s returned error: %s' % (self.IE_NAME, info['message']),
|
||||
expected=True)
|
||||
allowed_countries = info['videos'][0].get('geoblocage')
|
||||
if allowed_countries:
|
||||
georestricted = True
|
||||
geo_info = self._download_json(
|
||||
'http://geo.francetv.fr/ws/edgescape.json', video_id,
|
||||
'Downloading geo restriction info')
|
||||
country = geo_info['reponse']['geo_info']['country_code']
|
||||
if country not in allowed_countries:
|
||||
raise ExtractorError(
|
||||
'The video is not available from your location',
|
||||
expected=True)
|
||||
else:
|
||||
georestricted = False
|
||||
|
||||
def sign(manifest_url, manifest_id):
|
||||
for host in ('hdfauthftv-a.akamaihd.net', 'hdfauth.francetv.fr'):
|
||||
signed_url = url_or_none(self._download_webpage(
|
||||
'https://%s/esi/TA' % host, video_id,
|
||||
'Downloading signed %s manifest URL' % manifest_id,
|
||||
fatal=False, query={
|
||||
'url': manifest_url,
|
||||
}))
|
||||
if signed_url:
|
||||
return signed_url
|
||||
return manifest_url
|
||||
|
||||
is_live = None
|
||||
|
||||
videos = []
|
||||
title = None
|
||||
subtitle = None
|
||||
image = None
|
||||
duration = None
|
||||
timestamp = None
|
||||
spritesheets = None
|
||||
|
||||
for video in (info.get('videos') or []):
|
||||
if video.get('statut') != 'ONLINE':
|
||||
for device_type in ('desktop', 'mobile'):
|
||||
dinfo = self._download_json(
|
||||
'https://player.webservices.francetelevisions.fr/v1/videos/%s' % video_id,
|
||||
video_id, 'Downloading %s video JSON' % device_type, query={
|
||||
'device_type': device_type,
|
||||
'browser': 'chrome',
|
||||
}, fatal=False)
|
||||
|
||||
if not dinfo:
|
||||
continue
|
||||
if not video.get('url'):
|
||||
continue
|
||||
videos.append(video)
|
||||
|
||||
if not videos:
|
||||
for device_type in ['desktop', 'mobile']:
|
||||
fallback_info = self._download_json(
|
||||
'https://player.webservices.francetelevisions.fr/v1/videos/%s' % video_id,
|
||||
video_id, 'Downloading fallback %s video JSON' % device_type, query={
|
||||
'device_type': device_type,
|
||||
'browser': 'chrome',
|
||||
}, fatal=False)
|
||||
video = dinfo.get('video')
|
||||
if video:
|
||||
videos.append(video)
|
||||
if duration is None:
|
||||
duration = video.get('duration')
|
||||
if is_live is None:
|
||||
is_live = video.get('is_live')
|
||||
if spritesheets is None:
|
||||
spritesheets = video.get('spritesheets')
|
||||
|
||||
if fallback_info and fallback_info.get('video'):
|
||||
videos.append(fallback_info['video'])
|
||||
meta = dinfo.get('meta')
|
||||
if meta:
|
||||
if title is None:
|
||||
title = meta.get('title')
|
||||
# XXX: what is meta['pre_title']?
|
||||
if subtitle is None:
|
||||
subtitle = meta.get('additional_title')
|
||||
if image is None:
|
||||
image = meta.get('image_url')
|
||||
if timestamp is None:
|
||||
timestamp = parse_iso8601(meta.get('broadcasted_at'))
|
||||
|
||||
formats = []
|
||||
subtitles = {}
|
||||
for video in videos:
|
||||
video_url = video.get('url')
|
||||
if not video_url:
|
||||
continue
|
||||
if is_live is None:
|
||||
is_live = (try_get(
|
||||
video, lambda x: x['plages_ouverture'][0]['direct'], bool) is True
|
||||
or video.get('is_live') is True
|
||||
or '/live.francetv.fr/' in video_url)
|
||||
format_id = video.get('format')
|
||||
|
||||
video_url = None
|
||||
if video.get('workflow') == 'token-akamai':
|
||||
token_url = video.get('token')
|
||||
if token_url:
|
||||
token_json = self._download_json(
|
||||
token_url, video_id,
|
||||
'Downloading signed %s manifest URL' % format_id)
|
||||
if token_json:
|
||||
video_url = token_json.get('url')
|
||||
if not video_url:
|
||||
video_url = video.get('url')
|
||||
|
||||
ext = determine_ext(video_url)
|
||||
if ext == 'f4m':
|
||||
if georestricted:
|
||||
# See https://github.com/ytdl-org/youtube-dl/issues/3963
|
||||
# m3u8 urls work fine
|
||||
continue
|
||||
formats.extend(self._extract_f4m_formats(
|
||||
sign(video_url, format_id) + '&hdcore=3.7.0&plugin=aasp-3.7.0.39.44',
|
||||
video_id, f4m_id=format_id, fatal=False))
|
||||
video_url, video_id, f4m_id=format_id, fatal=False))
|
||||
elif ext == 'm3u8':
|
||||
m3u8_fmts, m3u8_subs = self._extract_m3u8_formats_and_subtitles(
|
||||
sign(video_url, format_id), video_id, 'mp4',
|
||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
||||
video_url, video_id, 'mp4',
|
||||
entry_protocol='m3u8_native', m3u8_id=format_id,
|
||||
fatal=False)
|
||||
formats.extend(m3u8_fmts)
|
||||
subtitles = self._merge_subtitles(subtitles, m3u8_subs)
|
||||
formats.extend(fmts)
|
||||
self._merge_subtitles(subs, target=subtitles)
|
||||
elif ext == 'mpd':
|
||||
formats.extend(self._extract_mpd_formats(
|
||||
sign(video_url, format_id), video_id, mpd_id=format_id, fatal=False))
|
||||
fmts, subs = self._extract_mpd_formats_and_subtitles(
|
||||
video_url, video_id, mpd_id=format_id, fatal=False)
|
||||
formats.extend(fmts)
|
||||
self._merge_subtitles(subs, target=subtitles)
|
||||
elif video_url.startswith('rtmp'):
|
||||
formats.append({
|
||||
'url': video_url,
|
||||
@@ -193,28 +170,43 @@ class FranceTVIE(InfoExtractor):
|
||||
'format_id': format_id,
|
||||
})
|
||||
|
||||
# XXX: what is video['captions']?
|
||||
|
||||
for f in formats:
|
||||
if f.get('acodec') != 'none' and f.get('language') in ('qtz', 'qad'):
|
||||
f['language_preference'] = -10
|
||||
f['format_note'] = 'audio description%s' % format_field(f, 'format_note', ', %s')
|
||||
|
||||
if spritesheets:
|
||||
formats.append({
|
||||
'format_id': 'spritesheets',
|
||||
'format_note': 'storyboard',
|
||||
'acodec': 'none',
|
||||
'vcodec': 'none',
|
||||
'ext': 'mhtml',
|
||||
'protocol': 'mhtml',
|
||||
'url': 'about:dummy',
|
||||
'fragments': [{
|
||||
'path': sheet,
|
||||
# XXX: not entirely accurate; each spritesheet seems to be
|
||||
# a 10×10 grid of thumbnails corresponding to approximately
|
||||
# 2 seconds of the video; the last spritesheet may be shorter
|
||||
'duration': 200,
|
||||
} for sheet in spritesheets]
|
||||
})
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
title = info['titre']
|
||||
subtitle = info.get('sous_titre')
|
||||
if subtitle:
|
||||
title += ' - %s' % subtitle
|
||||
title = title.strip()
|
||||
|
||||
subtitles.setdefault('fr', []).extend(
|
||||
[{
|
||||
'url': subformat['url'],
|
||||
'ext': subformat.get('format'),
|
||||
} for subformat in info.get('subtitles', []) if subformat.get('url')]
|
||||
)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': self._live_title(title) if is_live else title,
|
||||
'description': clean_html(info.get('synopsis')),
|
||||
'thumbnail': urljoin('https://sivideo.webservices.francetelevisions.fr', info.get('image')),
|
||||
'duration': int_or_none(info.get('real_duration')) or parse_duration(info.get('duree')),
|
||||
'timestamp': int_or_none(try_get(info, lambda x: x['diffusion']['timestamp'])),
|
||||
'thumbnail': image,
|
||||
'duration': duration,
|
||||
'timestamp': timestamp,
|
||||
'is_live': is_live,
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
@@ -308,35 +300,6 @@ class FranceTVSiteIE(FranceTVBaseInfoExtractor):
|
||||
return self._make_url_result(video_id, catalogue)
|
||||
|
||||
|
||||
class FranceTVEmbedIE(FranceTVBaseInfoExtractor):
|
||||
_VALID_URL = r'https?://embed\.francetv\.fr/*\?.*?\bue=(?P<id>[^&]+)'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'http://embed.francetv.fr/?ue=7fd581a2ccf59d2fc5719c5c13cf6961',
|
||||
'info_dict': {
|
||||
'id': 'NI_983319',
|
||||
'ext': 'mp4',
|
||||
'title': 'Le Pen Reims',
|
||||
'upload_date': '20170505',
|
||||
'timestamp': 1493981780,
|
||||
'duration': 16,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
'add_ie': [FranceTVIE.ie_key()],
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
|
||||
video = self._download_json(
|
||||
'http://api-embed.webservices.francetelevisions.fr/key/%s' % video_id,
|
||||
video_id)
|
||||
|
||||
return self._make_url_result(video['video_id'], video.get('catalog'))
|
||||
|
||||
|
||||
class FranceTVInfoIE(FranceTVBaseInfoExtractor):
|
||||
IE_NAME = 'francetvinfo.fr'
|
||||
_VALID_URL = r'https?://(?:www|mobile|france3-regions)\.francetvinfo\.fr/(?:[^/]+/)*(?P<id>[^/?#&.]+)'
|
||||
@@ -426,139 +389,3 @@ class FranceTVInfoIE(FranceTVBaseInfoExtractor):
|
||||
webpage, 'video id')
|
||||
|
||||
return self._make_url_result(video_id)
|
||||
|
||||
|
||||
class FranceTVInfoSportIE(FranceTVBaseInfoExtractor):
|
||||
IE_NAME = 'sport.francetvinfo.fr'
|
||||
_VALID_URL = r'https?://sport\.francetvinfo\.fr/(?:[^/]+/)*(?P<id>[^/?#&]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://sport.francetvinfo.fr/les-jeux-olympiques/retour-sur-les-meilleurs-moments-de-pyeongchang-2018',
|
||||
'info_dict': {
|
||||
'id': '6e49080e-3f45-11e8-b459-000d3a2439ea',
|
||||
'ext': 'mp4',
|
||||
'title': 'Retour sur les meilleurs moments de Pyeongchang 2018',
|
||||
'timestamp': 1523639962,
|
||||
'upload_date': '20180413',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
'add_ie': [FranceTVIE.ie_key()],
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
video_id = self._search_regex(r'data-video="([^"]+)"', webpage, 'video_id')
|
||||
return self._make_url_result(video_id, 'Sport-web')
|
||||
|
||||
|
||||
class GenerationWhatIE(InfoExtractor):
|
||||
IE_NAME = 'france2.fr:generation-what'
|
||||
_VALID_URL = r'https?://generation-what\.francetv\.fr/[^/]+/video/(?P<id>[^/?#&]+)'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'http://generation-what.francetv.fr/portrait/video/present-arms',
|
||||
'info_dict': {
|
||||
'id': 'wtvKYUG45iw',
|
||||
'ext': 'mp4',
|
||||
'title': 'Generation What - Garde à vous - FRA',
|
||||
'uploader': 'Generation What',
|
||||
'uploader_id': 'UCHH9p1eetWCgt4kXBYCb3_w',
|
||||
'upload_date': '20160411',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
'add_ie': ['Youtube'],
|
||||
}, {
|
||||
'url': 'http://generation-what.francetv.fr/europe/video/present-arms',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
|
||||
youtube_id = self._search_regex(
|
||||
r"window\.videoURL\s*=\s*'([0-9A-Za-z_-]{11})';",
|
||||
webpage, 'youtube id')
|
||||
|
||||
return self.url_result(youtube_id, ie='Youtube', video_id=youtube_id)
|
||||
|
||||
|
||||
class CultureboxIE(FranceTVBaseInfoExtractor):
|
||||
_VALID_URL = r'https?://(?:m\.)?culturebox\.francetvinfo\.fr/(?:[^/]+/)*(?P<id>[^/?#&]+)'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://culturebox.francetvinfo.fr/opera-classique/musique-classique/c-est-baroque/concerts/cantates-bwv-4-106-et-131-de-bach-par-raphael-pichon-57-268689',
|
||||
'info_dict': {
|
||||
'id': 'EV_134885',
|
||||
'ext': 'mp4',
|
||||
'title': 'Cantates BWV 4, 106 et 131 de Bach par Raphaël Pichon 5/7',
|
||||
'description': 'md5:19c44af004b88219f4daa50fa9a351d4',
|
||||
'upload_date': '20180206',
|
||||
'timestamp': 1517945220,
|
||||
'duration': 5981,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
'add_ie': [FranceTVIE.ie_key()],
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
|
||||
if ">Ce live n'est plus disponible en replay<" in webpage:
|
||||
raise ExtractorError(
|
||||
'Video %s is not available' % display_id, expected=True)
|
||||
|
||||
video_id, catalogue = self._search_regex(
|
||||
r'["\'>]https?://videos\.francetv\.fr/video/([^@]+@.+?)["\'<]',
|
||||
webpage, 'video id').split('@')
|
||||
|
||||
return self._make_url_result(video_id, catalogue)
|
||||
|
||||
|
||||
class FranceTVJeunesseIE(FranceTVBaseInfoExtractor):
|
||||
_VALID_URL = r'(?P<url>https?://(?:www\.)?(?:zouzous|ludo)\.fr/heros/(?P<id>[^/?#&]+))'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://www.zouzous.fr/heros/simon',
|
||||
'info_dict': {
|
||||
'id': 'simon',
|
||||
},
|
||||
'playlist_count': 9,
|
||||
}, {
|
||||
'url': 'https://www.ludo.fr/heros/ninjago',
|
||||
'info_dict': {
|
||||
'id': 'ninjago',
|
||||
},
|
||||
'playlist_count': 10,
|
||||
}, {
|
||||
'url': 'https://www.zouzous.fr/heros/simon?abc',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = self._match_valid_url(url)
|
||||
playlist_id = mobj.group('id')
|
||||
|
||||
playlist = self._download_json(
|
||||
'%s/%s' % (mobj.group('url'), 'playlist'), playlist_id)
|
||||
|
||||
if not playlist.get('count'):
|
||||
raise ExtractorError(
|
||||
'%s is not available' % playlist_id, expected=True)
|
||||
|
||||
entries = []
|
||||
for item in playlist['items']:
|
||||
identity = item.get('identity')
|
||||
if identity and isinstance(identity, compat_str):
|
||||
entries.append(self._make_url_result(identity))
|
||||
|
||||
return self.playlist_result(entries, playlist_id)
|
||||
|
||||
@@ -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>[^/?#&]+))/?(?:[?#]|$)'
|
||||
|
||||
@@ -302,24 +324,29 @@ class FunimationShowIE(FunimationIE):
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_initialize(self):
|
||||
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=US&deviceType=web&locale=%s'
|
||||
% (display_id, locale or 'en'), display_id)
|
||||
items = 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_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))),
|
||||
}
|
||||
|
||||
@@ -1215,14 +1215,13 @@ class GenericIE(InfoExtractor):
|
||||
},
|
||||
{
|
||||
# JWPlatform iframe
|
||||
'url': 'https://www.mediaite.com/tv/dem-senator-claims-gary-cohn-faked-a-bad-connection-during-trump-call-to-get-him-off-the-phone/',
|
||||
'md5': 'ca00a040364b5b439230e7ebfd02c4e9',
|
||||
'url': 'https://www.covermagazine.co.uk/feature/2465255/business-protection-involved',
|
||||
'info_dict': {
|
||||
'id': 'O0c5JcKT',
|
||||
'id': 'AG26UQXM',
|
||||
'ext': 'mp4',
|
||||
'upload_date': '20171122',
|
||||
'timestamp': 1511366290,
|
||||
'title': 'Dem Senator Claims Gary Cohn Faked a Bad Connection During Trump Call to Get Him Off the Phone',
|
||||
'upload_date': '20160719',
|
||||
'timestamp': 468923808,
|
||||
'title': '2016_05_18 Cover L&G Business Protection V1 FINAL.mp4',
|
||||
},
|
||||
'add_ie': [JWPlatformIE.ie_key()],
|
||||
},
|
||||
@@ -2756,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
|
||||
|
||||
110
yt_dlp/extractor/gettr.py
Normal file
110
yt_dlp/extractor/gettr.py
Normal file
@@ -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'),
|
||||
}
|
||||
@@ -9,15 +9,14 @@ import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import (
|
||||
compat_HTTPError,
|
||||
compat_str,
|
||||
)
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
float_or_none,
|
||||
int_or_none,
|
||||
orderedSet,
|
||||
str_or_none,
|
||||
try_get,
|
||||
)
|
||||
|
||||
|
||||
@@ -26,18 +25,19 @@ class GloboIE(InfoExtractor):
|
||||
_NETRC_MACHINE = 'globo'
|
||||
_TESTS = [{
|
||||
'url': 'http://g1.globo.com/carros/autoesporte/videos/t/exclusivos-do-g1/v/mercedes-benz-gla-passa-por-teste-de-colisao-na-europa/3607726/',
|
||||
'md5': 'b3ccc801f75cd04a914d51dadb83a78d',
|
||||
'info_dict': {
|
||||
'id': '3607726',
|
||||
'ext': 'mp4',
|
||||
'title': 'Mercedes-Benz GLA passa por teste de colisão na Europa',
|
||||
'duration': 103.204,
|
||||
'uploader': 'Globo.com',
|
||||
'uploader_id': '265',
|
||||
'uploader': 'G1',
|
||||
'uploader_id': '2015',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
'url': 'http://globoplay.globo.com/v/4581987/',
|
||||
'md5': 'f36a1ecd6a50da1577eee6dd17f67eff',
|
||||
'info_dict': {
|
||||
'id': '4581987',
|
||||
'ext': 'mp4',
|
||||
@@ -46,6 +46,9 @@ class GloboIE(InfoExtractor):
|
||||
'uploader': 'Rede Globo',
|
||||
'uploader_id': '196',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
'url': 'http://canalbrasil.globo.com/programas/sangue-latino/videos/3928201.html',
|
||||
'only_matching': True,
|
||||
@@ -66,30 +69,6 @@ class GloboIE(InfoExtractor):
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_initialize(self):
|
||||
email, password = self._get_login_info()
|
||||
if email is None:
|
||||
return
|
||||
|
||||
try:
|
||||
glb_id = (self._download_json(
|
||||
'https://login.globo.com/api/authentication', None, data=json.dumps({
|
||||
'payload': {
|
||||
'email': email,
|
||||
'password': password,
|
||||
'serviceId': 4654,
|
||||
},
|
||||
}).encode(), headers={
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
}) or {}).get('glbId')
|
||||
if glb_id:
|
||||
self._set_cookie('.globo.com', 'GLBID', glb_id)
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
|
||||
resp = self._parse_json(e.cause.read(), None)
|
||||
raise ExtractorError(resp.get('userMessage') or resp['id'], expected=True)
|
||||
raise
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
|
||||
@@ -102,73 +81,67 @@ class GloboIE(InfoExtractor):
|
||||
title = video['title']
|
||||
|
||||
formats = []
|
||||
security = self._download_json(
|
||||
'https://playback.video.globo.com/v1/video-session', video_id, 'Downloading security hash for %s' % video_id,
|
||||
headers={'content-type': 'application/json'}, data=json.dumps({
|
||||
"player_type": "desktop",
|
||||
"video_id": video_id,
|
||||
"quality": "max",
|
||||
"content_protection": "widevine",
|
||||
"vsid": "581b986b-4c40-71f0-5a58-803e579d5fa2",
|
||||
"tz": "-3.0:00"
|
||||
}).encode())
|
||||
|
||||
security_hash = security['source']['token']
|
||||
if not security_hash:
|
||||
message = security.get('message')
|
||||
if message:
|
||||
raise ExtractorError(
|
||||
'%s returned error: %s' % (self.IE_NAME, message), expected=True)
|
||||
|
||||
hash_code = security_hash[:2]
|
||||
padding = '%010d' % random.randint(1, 10000000000)
|
||||
if hash_code in ('04', '14'):
|
||||
received_time = security_hash[3:13]
|
||||
received_md5 = security_hash[24:]
|
||||
hash_prefix = security_hash[:23]
|
||||
elif hash_code in ('02', '12', '03', '13'):
|
||||
received_time = security_hash[2:12]
|
||||
received_md5 = security_hash[22:]
|
||||
padding += '1'
|
||||
hash_prefix = '05' + security_hash[:22]
|
||||
|
||||
padded_sign_time = compat_str(int(received_time) + 86400) + padding
|
||||
md5_data = (received_md5 + padded_sign_time + '0xAC10FD').encode()
|
||||
signed_md5 = base64.urlsafe_b64encode(hashlib.md5(md5_data).digest()).decode().strip('=')
|
||||
signed_hash = hash_prefix + padded_sign_time + signed_md5
|
||||
source = security['source']['url_parts']
|
||||
resource_url = source['scheme'] + '://' + source['domain'] + source['path']
|
||||
signed_url = '%s?h=%s&k=html5&a=%s' % (resource_url, signed_hash, 'F' if video.get('subscriber_only') else 'A')
|
||||
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
signed_url, video_id, 'mp4', entry_protocol='m3u8_native', m3u8_id='hls', fatal=False))
|
||||
self._sort_formats(formats)
|
||||
|
||||
subtitles = {}
|
||||
for resource in video['resources']:
|
||||
resource_id = resource.get('_id')
|
||||
resource_url = resource.get('url')
|
||||
resource_type = resource.get('type')
|
||||
if not resource_url or (resource_type == 'media' and not resource_id) or resource_type not in ('subtitle', 'media'):
|
||||
continue
|
||||
|
||||
if resource_type == 'subtitle':
|
||||
if resource.get('type') == 'subtitle':
|
||||
subtitles.setdefault(resource.get('language') or 'por', []).append({
|
||||
'url': resource_url,
|
||||
'url': resource.get('url'),
|
||||
})
|
||||
continue
|
||||
|
||||
security = self._download_json(
|
||||
'http://security.video.globo.com/videos/%s/hash' % video_id,
|
||||
video_id, 'Downloading security hash for %s' % resource_id, query={
|
||||
'player': 'desktop',
|
||||
'version': '5.19.1',
|
||||
'resource_id': resource_id,
|
||||
subs = try_get(security, lambda x: x['source']['subtitles'], expected_type=dict) or {}
|
||||
for sub_lang, sub_url in subs.items():
|
||||
if sub_url:
|
||||
subtitles.setdefault(sub_lang or 'por', []).append({
|
||||
'url': sub_url,
|
||||
})
|
||||
|
||||
security_hash = security.get('hash')
|
||||
if not security_hash:
|
||||
message = security.get('message')
|
||||
if message:
|
||||
raise ExtractorError(
|
||||
'%s returned error: %s' % (self.IE_NAME, message), expected=True)
|
||||
continue
|
||||
|
||||
hash_code = security_hash[:2]
|
||||
padding = '%010d' % random.randint(1, 10000000000)
|
||||
if hash_code in ('04', '14'):
|
||||
received_time = security_hash[3:13]
|
||||
received_md5 = security_hash[24:]
|
||||
hash_prefix = security_hash[:23]
|
||||
elif hash_code in ('02', '12', '03', '13'):
|
||||
received_time = security_hash[2:12]
|
||||
received_md5 = security_hash[22:]
|
||||
padding += '1'
|
||||
hash_prefix = '05' + security_hash[:22]
|
||||
|
||||
padded_sign_time = compat_str(int(received_time) + 86400) + padding
|
||||
md5_data = (received_md5 + padded_sign_time + '0xAC10FD').encode()
|
||||
signed_md5 = base64.urlsafe_b64encode(hashlib.md5(md5_data).digest()).decode().strip('=')
|
||||
signed_hash = hash_prefix + padded_sign_time + signed_md5
|
||||
signed_url = '%s?h=%s&k=html5&a=%s&u=%s' % (resource_url, signed_hash, 'F' if video.get('subscriber_only') else 'A', security.get('user') or '')
|
||||
|
||||
if resource_id.endswith('m3u8') or resource_url.endswith('.m3u8'):
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
signed_url, resource_id, 'mp4', entry_protocol='m3u8_native',
|
||||
m3u8_id='hls', fatal=False))
|
||||
elif resource_id.endswith('mpd') or resource_url.endswith('.mpd'):
|
||||
formats.extend(self._extract_mpd_formats(
|
||||
signed_url, resource_id, mpd_id='dash', fatal=False))
|
||||
elif resource_id.endswith('manifest') or resource_url.endswith('/manifest'):
|
||||
formats.extend(self._extract_ism_formats(
|
||||
signed_url, resource_id, ism_id='mss', fatal=False))
|
||||
else:
|
||||
formats.append({
|
||||
'url': signed_url,
|
||||
'format_id': 'http-%s' % resource_id,
|
||||
'height': int_or_none(resource.get('height')),
|
||||
subs = try_get(security, lambda x: x['source']['subtitles_webvtt'], expected_type=dict) or {}
|
||||
for sub_lang, sub_url in subs.items():
|
||||
if sub_url:
|
||||
subtitles.setdefault(sub_lang or 'por', []).append({
|
||||
'url': sub_url,
|
||||
})
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
duration = float_or_none(video.get('duration'), 1000)
|
||||
uploader = video.get('channel')
|
||||
uploader_id = str_or_none(video.get('channel_id'))
|
||||
|
||||
110
yt_dlp/extractor/gopro.py
Normal file
110
yt_dlp/extractor/gopro.py
Normal file
@@ -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')),
|
||||
}
|
||||
73
yt_dlp/extractor/gotostage.py
Normal file
73
yt_dlp/extractor/gotostage.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_str
|
||||
from ..utils import (
|
||||
try_get,
|
||||
url_or_none
|
||||
)
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class GoToStageIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?gotostage\.com/channel/[a-z0-9]+/recording/(?P<id>[a-z0-9]+)/watch'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.gotostage.com/channel/8901680603948959494/recording/60bb55548d434f21b9ce4f0e225c4895/watch',
|
||||
'md5': 'ca72ce990cdcd7a2bd152f7217e319a2',
|
||||
'info_dict': {
|
||||
'id': '60bb55548d434f21b9ce4f0e225c4895',
|
||||
'ext': 'mp4',
|
||||
'title': 'What is GoToStage?',
|
||||
'thumbnail': r're:^https?://.*\.jpg$',
|
||||
'duration': 93.924711
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.gotostage.com/channel/bacc3d3535b34bafacc3f4ef8d4df78a/recording/831e74cd3e0042be96defba627b6f676/watch?source=HOMEPAGE',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
metadata = self._download_json(
|
||||
'https://api.gotostage.com/contents?ids=%s' % video_id,
|
||||
video_id,
|
||||
note='Downloading video metadata',
|
||||
errnote='Unable to download video metadata')[0]
|
||||
|
||||
registration_data = {
|
||||
'product': metadata['product'],
|
||||
'resourceType': metadata['contentType'],
|
||||
'productReferenceKey': metadata['productRefKey'],
|
||||
'firstName': 'foo',
|
||||
'lastName': 'bar',
|
||||
'email': 'foobar@example.com'
|
||||
}
|
||||
|
||||
registration_response = self._download_json(
|
||||
'https://api-registrations.logmeininc.com/registrations',
|
||||
video_id,
|
||||
data=json.dumps(registration_data).encode(),
|
||||
expected_status=409,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
note='Register user',
|
||||
errnote='Unable to register user')
|
||||
|
||||
content_response = self._download_json(
|
||||
'https://api.gotostage.com/contents/%s/asset' % video_id,
|
||||
video_id,
|
||||
headers={'x-registrantkey': registration_response['registrationKey']},
|
||||
note='Get download url',
|
||||
errnote='Unable to get download url')
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': try_get(metadata, lambda x: x['title'], compat_str),
|
||||
'url': try_get(content_response, lambda x: x['cdnLocation'], compat_str),
|
||||
'ext': 'mp4',
|
||||
'thumbnail': url_or_none(try_get(metadata, lambda x: x['thumbnail']['location'])),
|
||||
'duration': try_get(metadata, lambda x: x['duration'], float),
|
||||
'categories': [try_get(metadata, lambda x: x['category'], compat_str)],
|
||||
'is_live': False
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_str
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
int_or_none,
|
||||
try_get,
|
||||
url_or_none,
|
||||
urlencode_postdata,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
@@ -53,65 +52,70 @@ class HiDiveIE(InfoExtractor):
|
||||
self._download_webpage(
|
||||
self._LOGIN_URL, None, 'Logging in', data=urlencode_postdata(data))
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = self._match_valid_url(url)
|
||||
title, key = mobj.group('title', 'key')
|
||||
video_id = '%s/%s' % (title, key)
|
||||
|
||||
settings = self._download_json(
|
||||
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({
|
||||
'Title': title,
|
||||
'Key': key,
|
||||
'PlayerId': 'f4f895ce1ca713ba263b91caeb1daa2d08904783',
|
||||
}))
|
||||
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):
|
||||
video_id, title, key = self._match_valid_url(url).group('id', 'title', 'key')
|
||||
settings = self._call_api(video_id, title, key)
|
||||
|
||||
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)
|
||||
|
||||
formats = []
|
||||
subtitles = {}
|
||||
formats, parsed_urls = [], {}, {None}
|
||||
for rendition_id, rendition in settings['renditions'].items():
|
||||
bitrates = rendition.get('bitrates')
|
||||
if not isinstance(bitrates, dict):
|
||||
continue
|
||||
m3u8_url = url_or_none(bitrates.get('hls'))
|
||||
if not m3u8_url:
|
||||
continue
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
m3u8_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||
m3u8_id='%s-hls' % rendition_id, fatal=False))
|
||||
cc_files = rendition.get('ccFiles')
|
||||
if not isinstance(cc_files, list):
|
||||
continue
|
||||
for cc_file in cc_files:
|
||||
if not isinstance(cc_file, list) or len(cc_file) < 3:
|
||||
continue
|
||||
cc_lang = cc_file[0]
|
||||
cc_url = url_or_none(cc_file[2])
|
||||
if not isinstance(cc_lang, compat_str) or not cc_url:
|
||||
continue
|
||||
subtitles.setdefault(cc_lang, []).append({
|
||||
'url': cc_url,
|
||||
})
|
||||
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=rendition_id, fatal=False)
|
||||
for f in frmt:
|
||||
f['language'] = audio
|
||||
f['format_note'] = f'{version}, {extra}'
|
||||
formats.extend(frmt)
|
||||
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}
|
||||
}
|
||||
|
||||
@@ -254,6 +254,9 @@ class HotStarIE(HotStarBaseIE):
|
||||
'season_id': video_data.get('seasonId'),
|
||||
'episode': title,
|
||||
'episode_number': int_or_none(video_data.get('episodeNo')),
|
||||
'http_headers': {
|
||||
'Referer': 'https://www.hotstar.com/in',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -287,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': {
|
||||
@@ -309,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',
|
||||
@@ -321,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')]
|
||||
|
||||
167
yt_dlp/extractor/ichinanalive.py
Normal file
167
yt_dlp/extractor/ichinanalive.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import ExtractorError, str_or_none, traverse_obj, unified_strdate
|
||||
from ..compat import compat_str
|
||||
|
||||
|
||||
class IchinanaLiveIE(InfoExtractor):
|
||||
IE_NAME = '17live'
|
||||
_VALID_URL = r'https?://(?:www\.)?17\.live/(?:[^/]+/)*(?:live|profile/r)/(?P<id>\d+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://17.live/live/3773096',
|
||||
'info_dict': {
|
||||
'id': '3773096',
|
||||
'title': '萠珈☕🤡🍫moka',
|
||||
'is_live': True,
|
||||
'uploader': '萠珈☕🤡🍫moka',
|
||||
'uploader_id': '3773096',
|
||||
'like_count': 366,
|
||||
'view_count': 18121,
|
||||
'timestamp': 1630569012,
|
||||
},
|
||||
'skip': 'running as of writing, but may be ended as of testing',
|
||||
}, {
|
||||
'note': 'nothing except language differs',
|
||||
'url': 'https://17.live/ja/live/3773096',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
@classmethod
|
||||
def suitable(cls, url):
|
||||
return not IchinanaLiveClipIE.suitable(url) and super(IchinanaLiveIE, cls).suitable(url)
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
url = 'https://17.live/live/%s' % video_id
|
||||
|
||||
enter = self._download_json(
|
||||
'https://api-dsa.17app.co/api/v1/lives/%s/enter' % video_id, video_id,
|
||||
headers={'Referer': url}, fatal=False, expected_status=420,
|
||||
data=b'\0')
|
||||
if enter and enter.get('message') == 'ended':
|
||||
raise ExtractorError('This live has ended.', expected=True)
|
||||
|
||||
view_data = self._download_json(
|
||||
'https://api-dsa.17app.co/api/v1/lives/%s' % video_id, video_id,
|
||||
headers={'Referer': url})
|
||||
|
||||
uploader = traverse_obj(
|
||||
view_data, ('userInfo', 'displayName'), ('userInfo', 'openID'))
|
||||
|
||||
video_urls = view_data.get('rtmpUrls')
|
||||
if not video_urls:
|
||||
raise ExtractorError('unable to extract live URL information')
|
||||
formats = []
|
||||
for (name, value) in video_urls[0].items():
|
||||
if not isinstance(value, compat_str):
|
||||
continue
|
||||
if not value.startswith('http'):
|
||||
continue
|
||||
quality = -1
|
||||
if 'web' in name:
|
||||
quality -= 1
|
||||
if 'High' in name:
|
||||
quality += 4
|
||||
if 'Low' in name:
|
||||
quality -= 2
|
||||
formats.append({
|
||||
'format_id': name,
|
||||
'url': value,
|
||||
'quality': quality,
|
||||
'http_headers': {'Referer': url},
|
||||
'ext': 'flv',
|
||||
'vcodec': 'h264',
|
||||
'acodec': 'aac',
|
||||
})
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': uploader or video_id,
|
||||
'formats': formats,
|
||||
'is_live': True,
|
||||
'uploader': uploader,
|
||||
'uploader_id': video_id,
|
||||
'like_count': view_data.get('receivedLikeCount'),
|
||||
'view_count': view_data.get('viewerCount'),
|
||||
'thumbnail': view_data.get('coverPhoto'),
|
||||
'description': view_data.get('caption'),
|
||||
'timestamp': view_data.get('beginTime'),
|
||||
}
|
||||
|
||||
|
||||
class IchinanaLiveClipIE(InfoExtractor):
|
||||
IE_NAME = '17live:clip'
|
||||
_VALID_URL = r'https?://(?:www\.)?17\.live/(?:[^/]+/)*profile/r/(?P<uploader_id>\d+)/clip/(?P<id>[^/]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://17.live/profile/r/1789280/clip/1bHQSK8KUieruFXaCH4A4upCzlN',
|
||||
'info_dict': {
|
||||
'id': '1bHQSK8KUieruFXaCH4A4upCzlN',
|
||||
'title': 'マチコ先生🦋Class💋',
|
||||
'description': 'マチ戦隊 第一次 バスターコール\n総額200万coin!\n動画制作@うぉーかー🌱Walker🎫',
|
||||
'uploader_id': '1789280',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://17.live/ja/profile/r/1789280/clip/1bHQSK8KUieruFXaCH4A4upCzlN',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
uploader_id, video_id = self._match_valid_url(url).groups()
|
||||
url = 'https://17.live/profile/r/%s/clip/%s' % (uploader_id, video_id)
|
||||
|
||||
view_data = self._download_json(
|
||||
'https://api-dsa.17app.co/api/v1/clips/%s' % video_id, video_id,
|
||||
headers={'Referer': url})
|
||||
|
||||
uploader = traverse_obj(
|
||||
view_data, ('userInfo', 'displayName'), ('userInfo', 'name'))
|
||||
|
||||
formats = []
|
||||
if view_data.get('videoURL'):
|
||||
formats.append({
|
||||
'id': 'video',
|
||||
'url': view_data['videoURL'],
|
||||
'quality': -1,
|
||||
})
|
||||
if view_data.get('transcodeURL'):
|
||||
formats.append({
|
||||
'id': 'transcode',
|
||||
'url': view_data['transcodeURL'],
|
||||
'quality': -1,
|
||||
})
|
||||
if view_data.get('srcVideoURL'):
|
||||
# highest quality
|
||||
formats.append({
|
||||
'id': 'srcVideo',
|
||||
'url': view_data['srcVideoURL'],
|
||||
'quality': 1,
|
||||
})
|
||||
|
||||
for fmt in formats:
|
||||
fmt.update({
|
||||
'ext': 'mp4',
|
||||
'protocol': 'https',
|
||||
'vcodec': 'h264',
|
||||
'acodec': 'aac',
|
||||
'http_headers': {'Referer': url},
|
||||
})
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': uploader or video_id,
|
||||
'formats': formats,
|
||||
'uploader': uploader,
|
||||
'uploader_id': uploader_id,
|
||||
'like_count': view_data.get('likeCount'),
|
||||
'view_count': view_data.get('viewCount'),
|
||||
'thumbnail': view_data.get('imageURL'),
|
||||
'duration': view_data.get('duration'),
|
||||
'description': view_data.get('caption'),
|
||||
'upload_date': unified_strdate(str_or_none(view_data.get('createdAt'))),
|
||||
}
|
||||
@@ -5,10 +5,14 @@ import json
|
||||
|
||||
from .common import InfoExtractor
|
||||
from .brightcove import BrightcoveNewIE
|
||||
|
||||
from ..compat import compat_str
|
||||
from ..utils import (
|
||||
base_url,
|
||||
clean_html,
|
||||
determine_ext,
|
||||
extract_attributes,
|
||||
ExtractorError,
|
||||
get_element_by_class,
|
||||
JSON_LD_RE,
|
||||
merge_dicts,
|
||||
@@ -16,6 +20,8 @@ from ..utils import (
|
||||
smuggle_url,
|
||||
try_get,
|
||||
url_or_none,
|
||||
url_basename,
|
||||
urljoin,
|
||||
)
|
||||
|
||||
|
||||
@@ -23,15 +29,32 @@ class ITVIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?itv\.com/hub/[^/]+/(?P<id>[0-9a-zA-Z]+)'
|
||||
_GEO_COUNTRIES = ['GB']
|
||||
_TESTS = [{
|
||||
'url': 'https://www.itv.com/hub/liar/2a4547a0012',
|
||||
'url': 'https://www.itv.com/hub/plebs/2a1873a0002',
|
||||
'info_dict': {
|
||||
'id': '2a4547a0012',
|
||||
'id': '2a1873a0002',
|
||||
'ext': 'mp4',
|
||||
'title': 'Liar - Series 2 - Episode 6',
|
||||
'description': 'md5:d0f91536569dec79ea184f0a44cca089',
|
||||
'series': 'Liar',
|
||||
'season_number': 2,
|
||||
'episode_number': 6,
|
||||
'title': 'Plebs - The Orgy',
|
||||
'description': 'md5:4d7159af53ebd5b36e8b3ec82a41fdb4',
|
||||
'series': 'Plebs',
|
||||
'season_number': 1,
|
||||
'episode_number': 1,
|
||||
'thumbnail': r're:https?://hubimages\.itv\.com/episode/2_1873_0002'
|
||||
},
|
||||
'params': {
|
||||
# m3u8 download
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.itv.com/hub/the-jonathan-ross-show/2a1166a0209',
|
||||
'info_dict': {
|
||||
'id': '2a1166a0209',
|
||||
'ext': 'mp4',
|
||||
'title': 'The Jonathan Ross Show - Series 17 - Episode 8',
|
||||
'description': 'md5:3023dcdd375db1bc9967186cdb3f1399',
|
||||
'series': 'The Jonathan Ross Show',
|
||||
'episode_number': 8,
|
||||
'season_number': 17,
|
||||
'thumbnail': r're:https?://hubimages\.itv\.com/episode/2_1873_0002'
|
||||
},
|
||||
'params': {
|
||||
# m3u8 download
|
||||
@@ -51,22 +74,16 @@ class ITVIE(InfoExtractor):
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
params = extract_attributes(self._search_regex(
|
||||
r'(?s)(<[^>]+id="video"[^>]*>)', webpage, 'params'))
|
||||
|
||||
ios_playlist_url = params.get('data-video-playlist') or params['data-video-id']
|
||||
hmac = params['data-video-hmac']
|
||||
headers = self.geo_verification_headers()
|
||||
headers.update({
|
||||
def _generate_api_headers(self, hmac):
|
||||
return merge_dicts({
|
||||
'Accept': 'application/vnd.itv.vod.playlist.v2+json',
|
||||
'Content-Type': 'application/json',
|
||||
'hmac': hmac.upper(),
|
||||
})
|
||||
ios_playlist = self._download_json(
|
||||
ios_playlist_url, video_id, data=json.dumps({
|
||||
}, self.geo_verification_headers())
|
||||
|
||||
def _call_api(self, video_id, playlist_url, headers, platform_tag, featureset, fatal=True):
|
||||
return self._download_json(
|
||||
playlist_url, video_id, data=json.dumps({
|
||||
'user': {
|
||||
'itvUserId': '',
|
||||
'entitlements': [],
|
||||
@@ -87,15 +104,61 @@ class ITVIE(InfoExtractor):
|
||||
},
|
||||
'variantAvailability': {
|
||||
'featureset': {
|
||||
'min': ['hls', 'aes', 'outband-webvtt'],
|
||||
'max': ['hls', 'aes', 'outband-webvtt']
|
||||
'min': featureset,
|
||||
'max': featureset
|
||||
},
|
||||
'platformTag': 'dotcom'
|
||||
'platformTag': platform_tag
|
||||
}
|
||||
}).encode(), headers=headers)
|
||||
video_data = ios_playlist['Playlist']['Video']
|
||||
ios_base_url = video_data.get('Base')
|
||||
}).encode(), headers=headers, fatal=fatal)
|
||||
|
||||
def _get_subtitles(self, video_id, variants, ios_playlist_url, headers, *args, **kwargs):
|
||||
subtitles = {}
|
||||
# Prefer last matching featureset
|
||||
# See: https://github.com/yt-dlp/yt-dlp/issues/986
|
||||
platform_tag_subs, featureset_subs = next(
|
||||
((platform_tag, featureset)
|
||||
for platform_tag, featuresets in reversed(variants.items()) for featureset in featuresets
|
||||
if try_get(featureset, lambda x: x[2]) == 'outband-webvtt'),
|
||||
(None, None))
|
||||
|
||||
if platform_tag_subs and featureset_subs:
|
||||
subs_playlist = self._call_api(
|
||||
video_id, ios_playlist_url, headers, platform_tag_subs, featureset_subs, fatal=False)
|
||||
subs = try_get(subs_playlist, lambda x: x['Playlist']['Video']['Subtitles'], list) or []
|
||||
for sub in subs:
|
||||
if not isinstance(sub, dict):
|
||||
continue
|
||||
href = url_or_none(sub.get('Href'))
|
||||
if not href:
|
||||
continue
|
||||
subtitles.setdefault('en', []).append({'url': href})
|
||||
return subtitles
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
params = extract_attributes(self._search_regex(
|
||||
r'(?s)(<[^>]+id="video"[^>]*>)', webpage, 'params'))
|
||||
variants = self._parse_json(
|
||||
try_get(params, lambda x: x['data-video-variants'], compat_str) or '{}',
|
||||
video_id, fatal=False)
|
||||
# Prefer last matching featureset
|
||||
# See: https://github.com/yt-dlp/yt-dlp/issues/986
|
||||
platform_tag_video, featureset_video = next(
|
||||
((platform_tag, featureset)
|
||||
for platform_tag, featuresets in reversed(variants.items()) for featureset in featuresets
|
||||
if try_get(featureset, lambda x: x[:2]) == ['hls', 'aes']),
|
||||
(None, None))
|
||||
if not platform_tag_video or not featureset_video:
|
||||
raise ExtractorError('No downloads available', expected=True, video_id=video_id)
|
||||
|
||||
ios_playlist_url = params.get('data-video-playlist') or params['data-video-id']
|
||||
headers = self._generate_api_headers(params['data-video-hmac'])
|
||||
ios_playlist = self._call_api(
|
||||
video_id, ios_playlist_url, headers, platform_tag_video, featureset_video)
|
||||
|
||||
video_data = try_get(ios_playlist, lambda x: x['Playlist']['Video'], dict) or {}
|
||||
ios_base_url = video_data.get('Base')
|
||||
formats = []
|
||||
for media_file in (video_data.get('MediaFiles') or []):
|
||||
href = media_file.get('Href')
|
||||
@@ -113,20 +176,6 @@ class ITVIE(InfoExtractor):
|
||||
'url': href,
|
||||
})
|
||||
self._sort_formats(formats)
|
||||
|
||||
subtitles = {}
|
||||
subs = video_data.get('Subtitles') or []
|
||||
for sub in subs:
|
||||
if not isinstance(sub, dict):
|
||||
continue
|
||||
href = url_or_none(sub.get('Href'))
|
||||
if not href:
|
||||
continue
|
||||
subtitles.setdefault('en', []).append({
|
||||
'url': href,
|
||||
'ext': determine_ext(href, 'vtt'),
|
||||
})
|
||||
|
||||
info = self._search_json_ld(webpage, video_id, default={})
|
||||
if not info:
|
||||
json_ld = self._parse_json(self._search_regex(
|
||||
@@ -140,13 +189,33 @@ class ITVIE(InfoExtractor):
|
||||
info = self._json_ld(item, video_id, fatal=False) or {}
|
||||
break
|
||||
|
||||
thumbnails = []
|
||||
thumbnail_url = try_get(params, lambda x: x['data-video-posterframe'], compat_str)
|
||||
if thumbnail_url:
|
||||
thumbnails.extend([{
|
||||
'url': thumbnail_url.format(width=1920, height=1080, quality=100, blur=0, bg='false'),
|
||||
'width': 1920,
|
||||
'height': 1080,
|
||||
}, {
|
||||
'url': urljoin(base_url(thumbnail_url), url_basename(thumbnail_url)),
|
||||
'preference': -2
|
||||
}])
|
||||
|
||||
thumbnail_url = self._html_search_meta(['og:image', 'twitter:image'], webpage, default=None)
|
||||
if thumbnail_url:
|
||||
thumbnails.append({
|
||||
'url': thumbnail_url,
|
||||
})
|
||||
self._remove_duplicate_formats(thumbnails)
|
||||
|
||||
return merge_dicts({
|
||||
'id': video_id,
|
||||
'title': self._html_search_meta(['og:title', 'twitter:title'], webpage),
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
'subtitles': self.extract_subtitles(video_id, variants, ios_playlist_url, headers),
|
||||
'duration': parse_duration(video_data.get('Duration')),
|
||||
'description': clean_html(get_element_by_class('episode-info__synopsis', webpage)),
|
||||
'thumbnails': thumbnails
|
||||
}, info)
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
@@ -94,20 +93,21 @@ class IviIE(InfoExtractor):
|
||||
]
|
||||
})
|
||||
|
||||
bundled = hasattr(sys, 'frozen')
|
||||
|
||||
for site in (353, 183):
|
||||
content_data = (data % site).encode()
|
||||
if site == 353:
|
||||
if bundled:
|
||||
continue
|
||||
try:
|
||||
from Cryptodome.Cipher import Blowfish
|
||||
from Cryptodome.Hash import CMAC
|
||||
pycryptodomex_found = True
|
||||
pycryptodome_found = True
|
||||
except ImportError:
|
||||
pycryptodomex_found = False
|
||||
continue
|
||||
try:
|
||||
from Crypto.Cipher import Blowfish
|
||||
from Crypto.Hash import CMAC
|
||||
pycryptodome_found = True
|
||||
except ImportError:
|
||||
pycryptodome_found = False
|
||||
continue
|
||||
|
||||
timestamp = (self._download_json(
|
||||
self._LIGHT_URL, video_id,
|
||||
@@ -140,14 +140,8 @@ class IviIE(InfoExtractor):
|
||||
extractor_msg = 'Video %s does not exist'
|
||||
elif site == 353:
|
||||
continue
|
||||
elif bundled:
|
||||
raise ExtractorError(
|
||||
'This feature does not work from bundled exe. Run yt-dlp from sources.',
|
||||
expected=True)
|
||||
elif not pycryptodomex_found:
|
||||
raise ExtractorError(
|
||||
'pycryptodomex not found. Please install',
|
||||
expected=True)
|
||||
elif not pycryptodome_found:
|
||||
raise ExtractorError('pycryptodomex not found. Please install', expected=True)
|
||||
elif message:
|
||||
extractor_msg += ': ' + message
|
||||
raise ExtractorError(extractor_msg % video_id, expected=True)
|
||||
|
||||
116
yt_dlp/extractor/koo.py
Normal file
116
yt_dlp/extractor/koo.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
clean_html,
|
||||
try_get,
|
||||
)
|
||||
|
||||
|
||||
class KooIE(InfoExtractor):
|
||||
_VALID_URL = r'(?:https?://)(?:www\.)?kooapp\.com/koo/[^/]+/(?P<id>[^/&#$?]+)'
|
||||
_TESTS = [{ # Test for video in the comments
|
||||
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/946c4189-bc2d-4524-b95b-43f641e2adde',
|
||||
'info_dict': {
|
||||
'id': '946c4189-bc2d-4524-b95b-43f641e2adde',
|
||||
'ext': 'mp4',
|
||||
'title': 'test for video in comment',
|
||||
'description': 'md5:daa77dc214add4da8b6ea7d2226776e7',
|
||||
'timestamp': 1632215195,
|
||||
'uploader_id': 'ytdlpTestAccount',
|
||||
'uploader': 'yt-dlpTestAccount',
|
||||
'duration': 7000,
|
||||
'upload_date': '20210921'
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, { # Test for koo with long title
|
||||
'url': 'https://www.kooapp.com/koo/laxman_kumarDBFEC/33decbf7-5e1e-4bb8-bfd7-04744a064361',
|
||||
'info_dict': {
|
||||
'id': '33decbf7-5e1e-4bb8-bfd7-04744a064361',
|
||||
'ext': 'mp4',
|
||||
'title': 'md5:47a71c2337295330c5a19a8af1bbf450',
|
||||
'description': 'md5:06a6a84e9321499486dab541693d8425',
|
||||
'timestamp': 1632106884,
|
||||
'uploader_id': 'laxman_kumarDBFEC',
|
||||
'uploader': 'Laxman Kumar 🇮🇳',
|
||||
'duration': 46000,
|
||||
'upload_date': '20210920'
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, { # Test for audio
|
||||
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/a2a9c88e-ce4b-4d2d-952f-d06361c5b602',
|
||||
'info_dict': {
|
||||
'id': 'a2a9c88e-ce4b-4d2d-952f-d06361c5b602',
|
||||
'ext': 'mp4',
|
||||
'title': 'Test for audio',
|
||||
'description': 'md5:ecb9a2b6a5d34b736cecb53788cb11e8',
|
||||
'timestamp': 1632211634,
|
||||
'uploader_id': 'ytdlpTestAccount',
|
||||
'uploader': 'yt-dlpTestAccount',
|
||||
'duration': 214000,
|
||||
'upload_date': '20210921'
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, { # Test for video
|
||||
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/a3e56c53-c1ed-4ac9-ac02-ed1630e6b1d1',
|
||||
'info_dict': {
|
||||
'id': 'a3e56c53-c1ed-4ac9-ac02-ed1630e6b1d1',
|
||||
'ext': 'mp4',
|
||||
'title': 'Test for video',
|
||||
'description': 'md5:7afc4eb839074ddeb2beea5dd6fe9500',
|
||||
'timestamp': 1632211468,
|
||||
'uploader_id': 'ytdlpTestAccount',
|
||||
'uploader': 'yt-dlpTestAccount',
|
||||
'duration': 14000,
|
||||
'upload_date': '20210921'
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, { # Test for link
|
||||
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/01bf5b94-81a5-4d8e-a387-5f732022e15a',
|
||||
'skip': 'No video/audio found at the provided url.',
|
||||
'info_dict': {
|
||||
'id': '01bf5b94-81a5-4d8e-a387-5f732022e15a',
|
||||
'title': 'Test for link',
|
||||
'ext': 'none',
|
||||
},
|
||||
}, { # Test for images
|
||||
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/dc05d9cd-a61d-45fd-bb07-e8019d8ca8cb',
|
||||
'skip': 'No video/audio found at the provided url.',
|
||||
'info_dict': {
|
||||
'id': 'dc05d9cd-a61d-45fd-bb07-e8019d8ca8cb',
|
||||
'title': 'Test for images',
|
||||
'ext': 'none',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
id = self._match_id(url)
|
||||
data_json = self._download_json(f'https://www.kooapp.com/apiV1/ku/{id}?limit=20&offset=0&showSimilarKoos=true', id)['parentContent']
|
||||
item_json = next(content['items'][0] for content in data_json
|
||||
if try_get(content, lambda x: x['items'][0]['id']) == id)
|
||||
media_json = item_json['mediaMap']
|
||||
formats = []
|
||||
|
||||
mp4_url = media_json.get('videoMp4')
|
||||
video_m3u8_url = media_json.get('videoHls')
|
||||
if mp4_url:
|
||||
formats.append({
|
||||
'url': mp4_url,
|
||||
'ext': 'mp4',
|
||||
})
|
||||
if video_m3u8_url:
|
||||
formats.extend(self._extract_m3u8_formats(video_m3u8_url, id, fatal=False, ext='mp4'))
|
||||
if not formats:
|
||||
self.raise_no_formats('No video/audio found at the provided url.', expected=True)
|
||||
|
||||
self._sort_formats(formats)
|
||||
return {
|
||||
'id': id,
|
||||
'title': clean_html(item_json.get('title')),
|
||||
'description': f'{clean_html(item_json.get("title"))}\n\n{clean_html(item_json.get("enTransliteration"))}',
|
||||
'timestamp': item_json.get('createdAt'),
|
||||
'uploader_id': item_json.get('handle'),
|
||||
'uploader': item_json.get('name'),
|
||||
'duration': media_json.get('duration'),
|
||||
'formats': formats,
|
||||
}
|
||||
@@ -28,14 +28,19 @@ class LBRYBaseIE(InfoExtractor):
|
||||
_SUPPORTED_STREAM_TYPES = ['video', 'audio']
|
||||
|
||||
def _call_api_proxy(self, method, display_id, params, resource):
|
||||
return self._download_json(
|
||||
response = self._download_json(
|
||||
'https://api.lbry.tv/api/v1/proxy',
|
||||
display_id, 'Downloading %s JSON metadata' % resource,
|
||||
headers={'Content-Type': 'application/json-rpc'},
|
||||
data=json.dumps({
|
||||
'method': method,
|
||||
'params': params,
|
||||
}).encode())['result']
|
||||
}).encode())
|
||||
err = response.get('error')
|
||||
if err:
|
||||
raise ExtractorError(
|
||||
f'{self.IE_NAME} said: {err.get("code")} - {err.get("message")}', expected=True)
|
||||
return response['result']
|
||||
|
||||
def _resolve_url(self, url, display_id, resource):
|
||||
return self._call_api_proxy(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
93
yt_dlp/extractor/mediaite.py
Normal file
93
yt_dlp/extractor/mediaite.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from .common import InfoExtractor
|
||||
|
||||
|
||||
class MediaiteIE(InfoExtractor):
|
||||
_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': {
|
||||
'id': 'vPHKITzy',
|
||||
'ext': 'm4a',
|
||||
'title': 'Bill Burr On NFL And Black Lives Matter',
|
||||
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
|
||||
'thumbnail': 'https://cdn.jwplayer.com/v2/media/vPHKITzy/poster.jpg?width=720',
|
||||
'duration': 55,
|
||||
'timestamp': 1631630185,
|
||||
'upload_date': '20210914',
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, {
|
||||
'url': 'https://www.mediaite.com/tv/joe-scarborough-goes-off-on-tax-breaks-for-super-wealthy-largest-income-redistribution-scam-in-american-history/',
|
||||
'info_dict': {
|
||||
'id': 'eeFcK4Xm',
|
||||
'ext': 'mp4',
|
||||
'title': 'Morning Joe-6_16_52 am - 6_21_10 am-2021-09-14.mp4',
|
||||
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
|
||||
'thumbnail': 'https://cdn.jwplayer.com/v2/media/eeFcK4Xm/poster.jpg?width=720',
|
||||
'duration': 258,
|
||||
'timestamp': 1631618057,
|
||||
'upload_date': '20210914',
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, {
|
||||
'url': 'https://www.mediaite.com/politics/watch-rudy-giuliani-impersonates-queen-elizabeth-calls-mark-milley-an-asshle-in-bizarre-9-11-speech/',
|
||||
'info_dict': {
|
||||
'id': 'EiyiXKcr',
|
||||
'ext': 'mp4',
|
||||
'title': 'Giuliani 1',
|
||||
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
|
||||
'thumbnail': 'https://cdn.jwplayer.com/v2/media/EiyiXKcr/poster.jpg?width=720',
|
||||
'duration': 39,
|
||||
'timestamp': 1631536476,
|
||||
'upload_date': '20210913',
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, {
|
||||
'url': 'https://www.mediaite.com/podcasts/clarissa-ward-says-she-decided-to-become-a-journalist-on-9-11/',
|
||||
'info_dict': {
|
||||
'id': 'TxavoRTx',
|
||||
'ext': 'mp4',
|
||||
'title': 'clarissa-ward-3.mp4',
|
||||
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
|
||||
'thumbnail': 'https://cdn.jwplayer.com/v2/media/TxavoRTx/poster.jpg?width=720',
|
||||
'duration': 83,
|
||||
'timestamp': 1631311188,
|
||||
'upload_date': '20210910',
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, {
|
||||
'url': 'https://www.mediaite.com/opinion/mainstream-media-ignores-rose-mcgowans-bombshell-allegation-that-newsoms-wife-tried-to-silence-her-on-weinstein/',
|
||||
'info_dict': {
|
||||
'id': 'sEIWvKR7',
|
||||
'ext': 'mp4',
|
||||
'title': 'KTTV_09-13-2021_05.34.21',
|
||||
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
|
||||
'thumbnail': 'https://cdn.jwplayer.com/v2/media/sEIWvKR7/poster.jpg?width=720',
|
||||
'duration': 52,
|
||||
'timestamp': 1631553328,
|
||||
'upload_date': '20210913',
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, {
|
||||
'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):
|
||||
webpage = self._download_webpage(url, None)
|
||||
id = self._search_regex(r'data-video-id\s?=\s?\"([^\"]+)\"', webpage, 'id')
|
||||
data_json = self._download_json(f'https://cdn.jwplayer.com/v2/media/{id}', id)
|
||||
return self._parse_jwplayer_data(data_json)
|
||||
104
yt_dlp/extractor/mediaklikk.py
Normal file
104
yt_dlp/extractor/mediaklikk.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from ..utils import (
|
||||
unified_strdate
|
||||
)
|
||||
from .common import InfoExtractor
|
||||
from ..compat import (
|
||||
compat_urllib_parse_unquote,
|
||||
compat_str
|
||||
)
|
||||
|
||||
|
||||
class MediaKlikkIE(InfoExtractor):
|
||||
_VALID_URL = r'''(?x)^https?:\/\/(?:www\.)?
|
||||
(?:mediaklikk|m4sport|hirado|petofilive)\.hu\/.*?videok?\/
|
||||
(?:(?P<year>[0-9]{4})/(?P<month>[0-9]{1,2})/(?P<day>[0-9]{1,2})/)?
|
||||
(?P<id>[^/#?_]+)'''
|
||||
|
||||
_TESTS = [{
|
||||
# mediaklikk. date in html.
|
||||
'url': 'https://mediaklikk.hu/video/hazajaro-delnyugat-bacska-a-duna-menten-palankatol-doroszloig/',
|
||||
'info_dict': {
|
||||
'id': '4754129',
|
||||
'title': 'Hazajáró, DÉLNYUGAT-BÁCSKA – A Duna mentén Palánkától Doroszlóig',
|
||||
'ext': 'mp4',
|
||||
'upload_date': '20210901',
|
||||
'thumbnail': 'http://mediaklikk.hu/wp-content/uploads/sites/4/2014/02/hazajarouj_JO.jpg'
|
||||
}
|
||||
}, {
|
||||
# m4sport
|
||||
'url': 'https://m4sport.hu/video/2021/08/30/gyemant-liga-parizs/',
|
||||
'info_dict': {
|
||||
'id': '4754999',
|
||||
'title': 'Gyémánt Liga, Párizs',
|
||||
'ext': 'mp4',
|
||||
'upload_date': '20210830',
|
||||
'thumbnail': 'http://m4sport.hu/wp-content/uploads/sites/4/2021/08/vlcsnap-2021-08-30-18h21m20s10-1024x576.jpg'
|
||||
}
|
||||
}, {
|
||||
# m4sport with *video/ url and no date
|
||||
'url': 'https://m4sport.hu/bl-video/real-madrid-chelsea-1-1/',
|
||||
'info_dict': {
|
||||
'id': '4492099',
|
||||
'title': 'Real Madrid - Chelsea 1-1',
|
||||
'ext': 'mp4',
|
||||
'thumbnail': 'http://m4sport.hu/wp-content/uploads/sites/4/2021/04/Sequence-01.Still001-1024x576.png'
|
||||
}
|
||||
}, {
|
||||
# hirado
|
||||
'url': 'https://hirado.hu/videok/felteteleket-szabott-a-fovaros/',
|
||||
'info_dict': {
|
||||
'id': '4760120',
|
||||
'title': 'Feltételeket szabott a főváros',
|
||||
'ext': 'mp4',
|
||||
'thumbnail': 'http://hirado.hu/wp-content/uploads/sites/4/2021/09/vlcsnap-2021-09-01-20h20m37s165.jpg'
|
||||
}
|
||||
}, {
|
||||
# petofilive
|
||||
'url': 'https://petofilive.hu/video/2021/06/07/tha-shudras-az-akusztikban/',
|
||||
'info_dict': {
|
||||
'id': '4571948',
|
||||
'title': 'Tha Shudras az Akusztikban',
|
||||
'ext': 'mp4',
|
||||
'upload_date': '20210607',
|
||||
'thumbnail': 'http://petofilive.hu/wp-content/uploads/sites/4/2021/06/vlcsnap-2021-06-07-22h14m23s915-1024x576.jpg'
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = self._match_valid_url(url)
|
||||
display_id = mobj.group('id')
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
|
||||
player_data_str = self._html_search_regex(
|
||||
r'mtva_player_manager\.player\(document.getElementById\(.*\),\s?(\{.*\}).*\);', webpage, 'player data')
|
||||
player_data = self._parse_json(player_data_str, display_id, compat_urllib_parse_unquote)
|
||||
video_id = compat_str(player_data['contentId'])
|
||||
title = player_data.get('title') or self._og_search_title(webpage, fatal=False) or \
|
||||
self._html_search_regex(r'<h\d+\b[^>]+\bclass="article_title">([^<]+)<', webpage, 'title')
|
||||
|
||||
upload_date = unified_strdate(
|
||||
'%s-%s-%s' % (mobj.group('year'), mobj.group('month'), mobj.group('day')))
|
||||
if not upload_date:
|
||||
upload_date = unified_strdate(self._html_search_regex(
|
||||
r'<p+\b[^>]+\bclass="article_date">([^<]+)<', webpage, 'upload date', default=None))
|
||||
|
||||
player_data['video'] = player_data.pop('token')
|
||||
player_page = self._download_webpage('https://player.mediaklikk.hu/playernew/player.php', video_id, query=player_data)
|
||||
playlist_url = self._proto_relative_url(compat_urllib_parse_unquote(
|
||||
self._html_search_regex(r'\"file\":\s*\"(\\?/\\?/.*playlist\.m3u8)\"', player_page, 'playlist_url')).replace('\\/', '/'))
|
||||
|
||||
formats = self._extract_wowza_formats(
|
||||
playlist_url, video_id, skip_protocols=['f4m', 'smil', 'dash'])
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'display_id': display_id,
|
||||
'formats': formats,
|
||||
'upload_date': upload_date,
|
||||
'thumbnail': player_data.get('bgImage') or self._og_search_thumbnail(webpage)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class MTVServicesInfoExtractor(InfoExtractor):
|
||||
# Remove the templates, like &device={device}
|
||||
return re.sub(r'&[^=]*?={.*?}(?=(&|$))', '', url)
|
||||
|
||||
def _get_feed_url(self, uri):
|
||||
def _get_feed_url(self, uri, url=None):
|
||||
return self._FEED_URL
|
||||
|
||||
def _get_thumbnail_url(self, uri, itemdoc):
|
||||
@@ -229,9 +229,9 @@ class MTVServicesInfoExtractor(InfoExtractor):
|
||||
data['lang'] = self._LANG
|
||||
return data
|
||||
|
||||
def _get_videos_info(self, uri, use_hls=True):
|
||||
def _get_videos_info(self, uri, use_hls=True, url=None):
|
||||
video_id = self._id_from_uri(uri)
|
||||
feed_url = self._get_feed_url(uri)
|
||||
feed_url = self._get_feed_url(uri, url)
|
||||
info_url = update_url_query(feed_url, self._get_feed_query(uri))
|
||||
return self._get_videos_info_from_url(info_url, video_id, use_hls)
|
||||
|
||||
@@ -323,7 +323,7 @@ class MTVServicesInfoExtractor(InfoExtractor):
|
||||
title = url_basename(url)
|
||||
webpage = self._download_webpage(url, title)
|
||||
mgid = self._extract_mgid(webpage)
|
||||
videos_info = self._get_videos_info(mgid)
|
||||
videos_info = self._get_videos_info(mgid, url=url)
|
||||
return videos_info
|
||||
|
||||
|
||||
@@ -352,7 +352,7 @@ class MTVServicesEmbeddedIE(MTVServicesInfoExtractor):
|
||||
if mobj:
|
||||
return mobj.group('url')
|
||||
|
||||
def _get_feed_url(self, uri):
|
||||
def _get_feed_url(self, uri, url=None):
|
||||
video_id = self._id_from_uri(uri)
|
||||
config = self._download_json(
|
||||
'http://media.mtvnservices.com/pmt/e1/access/index.html?uri=%s&configtype=edge' % uri, video_id)
|
||||
|
||||
67
yt_dlp/extractor/musescore.py
Normal file
67
yt_dlp/extractor/musescore.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
|
||||
|
||||
class MuseScoreIE(InfoExtractor):
|
||||
_VALID_URL = r'(?:https?://)(?:www\.)?musescore\.com/(?:user/\d+|[^/]+)(?:/scores)?/(?P<id>[^#&?]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://musescore.com/user/73797/scores/142975',
|
||||
'info_dict': {
|
||||
'id': '142975',
|
||||
'ext': 'mp3',
|
||||
'title': 'WA Mozart Marche Turque (Turkish March fingered)',
|
||||
'description': 'md5:7ede08230e4eaabd67a4a98bb54d07be',
|
||||
'thumbnail': r're:(?:https?://)(?:www\.)?musescore\.com/.*\.png[^$]+',
|
||||
'uploader': 'PapyPiano',
|
||||
'creator': 'Wolfgang Amadeus Mozart',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://musescore.com/user/36164500/scores/6837638',
|
||||
'info_dict': {
|
||||
'id': '6837638',
|
||||
'ext': 'mp3',
|
||||
'title': 'Sweet Child O\' Mine – Guns N\' Roses sweet child',
|
||||
'description': 'md5:4dca71191c14abc312a0a4192492eace',
|
||||
'thumbnail': r're:(?:https?://)(?:www\.)?musescore\.com/.*\.png[^$]+',
|
||||
'uploader': 'roxbelviolin',
|
||||
'creator': 'Guns N´Roses Arr. Roxbel Violin',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://musescore.com/classicman/fur-elise',
|
||||
'info_dict': {
|
||||
'id': '33816',
|
||||
'ext': 'mp3',
|
||||
'title': 'Für Elise – Beethoven',
|
||||
'description': 'md5:49515a3556d5ecaf9fa4b2514064ac34',
|
||||
'thumbnail': r're:(?:https?://)(?:www\.)?musescore\.com/.*\.png[^$]+',
|
||||
'uploader': 'ClassicMan',
|
||||
'creator': 'Ludwig van Beethoven (1770–1827)',
|
||||
}
|
||||
}, {
|
||||
'url': 'https://musescore.com/minh_cuteee/scores/6555384',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
webpage = self._download_webpage(url, None)
|
||||
url = self._og_search_url(webpage) or url
|
||||
id = self._match_id(url)
|
||||
mp3_url = self._download_json(f'https://musescore.com/api/jmuse?id={id}&index=0&type=mp3&v2=1', id,
|
||||
headers={'authorization': '63794e5461e4cfa046edfbdddfccc1ac16daffd2'})['info']['url']
|
||||
formats = [{
|
||||
'url': mp3_url,
|
||||
'ext': 'mp3',
|
||||
'vcodec': 'none',
|
||||
}]
|
||||
|
||||
return {
|
||||
'id': id,
|
||||
'formats': formats,
|
||||
'title': self._og_search_title(webpage),
|
||||
'description': self._og_search_description(webpage),
|
||||
'thumbnail': self._og_search_thumbnail(webpage),
|
||||
'uploader': self._html_search_meta('musescore:author', webpage, 'uploader'),
|
||||
'creator': self._html_search_meta('musescore:composer', webpage, 'composer'),
|
||||
}
|
||||
@@ -3,43 +3,68 @@ from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_str
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
js_to_json,
|
||||
qualities,
|
||||
try_get,
|
||||
url_or_none,
|
||||
urljoin,
|
||||
)
|
||||
from ..utils import try_get
|
||||
|
||||
|
||||
class MxplayerIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?mxplayer\.in/(?:movie|show/[-\w]+/[-\w]+)/(?P<display_id>[-\w]+)-(?P<id>\w+)'
|
||||
_VALID_URL = r'https?://(?:www\.)?mxplayer\.in/(?P<type>movie|show/[-\w]+/[-\w]+)/(?P<display_id>[-\w]+)-(?P<id>\w+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.mxplayer.in/show/watch-my-girlfriend-is-an-alien-hindi-dubbed/season-1/episode-1-online-9d2013d31d5835bb8400e3b3c5e7bb72',
|
||||
'info_dict': {
|
||||
'id': '9d2013d31d5835bb8400e3b3c5e7bb72',
|
||||
'ext': 'mp4',
|
||||
'title': 'Episode 1',
|
||||
'description': 'md5:62ed43eb9fec5efde5cf3bd1040b7670',
|
||||
'season_number': 1,
|
||||
'episode_number': 1,
|
||||
'duration': 2451,
|
||||
'season': 'Season 1',
|
||||
'series': 'My Girlfriend Is An Alien (Hindi Dubbed)',
|
||||
'thumbnail': 'https://qqcdnpictest.mxplay.com/pic/9d2013d31d5835bb8400e3b3c5e7bb72/en/16x9/320x180/9562f5f8df42cad09c9a9c4e69eb1567_1920x1080.webp',
|
||||
'episode': 'Episode 1'
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.mxplayer.in/movie/watch-knock-knock-hindi-dubbed-movie-online-b9fa28df3bfb8758874735bbd7d2655a?watch=true',
|
||||
'info_dict': {
|
||||
'id': 'b9fa28df3bfb8758874735bbd7d2655a',
|
||||
'ext': 'mp4',
|
||||
'title': 'Knock Knock (Hindi Dubbed)',
|
||||
'description': 'md5:b195ba93ff1987309cfa58e2839d2a5b'
|
||||
'description': 'md5:b195ba93ff1987309cfa58e2839d2a5b',
|
||||
'season_number': 0,
|
||||
'episode_number': 0,
|
||||
'duration': 5970,
|
||||
'season': 'Season 0',
|
||||
'series': None,
|
||||
'thumbnail': 'https://qqcdnpictest.mxplay.com/pic/b9fa28df3bfb8758874735bbd7d2655a/en/16x9/320x180/test_pic1588676032011.webp',
|
||||
'episode': 'Episode 0'
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
'skip_download': True,
|
||||
'format': 'bestvideo'
|
||||
}
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.mxplayer.in/show/watch-shaitaan/season-1/the-infamous-taxi-gang-of-meerut-online-45055d5bcff169ad48f2ad7552a83d6c',
|
||||
'info_dict': {
|
||||
'id': '45055d5bcff169ad48f2ad7552a83d6c',
|
||||
'ext': 'm3u8',
|
||||
'ext': 'mp4',
|
||||
'title': 'The infamous taxi gang of Meerut',
|
||||
'description': 'md5:033a0a7e3fd147be4fb7e07a01a3dc28',
|
||||
'season_number': 1,
|
||||
'episode_number': 1,
|
||||
'duration': 2332,
|
||||
'season': 'Season 1',
|
||||
'series': 'Shaitaan'
|
||||
'series': 'Shaitaan',
|
||||
'thumbnail': 'https://qqcdnpictest.mxplay.com/pic/45055d5bcff169ad48f2ad7552a83d6c/en/16x9/320x180/voot_8e7d5f8d8183340869279c732c1e3a43.webp',
|
||||
'episode': 'Episode 1'
|
||||
},
|
||||
'params': {
|
||||
'format': 'best',
|
||||
'skip_download': True,
|
||||
}
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.mxplayer.in/show/watch-aashram/chapter-1/duh-swapna-online-d445579792b0135598ba1bc9088a84cb',
|
||||
'info_dict': {
|
||||
@@ -47,88 +72,110 @@ class MxplayerIE(InfoExtractor):
|
||||
'ext': 'mp4',
|
||||
'title': 'Duh Swapna',
|
||||
'description': 'md5:35ff39c4bdac403c53be1e16a04192d8',
|
||||
'season_number': 1,
|
||||
'episode_number': 3,
|
||||
'duration': 2568,
|
||||
'season': 'Chapter 1',
|
||||
'series': 'Aashram'
|
||||
'series': 'Aashram',
|
||||
'thumbnail': 'https://qqcdnpictest.mxplay.com/pic/d445579792b0135598ba1bc9088a84cb/en/4x3/1600x1200/test_pic1624819307993.webp',
|
||||
'episode': 'Episode 3'
|
||||
},
|
||||
'expected_warnings': ['Unknown MIME type application/mp4 in DASH manifest'],
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
'skip_download': True,
|
||||
'format': 'bestvideo'
|
||||
}
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.mxplayer.in/show/watch-dangerous/season-1/chapter-1-online-5a351b4f9fb69436f6bd6ae3a1a75292',
|
||||
'info_dict': {
|
||||
'id': '5a351b4f9fb69436f6bd6ae3a1a75292',
|
||||
'ext': 'mp4',
|
||||
'title': 'Chapter 1',
|
||||
'description': 'md5:233886b8598bc91648ac098abe1d288f',
|
||||
'season_number': 1,
|
||||
'episode_number': 1,
|
||||
'duration': 1305,
|
||||
'season': 'Season 1',
|
||||
'series': 'Dangerous',
|
||||
'thumbnail': 'https://qqcdnpictest.mxplay.com/pic/5a351b4f9fb69436f6bd6ae3a1a75292/en/4x3/1600x1200/test_pic1624706302350.webp',
|
||||
'episode': 'Episode 1'
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.mxplayer.in/movie/watch-the-attacks-of-2611-movie-online-0452f0d80226c398d63ce7e3ea40fa2d',
|
||||
'info_dict': {
|
||||
'id': '0452f0d80226c398d63ce7e3ea40fa2d',
|
||||
'ext': 'mp4',
|
||||
'title': 'The Attacks of 26/11',
|
||||
'description': 'md5:689bacd29e97b3f31eaf519eb14127e5',
|
||||
'season_number': 0,
|
||||
'episode_number': 0,
|
||||
'duration': 6085,
|
||||
'season': 'Season 0',
|
||||
'series': None,
|
||||
'thumbnail': 'https://qqcdnpictest.mxplay.com/pic/0452f0d80226c398d63ce7e3ea40fa2d/en/16x9/320x180/00c8955dab5e5d340dbde643f9b1f6fd_1920x1080.webp',
|
||||
'episode': 'Episode 0'
|
||||
},
|
||||
'params': {
|
||||
'format': 'best',
|
||||
'skip_download': True,
|
||||
},
|
||||
}]
|
||||
|
||||
def _get_stream_urls(self, video_dict):
|
||||
stream_provider_dict = try_get(
|
||||
video_dict,
|
||||
lambda x: x['stream'][x['stream']['provider']])
|
||||
if not stream_provider_dict:
|
||||
raise ExtractorError('No stream provider found', expected=True)
|
||||
|
||||
for stream_name, stream in stream_provider_dict.items():
|
||||
if stream_name in ('hls', 'dash', 'hlsUrl', 'dashUrl'):
|
||||
stream_type = stream_name.replace('Url', '')
|
||||
if isinstance(stream, dict):
|
||||
for quality, stream_url in stream.items():
|
||||
if stream_url:
|
||||
yield stream_type, quality, stream_url
|
||||
else:
|
||||
yield stream_type, 'base', stream
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id, video_id = self._match_valid_url(url).groups()
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
source = self._parse_json(
|
||||
js_to_json(self._html_search_regex(
|
||||
r'(?s)<script>window\.state\s*[:=]\s(\{.+\})\n(\w+).*(</script>).*',
|
||||
webpage, 'WindowState')),
|
||||
video_id)
|
||||
if not source:
|
||||
raise ExtractorError('Cannot find source', expected=True)
|
||||
|
||||
config_dict = source['config']
|
||||
video_dict = source['entities'][video_id]
|
||||
type, display_id, video_id = self._match_valid_url(url).groups()
|
||||
type = 'movie_film' if type == 'movie' else 'tvshow_episode'
|
||||
API_URL = 'https://androidapi.mxplay.com/v1/detail/'
|
||||
headers = {
|
||||
'X-Av-Code': '23',
|
||||
'X-Country': 'IN',
|
||||
'X-Platform': 'android',
|
||||
'X-App-Version': '1370001318',
|
||||
'X-Resolution': '3840x2160',
|
||||
}
|
||||
data_json = self._download_json(f'{API_URL}{type}/{video_id}', display_id, headers=headers)['profile']
|
||||
|
||||
season, series = None, None
|
||||
for dct in data_json.get('levelInfos', []):
|
||||
if dct.get('type') == 'tvshow_season':
|
||||
season = dct.get('name')
|
||||
elif dct.get('type') == 'tvshow_show':
|
||||
series = dct.get('name')
|
||||
thumbnails = []
|
||||
for i in video_dict.get('imageInfo') or []:
|
||||
for thumb in data_json.get('poster', []):
|
||||
thumbnails.append({
|
||||
'url': urljoin(config_dict['imageBaseUrl'], i['url']),
|
||||
'width': i['width'],
|
||||
'height': i['height'],
|
||||
'url': thumb.get('url'),
|
||||
'width': thumb.get('width'),
|
||||
'height': thumb.get('height'),
|
||||
})
|
||||
|
||||
formats = []
|
||||
get_quality = qualities(['main', 'base', 'high'])
|
||||
for stream_type, quality, stream_url in self._get_stream_urls(video_dict):
|
||||
format_url = url_or_none(urljoin(config_dict['videoCdnBaseUrl'], stream_url))
|
||||
if not format_url:
|
||||
continue
|
||||
if stream_type == 'dash':
|
||||
dash_formats = self._extract_mpd_formats(
|
||||
format_url, video_id, mpd_id='dash-%s' % quality, headers={'Referer': url})
|
||||
for frmt in dash_formats:
|
||||
frmt['quality'] = get_quality(quality)
|
||||
formats.extend(dash_formats)
|
||||
dash_formats_h265 = self._extract_mpd_formats(
|
||||
format_url.replace('h264_high', 'h265_main'), video_id, mpd_id='dash-%s' % quality, headers={'Referer': url}, fatal=False)
|
||||
for frmt in dash_formats_h265:
|
||||
frmt['quality'] = get_quality(quality)
|
||||
formats.extend(dash_formats_h265)
|
||||
elif stream_type == 'hls':
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
format_url, video_id, fatal=False,
|
||||
m3u8_id='hls-%s' % quality, quality=get_quality(quality), ext='mp4'))
|
||||
|
||||
subtitles = {}
|
||||
for dct in data_json.get('playInfo', []):
|
||||
if dct.get('extension') == 'mpd':
|
||||
frmt, subs = self._extract_mpd_formats_and_subtitles(dct.get('playUrl'), display_id, fatal=False)
|
||||
formats.extend(frmt)
|
||||
subtitles = self._merge_subtitles(subtitles, subs)
|
||||
elif dct.get('extension') == 'm3u8':
|
||||
frmt, subs = self._extract_m3u8_formats_and_subtitles(dct.get('playUrl'), display_id, fatal=False)
|
||||
formats.extend(frmt)
|
||||
subtitles = self._merge_subtitles(subtitles, subs)
|
||||
self._sort_formats(formats)
|
||||
return {
|
||||
'id': video_id,
|
||||
'display_id': display_id,
|
||||
'title': video_dict['title'] or self._og_search_title(webpage),
|
||||
'formats': formats,
|
||||
'description': video_dict.get('description'),
|
||||
'season': try_get(video_dict, lambda x: x['container']['title']),
|
||||
'series': try_get(video_dict, lambda x: x['container']['container']['title']),
|
||||
'title': data_json.get('name') or display_id,
|
||||
'description': data_json.get('description'),
|
||||
'season_number': data_json.get('seasonNum'),
|
||||
'episode_number': data_json.get('episodeNum'),
|
||||
'duration': data_json.get('duration'),
|
||||
'season': season,
|
||||
'series': series,
|
||||
'thumbnails': thumbnails,
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
}
|
||||
|
||||
|
||||
|
||||
136
yt_dlp/extractor/n1.py
Normal file
136
yt_dlp/extractor/n1.py
Normal file
@@ -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,
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
determine_ext,
|
||||
int_or_none,
|
||||
merge_dicts,
|
||||
parse_iso8601,
|
||||
parse_duration,
|
||||
qualities,
|
||||
try_get,
|
||||
unified_strdate,
|
||||
urljoin,
|
||||
)
|
||||
|
||||
@@ -28,110 +27,110 @@ class NDRIE(NDRBaseIE):
|
||||
IE_DESC = 'NDR.de - Norddeutscher Rundfunk'
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:daserste\.)?ndr\.de/(?:[^/]+/)*(?P<display_id>[^/?#]+),(?P<id>[\da-z]+)\.html'
|
||||
_TESTS = [{
|
||||
# httpVideo, same content id
|
||||
'url': 'http://www.ndr.de/fernsehen/Party-Poette-und-Parade,hafengeburtstag988.html',
|
||||
'md5': '6515bc255dc5c5f8c85bbc38e035a659',
|
||||
'info_dict': {
|
||||
'id': 'hafengeburtstag988',
|
||||
'display_id': 'Party-Poette-und-Parade',
|
||||
'ext': 'mp4',
|
||||
'title': 'Party, Pötte und Parade',
|
||||
'thumbnail': 'https://www.ndr.de/fernsehen/hafengeburtstag990_v-contentxl.jpg',
|
||||
'description': 'md5:ad14f9d2f91d3040b6930c697e5f6b4c',
|
||||
'uploader': 'ndrtv',
|
||||
'timestamp': 1431108900,
|
||||
'upload_date': '20150510',
|
||||
'series': None,
|
||||
'channel': 'NDR Fernsehen',
|
||||
'upload_date': '20150508',
|
||||
'duration': 3498,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
# httpVideo, different content id
|
||||
'url': 'http://www.ndr.de/sport/fussball/40-Osnabrueck-spielt-sich-in-einen-Rausch,osna270.html',
|
||||
'md5': '1043ff203eab307f0c51702ec49e9a71',
|
||||
'url': 'https://www.ndr.de/sport/fussball/Rostocks-Matchwinner-Froede-Ein-Hansa-Debuet-wie-im-Maerchen,hansa10312.html',
|
||||
'only_matching': True
|
||||
}, {
|
||||
'url': 'https://www.ndr.de/nachrichten/niedersachsen/kommunalwahl_niedersachsen_2021/Grosse-Parteien-zufrieden-mit-Ergebnissen-der-Kommunalwahl,kommunalwahl1296.html',
|
||||
'info_dict': {
|
||||
'id': 'osna272',
|
||||
'display_id': '40-Osnabrueck-spielt-sich-in-einen-Rausch',
|
||||
'id': 'kommunalwahl1296',
|
||||
'ext': 'mp4',
|
||||
'title': 'Osnabrück - Wehen Wiesbaden: Die Highlights',
|
||||
'description': 'md5:32e9b800b3d2d4008103752682d5dc01',
|
||||
'uploader': 'ndrtv',
|
||||
'timestamp': 1442059200,
|
||||
'upload_date': '20150912',
|
||||
'duration': 510,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
'title': 'Die Spitzenrunde: Die Wahl aus Sicht der Landespolitik',
|
||||
'thumbnail': 'https://www.ndr.de/fernsehen/screenshot1194912_v-contentxl.jpg',
|
||||
'description': 'md5:5c6e2ad744cef499135735a1036d7aa7',
|
||||
'series': 'Hallo Niedersachsen',
|
||||
'channel': 'NDR Fernsehen',
|
||||
'upload_date': '20210913',
|
||||
'duration': 438,
|
||||
},
|
||||
}, {
|
||||
# httpAudio, same content id
|
||||
'url': 'http://www.ndr.de/info/La-Valette-entgeht-der-Hinrichtung,audio51535.html',
|
||||
'md5': 'bb3cd38e24fbcc866d13b50ca59307b8',
|
||||
'info_dict': {
|
||||
'id': 'audio51535',
|
||||
'display_id': 'La-Valette-entgeht-der-Hinrichtung',
|
||||
'ext': 'mp3',
|
||||
'title': 'La Valette entgeht der Hinrichtung',
|
||||
'description': 'md5:22f9541913a40fe50091d5cdd7c9f536',
|
||||
'uploader': 'ndrinfo',
|
||||
'timestamp': 1290626100,
|
||||
'upload_date': '20140729',
|
||||
'duration': 884,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
# with subtitles
|
||||
'url': 'https://www.ndr.de/fernsehen/sendungen/extra_3/extra-3-Satiremagazin-mit-Christian-Ehring,sendung1091858.html',
|
||||
'info_dict': {
|
||||
'id': 'extra18674',
|
||||
'display_id': 'extra-3-Satiremagazin-mit-Christian-Ehring',
|
||||
'id': 'sendung1091858',
|
||||
'ext': 'mp4',
|
||||
'title': 'Extra 3 vom 11.11.2020 mit Christian Ehring',
|
||||
'description': 'md5:42ee53990a715eaaf4dc7f13a3bd56c6',
|
||||
'uploader': 'ndrtv',
|
||||
'upload_date': '20201113',
|
||||
'thumbnail': 'https://www.ndr.de/fernsehen/screenshot983938_v-contentxl.jpg',
|
||||
'description': 'md5:700f6de264010585012a72f97b0ac0c9',
|
||||
'series': 'extra 3',
|
||||
'channel': 'NDR Fernsehen',
|
||||
'upload_date': '20201111',
|
||||
'duration': 1749,
|
||||
'subtitles': {
|
||||
'de': [{
|
||||
'ext': 'ttml',
|
||||
'url': r're:^https://www\.ndr\.de.+',
|
||||
}],
|
||||
},
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
},
|
||||
'expected_warnings': ['Unable to download f4m manifest'],
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.ndr.de/Fettes-Brot-Ferris-MC-und-Thees-Uhlmann-live-on-stage,festivalsommer116.html',
|
||||
'only_matching': True,
|
||||
'url': 'http://www.ndr.de/info/La-Valette-entgeht-der-Hinrichtung,audio51535.html',
|
||||
'info_dict': {
|
||||
'id': 'audio51535',
|
||||
'ext': 'mp3',
|
||||
'title': 'La Valette entgeht der Hinrichtung',
|
||||
'thumbnail': 'https://www.ndr.de/mediathek/mediathekbild140_v-podcast.jpg',
|
||||
'description': 'md5:22f9541913a40fe50091d5cdd7c9f536',
|
||||
'upload_date': '20140729',
|
||||
'duration': 884.0,
|
||||
},
|
||||
'expected_warnings': ['unable to extract json url'],
|
||||
}]
|
||||
|
||||
def _extract_embed(self, webpage, display_id, id):
|
||||
embed_url = self._html_search_meta(
|
||||
'embedURL', webpage, 'embed URL',
|
||||
default=None) or self._search_regex(
|
||||
r'\bembedUrl["\']\s*:\s*(["\'])(?P<url>(?:(?!\1).)+)\1', webpage,
|
||||
'embed URL', fatal=False, group='url')
|
||||
if embed_url is None:
|
||||
return self.url_result('ndr:%s' % id, ie=NDREmbedBaseIE.ie_key())
|
||||
description = self._search_regex(
|
||||
r'<p[^>]+itemprop="description">([^<]+)</p>',
|
||||
webpage, 'description', default=None) or self._og_search_description(webpage)
|
||||
timestamp = parse_iso8601(
|
||||
self._search_regex(
|
||||
r'<span[^>]+itemprop="(?:datePublished|uploadDate)"[^>]+content="([^"]+)"',
|
||||
webpage, 'upload date', default=None))
|
||||
info = self._search_json_ld(webpage, display_id, default={})
|
||||
return merge_dicts({
|
||||
'_type': 'url_transparent',
|
||||
'url': embed_url,
|
||||
'display_id': display_id,
|
||||
'description': description,
|
||||
'timestamp': timestamp,
|
||||
}, info)
|
||||
formats = []
|
||||
base_url = 'https://www.ndr.de'
|
||||
json_url = self._search_regex(r'<iframe[^>]+src=\"([^\"]+)_theme-ndrde[^\.]*\.html\"', webpage,
|
||||
'json url', fatal=False)
|
||||
if json_url:
|
||||
data_json = self._download_json(base_url + json_url.replace('ardplayer_image', 'ardjson_image') + '.json',
|
||||
id, fatal=False)
|
||||
info_json = data_json.get('_info', {})
|
||||
media_json = try_get(data_json, lambda x: x['_mediaArray'][0]['_mediaStreamArray'])
|
||||
for media in media_json:
|
||||
if media.get('_quality') == 'auto':
|
||||
formats.extend(self._extract_m3u8_formats(media['_stream'], id))
|
||||
subtitles = {}
|
||||
sub_url = data_json.get('_subtitleUrl')
|
||||
if sub_url:
|
||||
subtitles.setdefault('de', []).append({
|
||||
'url': base_url + sub_url,
|
||||
})
|
||||
self._sort_formats(formats)
|
||||
return {
|
||||
'id': id,
|
||||
'title': info_json.get('clipTitle'),
|
||||
'thumbnail': base_url + data_json.get('_previewImage'),
|
||||
'description': info_json.get('clipDescription'),
|
||||
'series': info_json.get('seriesTitle') or None,
|
||||
'channel': info_json.get('channelTitle'),
|
||||
'upload_date': unified_strdate(info_json.get('clipDate')),
|
||||
'duration': data_json.get('_duration'),
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
}
|
||||
else:
|
||||
json_url = base_url + self._search_regex(r'apiUrl\s?=\s?\'([^\']+)\'', webpage, 'json url').replace(
|
||||
'_belongsToPodcast-', '')
|
||||
data_json = self._download_json(json_url, id, fatal=False)
|
||||
return {
|
||||
'id': id,
|
||||
'title': data_json.get('title'),
|
||||
'thumbnail': base_url + data_json.get('poster'),
|
||||
'description': data_json.get('summary'),
|
||||
'upload_date': unified_strdate(data_json.get('publicationDate')),
|
||||
'duration': parse_duration(data_json.get('duration')),
|
||||
'formats': [{
|
||||
'url': try_get(data_json, (lambda x: x['audio'][0]['url'], lambda x: x['files'][0]['url'])),
|
||||
'vcodec': 'none',
|
||||
'ext': 'mp3',
|
||||
}],
|
||||
}
|
||||
|
||||
|
||||
class NJoyIE(NDRBaseIE):
|
||||
|
||||
@@ -1,238 +1,238 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
from urllib.error import HTTPError
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_str, compat_urllib_parse_unquote, compat_urllib_parse_quote
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
parse_iso8601,
|
||||
try_get,
|
||||
urljoin,
|
||||
)
|
||||
|
||||
|
||||
class NebulaIE(InfoExtractor):
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:watchnebula\.com|nebula\.app)/videos/(?P<id>[-\w]+)'
|
||||
_TESTS = [
|
||||
{
|
||||
'url': 'https://nebula.app/videos/that-time-disney-remade-beauty-and-the-beast',
|
||||
'md5': 'fe79c4df8b3aa2fea98a93d027465c7e',
|
||||
'info_dict': {
|
||||
'id': '5c271b40b13fd613090034fd',
|
||||
'ext': 'mp4',
|
||||
'title': 'That Time Disney Remade Beauty and the Beast',
|
||||
'description': 'Note: this video was originally posted on YouTube with the sponsor read included. We weren’t able to remove it without reducing video quality, so it’s presented here in its original context.',
|
||||
'upload_date': '20180731',
|
||||
'timestamp': 1533009600,
|
||||
'channel': 'Lindsay Ellis',
|
||||
'uploader': 'Lindsay Ellis',
|
||||
},
|
||||
'params': {
|
||||
'usenetrc': True,
|
||||
},
|
||||
'skip': 'All Nebula content requires authentication',
|
||||
},
|
||||
{
|
||||
'url': 'https://nebula.app/videos/the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore',
|
||||
'md5': '6d4edd14ce65720fa63aba5c583fb328',
|
||||
'info_dict': {
|
||||
'id': '5e7e78171aaf320001fbd6be',
|
||||
'ext': 'mp4',
|
||||
'title': 'Landing Craft - How The Allies Got Ashore',
|
||||
'description': r're:^In this episode we explore the unsung heroes of D-Day, the landing craft.',
|
||||
'upload_date': '20200327',
|
||||
'timestamp': 1585348140,
|
||||
'channel': 'The Logistics of D-Day',
|
||||
'uploader': 'The Logistics of D-Day',
|
||||
},
|
||||
'params': {
|
||||
'usenetrc': True,
|
||||
},
|
||||
'skip': 'All Nebula content requires authentication',
|
||||
},
|
||||
{
|
||||
'url': 'https://nebula.app/videos/money-episode-1-the-draw',
|
||||
'md5': '8c7d272910eea320f6f8e6d3084eecf5',
|
||||
'info_dict': {
|
||||
'id': '5e779ebdd157bc0001d1c75a',
|
||||
'ext': 'mp4',
|
||||
'title': 'Episode 1: The Draw',
|
||||
'description': r'contains:There’s free money on offer… if the players can all work together.',
|
||||
'upload_date': '20200323',
|
||||
'timestamp': 1584980400,
|
||||
'channel': 'Tom Scott Presents: Money',
|
||||
'uploader': 'Tom Scott Presents: Money',
|
||||
},
|
||||
'params': {
|
||||
'usenetrc': True,
|
||||
},
|
||||
'skip': 'All Nebula content requires authentication',
|
||||
},
|
||||
{
|
||||
'url': 'https://watchnebula.com/videos/money-episode-1-the-draw',
|
||||
'only_matching': True,
|
||||
},
|
||||
]
|
||||
_NETRC_MACHINE = 'watchnebula'
|
||||
|
||||
_nebula_token = None
|
||||
|
||||
def _retrieve_nebula_auth(self):
|
||||
"""
|
||||
Log in to Nebula, and returns a Nebula API token
|
||||
"""
|
||||
|
||||
username, password = self._get_login_info()
|
||||
if not (username and password):
|
||||
self.raise_login_required()
|
||||
|
||||
self.report_login()
|
||||
data = json.dumps({'email': username, 'password': password}).encode('utf8')
|
||||
response = self._download_json(
|
||||
'https://api.watchnebula.com/api/v1/auth/login/',
|
||||
data=data, fatal=False, video_id=None,
|
||||
headers={
|
||||
'content-type': 'application/json',
|
||||
# Submitting the 'sessionid' cookie always causes a 403 on auth endpoint
|
||||
'cookie': ''
|
||||
},
|
||||
note='Authenticating to Nebula with supplied credentials',
|
||||
errnote='Authentication failed or rejected')
|
||||
if not response or not response.get('key'):
|
||||
self.raise_login_required()
|
||||
|
||||
# save nebula token as cookie
|
||||
self._set_cookie(
|
||||
'nebula.app', 'nebula-auth',
|
||||
compat_urllib_parse_quote(
|
||||
json.dumps({
|
||||
"apiToken": response["key"],
|
||||
"isLoggingIn": False,
|
||||
"isLoggingOut": False,
|
||||
}, separators=(",", ":"))),
|
||||
expire_time=int(time.time()) + 86400 * 365,
|
||||
)
|
||||
|
||||
return response['key']
|
||||
|
||||
def _retrieve_zype_api_key(self, page_url, display_id):
|
||||
"""
|
||||
Retrieves the Zype API key
|
||||
"""
|
||||
|
||||
# Find the js that has the API key from the webpage and download it
|
||||
webpage = self._download_webpage(page_url, video_id=display_id)
|
||||
main_script_relpath = self._search_regex(
|
||||
r'<script[^>]*src="(?P<script_relpath>[^"]*main.[0-9a-f]*.chunk.js)"[^>]*>', webpage,
|
||||
group='script_relpath', name='script relative path', fatal=True)
|
||||
main_script_abspath = urljoin(page_url, main_script_relpath)
|
||||
main_script = self._download_webpage(main_script_abspath, video_id=display_id,
|
||||
note='Retrieving Zype API key')
|
||||
|
||||
api_key = self._search_regex(
|
||||
r'REACT_APP_ZYPE_API_KEY\s*:\s*"(?P<api_key>[\w-]*)"', main_script,
|
||||
group='api_key', name='API key', fatal=True)
|
||||
|
||||
return api_key
|
||||
|
||||
def _call_zype_api(self, path, params, video_id, api_key, note):
|
||||
"""
|
||||
A helper for making calls to the Zype API.
|
||||
"""
|
||||
query = {'api_key': api_key, 'per_page': 1}
|
||||
query.update(params)
|
||||
return self._download_json('https://api.zype.com' + path, video_id, query=query, note=note)
|
||||
|
||||
def _call_nebula_api(self, path, video_id, access_token, note):
|
||||
"""
|
||||
A helper for making calls to the Nebula API.
|
||||
"""
|
||||
return self._download_json('https://api.watchnebula.com/api/v1' + path, video_id, headers={
|
||||
'Authorization': 'Token {access_token}'.format(access_token=access_token)
|
||||
}, note=note)
|
||||
|
||||
def _fetch_zype_access_token(self, video_id):
|
||||
try:
|
||||
user_object = self._call_nebula_api('/auth/user/', video_id, self._nebula_token, note='Retrieving Zype access token')
|
||||
except ExtractorError as exc:
|
||||
# if 401, attempt credential auth and retry
|
||||
if exc.cause and isinstance(exc.cause, HTTPError) and exc.cause.code == 401:
|
||||
self._nebula_token = self._retrieve_nebula_auth()
|
||||
user_object = self._call_nebula_api('/auth/user/', video_id, self._nebula_token, note='Retrieving Zype access token')
|
||||
else:
|
||||
raise
|
||||
|
||||
access_token = try_get(user_object, lambda x: x['zype_auth_info']['access_token'], compat_str)
|
||||
if not access_token:
|
||||
if try_get(user_object, lambda x: x['is_subscribed'], bool):
|
||||
# TODO: Reimplement the same Zype token polling the Nebula frontend implements
|
||||
# see https://github.com/ytdl-org/youtube-dl/pull/24805#issuecomment-749231532
|
||||
raise ExtractorError(
|
||||
'Unable to extract Zype access token from Nebula API authentication endpoint. '
|
||||
'Open an arbitrary video in a browser with this account to generate a token',
|
||||
expected=True)
|
||||
raise ExtractorError('Unable to extract Zype access token from Nebula API authentication endpoint')
|
||||
return access_token
|
||||
|
||||
def _extract_channel_title(self, video_meta):
|
||||
# TODO: Implement the API calls giving us the channel list,
|
||||
# so that we can do the title lookup and then figure out the channel URL
|
||||
categories = video_meta.get('categories', []) if video_meta else []
|
||||
# the channel name is the value of the first category
|
||||
for category in categories:
|
||||
if category.get('value'):
|
||||
return category['value'][0]
|
||||
|
||||
def _real_initialize(self):
|
||||
# check cookie jar for valid token
|
||||
nebula_cookies = self._get_cookies('https://nebula.app')
|
||||
nebula_cookie = nebula_cookies.get('nebula-auth')
|
||||
if nebula_cookie:
|
||||
self.to_screen('Authenticating to Nebula with token from cookie jar')
|
||||
nebula_cookie_value = compat_urllib_parse_unquote(nebula_cookie.value)
|
||||
self._nebula_token = self._parse_json(nebula_cookie_value, None).get('apiToken')
|
||||
|
||||
# try to authenticate using credentials if no valid token has been found
|
||||
if not self._nebula_token:
|
||||
self._nebula_token = self._retrieve_nebula_auth()
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
api_key = self._retrieve_zype_api_key(url, display_id)
|
||||
|
||||
response = self._call_zype_api('/videos', {'friendly_title': display_id},
|
||||
display_id, api_key, note='Retrieving metadata from Zype')
|
||||
if len(response.get('response') or []) != 1:
|
||||
raise ExtractorError('Unable to find video on Zype API')
|
||||
video_meta = response['response'][0]
|
||||
|
||||
video_id = video_meta['_id']
|
||||
zype_access_token = self._fetch_zype_access_token(display_id)
|
||||
|
||||
channel_title = self._extract_channel_title(video_meta)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'display_id': display_id,
|
||||
'_type': 'url_transparent',
|
||||
'ie_key': 'Zype',
|
||||
'url': 'https://player.zype.com/embed/%s.html?access_token=%s' % (video_id, zype_access_token),
|
||||
'title': video_meta.get('title'),
|
||||
'description': video_meta.get('description'),
|
||||
'timestamp': parse_iso8601(video_meta.get('published_at')),
|
||||
'thumbnails': [{
|
||||
'id': tn.get('name'), # this appears to be null
|
||||
'url': tn['url'],
|
||||
'width': tn.get('width'),
|
||||
'height': tn.get('height'),
|
||||
} for tn in video_meta.get('thumbnails', [])],
|
||||
'duration': video_meta.get('duration'),
|
||||
'channel': channel_title,
|
||||
'uploader': channel_title, # we chose uploader = channel name
|
||||
# TODO: uploader_url, channel_id, channel_url
|
||||
}
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
from urllib.error import HTTPError
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_str, compat_urllib_parse_unquote, compat_urllib_parse_quote
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
parse_iso8601,
|
||||
try_get,
|
||||
urljoin,
|
||||
)
|
||||
|
||||
|
||||
class NebulaIE(InfoExtractor):
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:watchnebula\.com|nebula\.app)/videos/(?P<id>[-\w]+)'
|
||||
_TESTS = [
|
||||
{
|
||||
'url': 'https://nebula.app/videos/that-time-disney-remade-beauty-and-the-beast',
|
||||
'md5': 'fe79c4df8b3aa2fea98a93d027465c7e',
|
||||
'info_dict': {
|
||||
'id': '5c271b40b13fd613090034fd',
|
||||
'ext': 'mp4',
|
||||
'title': 'That Time Disney Remade Beauty and the Beast',
|
||||
'description': 'Note: this video was originally posted on YouTube with the sponsor read included. We weren’t able to remove it without reducing video quality, so it’s presented here in its original context.',
|
||||
'upload_date': '20180731',
|
||||
'timestamp': 1533009600,
|
||||
'channel': 'Lindsay Ellis',
|
||||
'uploader': 'Lindsay Ellis',
|
||||
},
|
||||
'params': {
|
||||
'usenetrc': True,
|
||||
},
|
||||
'skip': 'All Nebula content requires authentication',
|
||||
},
|
||||
{
|
||||
'url': 'https://nebula.app/videos/the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore',
|
||||
'md5': '6d4edd14ce65720fa63aba5c583fb328',
|
||||
'info_dict': {
|
||||
'id': '5e7e78171aaf320001fbd6be',
|
||||
'ext': 'mp4',
|
||||
'title': 'Landing Craft - How The Allies Got Ashore',
|
||||
'description': r're:^In this episode we explore the unsung heroes of D-Day, the landing craft.',
|
||||
'upload_date': '20200327',
|
||||
'timestamp': 1585348140,
|
||||
'channel': 'The Logistics of D-Day',
|
||||
'uploader': 'The Logistics of D-Day',
|
||||
},
|
||||
'params': {
|
||||
'usenetrc': True,
|
||||
},
|
||||
'skip': 'All Nebula content requires authentication',
|
||||
},
|
||||
{
|
||||
'url': 'https://nebula.app/videos/money-episode-1-the-draw',
|
||||
'md5': '8c7d272910eea320f6f8e6d3084eecf5',
|
||||
'info_dict': {
|
||||
'id': '5e779ebdd157bc0001d1c75a',
|
||||
'ext': 'mp4',
|
||||
'title': 'Episode 1: The Draw',
|
||||
'description': r'contains:There’s free money on offer… if the players can all work together.',
|
||||
'upload_date': '20200323',
|
||||
'timestamp': 1584980400,
|
||||
'channel': 'Tom Scott Presents: Money',
|
||||
'uploader': 'Tom Scott Presents: Money',
|
||||
},
|
||||
'params': {
|
||||
'usenetrc': True,
|
||||
},
|
||||
'skip': 'All Nebula content requires authentication',
|
||||
},
|
||||
{
|
||||
'url': 'https://watchnebula.com/videos/money-episode-1-the-draw',
|
||||
'only_matching': True,
|
||||
},
|
||||
]
|
||||
_NETRC_MACHINE = 'watchnebula'
|
||||
|
||||
_nebula_token = None
|
||||
|
||||
def _retrieve_nebula_auth(self):
|
||||
"""
|
||||
Log in to Nebula, and returns a Nebula API token
|
||||
"""
|
||||
|
||||
username, password = self._get_login_info()
|
||||
if not (username and password):
|
||||
self.raise_login_required()
|
||||
|
||||
self.report_login()
|
||||
data = json.dumps({'email': username, 'password': password}).encode('utf8')
|
||||
response = self._download_json(
|
||||
'https://api.watchnebula.com/api/v1/auth/login/',
|
||||
data=data, fatal=False, video_id=None,
|
||||
headers={
|
||||
'content-type': 'application/json',
|
||||
# Submitting the 'sessionid' cookie always causes a 403 on auth endpoint
|
||||
'cookie': ''
|
||||
},
|
||||
note='Authenticating to Nebula with supplied credentials',
|
||||
errnote='Authentication failed or rejected')
|
||||
if not response or not response.get('key'):
|
||||
self.raise_login_required()
|
||||
|
||||
# save nebula token as cookie
|
||||
self._set_cookie(
|
||||
'nebula.app', 'nebula-auth',
|
||||
compat_urllib_parse_quote(
|
||||
json.dumps({
|
||||
"apiToken": response["key"],
|
||||
"isLoggingIn": False,
|
||||
"isLoggingOut": False,
|
||||
}, separators=(",", ":"))),
|
||||
expire_time=int(time.time()) + 86400 * 365,
|
||||
)
|
||||
|
||||
return response['key']
|
||||
|
||||
def _retrieve_zype_api_key(self, page_url, display_id):
|
||||
"""
|
||||
Retrieves the Zype API key
|
||||
"""
|
||||
|
||||
# Find the js that has the API key from the webpage and download it
|
||||
webpage = self._download_webpage(page_url, video_id=display_id)
|
||||
main_script_relpath = self._search_regex(
|
||||
r'<script[^>]*src="(?P<script_relpath>[^"]*main.[0-9a-f]*.chunk.js)"[^>]*>', webpage,
|
||||
group='script_relpath', name='script relative path', fatal=True)
|
||||
main_script_abspath = urljoin(page_url, main_script_relpath)
|
||||
main_script = self._download_webpage(main_script_abspath, video_id=display_id,
|
||||
note='Retrieving Zype API key')
|
||||
|
||||
api_key = self._search_regex(
|
||||
r'REACT_APP_ZYPE_API_KEY\s*:\s*"(?P<api_key>[\w-]*)"', main_script,
|
||||
group='api_key', name='API key', fatal=True)
|
||||
|
||||
return api_key
|
||||
|
||||
def _call_zype_api(self, path, params, video_id, api_key, note):
|
||||
"""
|
||||
A helper for making calls to the Zype API.
|
||||
"""
|
||||
query = {'api_key': api_key, 'per_page': 1}
|
||||
query.update(params)
|
||||
return self._download_json('https://api.zype.com' + path, video_id, query=query, note=note)
|
||||
|
||||
def _call_nebula_api(self, path, video_id, access_token, note):
|
||||
"""
|
||||
A helper for making calls to the Nebula API.
|
||||
"""
|
||||
return self._download_json('https://api.watchnebula.com/api/v1' + path, video_id, headers={
|
||||
'Authorization': 'Token {access_token}'.format(access_token=access_token)
|
||||
}, note=note)
|
||||
|
||||
def _fetch_zype_access_token(self, video_id):
|
||||
try:
|
||||
user_object = self._call_nebula_api('/auth/user/', video_id, self._nebula_token, note='Retrieving Zype access token')
|
||||
except ExtractorError as exc:
|
||||
# if 401, attempt credential auth and retry
|
||||
if exc.cause and isinstance(exc.cause, HTTPError) and exc.cause.code == 401:
|
||||
self._nebula_token = self._retrieve_nebula_auth()
|
||||
user_object = self._call_nebula_api('/auth/user/', video_id, self._nebula_token, note='Retrieving Zype access token')
|
||||
else:
|
||||
raise
|
||||
|
||||
access_token = try_get(user_object, lambda x: x['zype_auth_info']['access_token'], compat_str)
|
||||
if not access_token:
|
||||
if try_get(user_object, lambda x: x['is_subscribed'], bool):
|
||||
# TODO: Reimplement the same Zype token polling the Nebula frontend implements
|
||||
# see https://github.com/ytdl-org/youtube-dl/pull/24805#issuecomment-749231532
|
||||
raise ExtractorError(
|
||||
'Unable to extract Zype access token from Nebula API authentication endpoint. '
|
||||
'Open an arbitrary video in a browser with this account to generate a token',
|
||||
expected=True)
|
||||
raise ExtractorError('Unable to extract Zype access token from Nebula API authentication endpoint')
|
||||
return access_token
|
||||
|
||||
def _extract_channel_title(self, video_meta):
|
||||
# TODO: Implement the API calls giving us the channel list,
|
||||
# so that we can do the title lookup and then figure out the channel URL
|
||||
categories = video_meta.get('categories', []) if video_meta else []
|
||||
# the channel name is the value of the first category
|
||||
for category in categories:
|
||||
if category.get('value'):
|
||||
return category['value'][0]
|
||||
|
||||
def _real_initialize(self):
|
||||
# check cookie jar for valid token
|
||||
nebula_cookies = self._get_cookies('https://nebula.app')
|
||||
nebula_cookie = nebula_cookies.get('nebula-auth')
|
||||
if nebula_cookie:
|
||||
self.to_screen('Authenticating to Nebula with token from cookie jar')
|
||||
nebula_cookie_value = compat_urllib_parse_unquote(nebula_cookie.value)
|
||||
self._nebula_token = self._parse_json(nebula_cookie_value, None).get('apiToken')
|
||||
|
||||
# try to authenticate using credentials if no valid token has been found
|
||||
if not self._nebula_token:
|
||||
self._nebula_token = self._retrieve_nebula_auth()
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
api_key = self._retrieve_zype_api_key(url, display_id)
|
||||
|
||||
response = self._call_zype_api('/videos', {'friendly_title': display_id},
|
||||
display_id, api_key, note='Retrieving metadata from Zype')
|
||||
if len(response.get('response') or []) != 1:
|
||||
raise ExtractorError('Unable to find video on Zype API')
|
||||
video_meta = response['response'][0]
|
||||
|
||||
video_id = video_meta['_id']
|
||||
zype_access_token = self._fetch_zype_access_token(display_id)
|
||||
|
||||
channel_title = self._extract_channel_title(video_meta)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'display_id': display_id,
|
||||
'_type': 'url_transparent',
|
||||
'ie_key': 'Zype',
|
||||
'url': 'https://player.zype.com/embed/%s.html?access_token=%s' % (video_id, zype_access_token),
|
||||
'title': video_meta.get('title'),
|
||||
'description': video_meta.get('description'),
|
||||
'timestamp': parse_iso8601(video_meta.get('published_at')),
|
||||
'thumbnails': [{
|
||||
'id': tn.get('name'), # this appears to be null
|
||||
'url': tn['url'],
|
||||
'width': tn.get('width'),
|
||||
'height': tn.get('height'),
|
||||
} for tn in video_meta.get('thumbnails', [])],
|
||||
'duration': video_meta.get('duration'),
|
||||
'channel': channel_title,
|
||||
'uploader': channel_title, # we chose uploader = channel name
|
||||
# TODO: uploader_url, channel_id, channel_url
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import functools
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
@@ -8,8 +10,9 @@ from ..utils import (
|
||||
int_or_none,
|
||||
parse_count,
|
||||
parse_duration,
|
||||
parse_filesize,
|
||||
unified_timestamp,
|
||||
OnDemandPagedList,
|
||||
try_get,
|
||||
)
|
||||
|
||||
|
||||
@@ -39,6 +42,7 @@ class NewgroundsIE(InfoExtractor):
|
||||
'timestamp': 955064100,
|
||||
'upload_date': '20000406',
|
||||
'description': 'Scrotum plays "catch."',
|
||||
'age_limit': 17,
|
||||
},
|
||||
}, {
|
||||
# source format unavailable, additional mp4 formats
|
||||
@@ -51,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,
|
||||
@@ -66,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',
|
||||
@@ -78,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)
|
||||
@@ -88,10 +101,10 @@ class NewgroundsIE(InfoExtractor):
|
||||
webpage = self._download_webpage(url, media_id)
|
||||
|
||||
title = self._html_search_regex(
|
||||
r'<title>([^>]+)</title>', webpage, 'title')
|
||||
r'<title>(.+?)</title>', webpage, 'title')
|
||||
|
||||
media_url_string = self._search_regex(
|
||||
r'"url"\s*:\s*("[^"]+"),', webpage, 'media url', default=None, fatal=False)
|
||||
r'"url"\s*:\s*("[^"]+"),', webpage, 'media url', default=None)
|
||||
|
||||
if media_url_string:
|
||||
media_url = self._parse_json(media_url_string, media_id)
|
||||
@@ -124,24 +137,34 @@ 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._search_regex(
|
||||
r'(?s)<dd>\s*Song\s*</dd>\s*<dd>.+?</dd>\s*<dd>([^<]+)', webpage,
|
||||
duration = parse_duration(self._html_search_regex(
|
||||
r'"duration"\s*:\s*["\']?(\d+)["\']?', webpage,
|
||||
'duration', default=None))
|
||||
|
||||
view_count = parse_count(self._html_search_regex(r'(?s)<dt>\s*Views\s*</dt>\s*<dd>([\d\.,]+)</dd>', webpage,
|
||||
'view_count', fatal=False, default=None))
|
||||
view_count = parse_count(self._html_search_regex(
|
||||
r'(?s)<dt>\s*(?:Views|Listens)\s*</dt>\s*<dd>([\d\.,]+)</dd>', webpage,
|
||||
'view count', default=None))
|
||||
|
||||
filesize_approx = parse_filesize(self._html_search_regex(
|
||||
r'(?s)<dd>\s*Song\s*</dd>\s*<dd>(.+?)</dd>', webpage, 'filesize',
|
||||
filesize = int_or_none(self._html_search_regex(
|
||||
r'"filesize"\s*:\s*["\']?([\d]+)["\']?,', webpage, 'filesize',
|
||||
default=None))
|
||||
if len(formats) == 1:
|
||||
formats[0]['filesize_approx'] = filesize_approx
|
||||
|
||||
if '<dd>Song' in webpage:
|
||||
video_type_description = self._html_search_regex(
|
||||
r'"description"\s*:\s*["\']?([^"\']+)["\']?,', webpage, 'filesize',
|
||||
default=None)
|
||||
|
||||
if len(formats) == 1:
|
||||
formats[0]['filesize'] = filesize
|
||||
|
||||
if video_type_description == 'Audio File':
|
||||
formats[0]['vcodec'] = 'none'
|
||||
self._check_formats(formats, media_id)
|
||||
self._sort_formats(formats)
|
||||
@@ -155,11 +178,13 @@ 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,
|
||||
}
|
||||
|
||||
|
||||
class NewgroundsPlaylistIE(InfoExtractor):
|
||||
IE_NAME = 'Newgrounds:playlist'
|
||||
_VALID_URL = r'https?://(?:www\.)?newgrounds\.com/(?:collection|[^/]+/search/[^/]+)/(?P<id>[^/?#&]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.newgrounds.com/collection/cats',
|
||||
@@ -202,7 +227,57 @@ class NewgroundsPlaylistIE(InfoExtractor):
|
||||
continue
|
||||
entries.append(
|
||||
self.url_result(
|
||||
'https://www.newgrounds.com/%s' % path,
|
||||
f'https://www.newgrounds.com/{path}',
|
||||
ie=NewgroundsIE.ie_key(), video_id=media_id))
|
||||
|
||||
return self.playlist_result(entries, playlist_id, title)
|
||||
|
||||
|
||||
class NewgroundsUserIE(InfoExtractor):
|
||||
IE_NAME = 'Newgrounds:user'
|
||||
_VALID_URL = r'https?://(?P<id>[^\.]+)\.newgrounds\.com/(?:movies|audio)/?(?:[#?]|$)'
|
||||
_TESTS = [{
|
||||
'url': 'https://burn7.newgrounds.com/audio',
|
||||
'info_dict': {
|
||||
'id': 'burn7',
|
||||
},
|
||||
'playlist_mincount': 150,
|
||||
}, {
|
||||
'url': 'https://burn7.newgrounds.com/movies',
|
||||
'info_dict': {
|
||||
'id': 'burn7',
|
||||
},
|
||||
'playlist_mincount': 2,
|
||||
}, {
|
||||
'url': 'https://brian-beaton.newgrounds.com/movies',
|
||||
'info_dict': {
|
||||
'id': 'brian-beaton',
|
||||
},
|
||||
'playlist_mincount': 10,
|
||||
}]
|
||||
_PAGE_SIZE = 30
|
||||
|
||||
def _fetch_page(self, channel_id, url, page):
|
||||
page += 1
|
||||
posts_info = self._download_json(
|
||||
f'{url}/page/{page}', channel_id,
|
||||
note=f'Downloading page {page}', headers={
|
||||
'Accept': 'application/json, text/javascript, */*; q = 0.01',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
})
|
||||
sequence = posts_info.get('sequence', [])
|
||||
for year in sequence:
|
||||
posts = try_get(posts_info, lambda x: x['years'][str(year)]['items'])
|
||||
for post in posts:
|
||||
path, media_id = self._search_regex(
|
||||
r'<a[^>]+\bhref=["\'][^"\']+((?:portal/view|audio/listen)/(\d+))[^>]+>',
|
||||
post, 'url', group=(1, 2))
|
||||
yield self.url_result(f'https://www.newgrounds.com/{path}', NewgroundsIE.ie_key(), media_id)
|
||||
|
||||
def _real_extract(self, url):
|
||||
channel_id = self._match_id(url)
|
||||
|
||||
entries = OnDemandPagedList(functools.partial(
|
||||
self._fetch_page, channel_id, url), self._PAGE_SIZE)
|
||||
|
||||
return self.playlist_result(entries, channel_id)
|
||||
|
||||
@@ -8,6 +8,10 @@ from ..utils import (
|
||||
int_or_none,
|
||||
float_or_none,
|
||||
smuggle_url,
|
||||
str_or_none,
|
||||
try_get,
|
||||
unified_strdate,
|
||||
unified_timestamp,
|
||||
)
|
||||
|
||||
|
||||
@@ -37,6 +41,24 @@ class NineNowIE(InfoExtractor):
|
||||
# DRM protected
|
||||
'url': 'https://www.9now.com.au/andrew-marrs-history-of-the-world/season-1/episode-1',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
# episode of series
|
||||
'url': 'https://www.9now.com.au/lego-masters/season-3/episode-3',
|
||||
'info_dict': {
|
||||
'id': '6249614030001',
|
||||
'title': 'Episode 3',
|
||||
'ext': 'mp4',
|
||||
'season_number': 3,
|
||||
'episode_number': 3,
|
||||
'description': 'In the first elimination of the competition, teams will have 10 hours to build a world inside a snow globe.',
|
||||
'uploader_id': '4460760524001',
|
||||
'timestamp': 1619002200,
|
||||
'upload_date': '20210421',
|
||||
},
|
||||
'expected_warnings': ['Ignoring subtitle tracks'],
|
||||
'params':{
|
||||
'skip_download': True,
|
||||
}
|
||||
}]
|
||||
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/4460760524001/default_default/index.html?videoId=%s'
|
||||
|
||||
@@ -59,26 +81,31 @@ class NineNowIE(InfoExtractor):
|
||||
cache = page_data.get(kind, {}).get('%sCache' % kind, {})
|
||||
if not cache:
|
||||
continue
|
||||
common_data = (cache.get(current_key) or list(cache.values())[0])[kind]
|
||||
common_data = {
|
||||
'episode': (cache.get(current_key) or list(cache.values())[0])[kind],
|
||||
'season': (cache.get(current_key) or list(cache.values())[0]).get('season', None)
|
||||
}
|
||||
break
|
||||
else:
|
||||
raise ExtractorError('Unable to find video data')
|
||||
|
||||
video_data = common_data['video']
|
||||
|
||||
brightcove_id = video_data.get('brightcoveId') or 'ref:' + video_data['referenceId']
|
||||
video_id = compat_str(video_data.get('id') or brightcove_id)
|
||||
|
||||
if not self.get_param('allow_unplayable_formats') and video_data.get('drm'):
|
||||
self.report_drm(video_id)
|
||||
|
||||
title = common_data['name']
|
||||
if not self.get_param('allow_unplayable_formats') and try_get(common_data, lambda x: x['episode']['video']['drm'], bool):
|
||||
self.report_drm(display_id)
|
||||
brightcove_id = try_get(
|
||||
common_data, lambda x: x['episode']['video']['brightcoveId'], compat_str) or 'ref:%s' % common_data['episode']['video']['referenceId']
|
||||
video_id = str_or_none(try_get(common_data, lambda x: x['episode']['video']['id'])) or brightcove_id
|
||||
|
||||
title = try_get(common_data, lambda x: x['episode']['name'], compat_str)
|
||||
season_number = try_get(common_data, lambda x: x['season']['seasonNumber'], int)
|
||||
episode_number = try_get(common_data, lambda x: x['episode']['episodeNumber'], int)
|
||||
timestamp = unified_timestamp(try_get(common_data, lambda x: x['episode']['airDate'], compat_str))
|
||||
release_date = unified_strdate(try_get(common_data, lambda x: x['episode']['availability'], compat_str))
|
||||
thumbnails_data = try_get(common_data, lambda x: x['episode']['image']['sizes'], dict) or {}
|
||||
thumbnails = [{
|
||||
'id': thumbnail_id,
|
||||
'url': thumbnail_url,
|
||||
'width': int_or_none(thumbnail_id[1:])
|
||||
} for thumbnail_id, thumbnail_url in common_data.get('image', {}).get('sizes', {}).items()]
|
||||
'width': int_or_none(thumbnail_id[1:]),
|
||||
} for thumbnail_id, thumbnail_url in thumbnails_data.items()]
|
||||
|
||||
return {
|
||||
'_type': 'url_transparent',
|
||||
@@ -87,8 +114,12 @@ class NineNowIE(InfoExtractor):
|
||||
{'geo_countries': self._GEO_COUNTRIES}),
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'description': common_data.get('description'),
|
||||
'duration': float_or_none(video_data.get('duration'), 1000),
|
||||
'description': try_get(common_data, lambda x: x['episode']['description'], compat_str),
|
||||
'duration': float_or_none(try_get(common_data, lambda x: x['episode']['video']['duration'], float), 1000),
|
||||
'thumbnails': thumbnails,
|
||||
'ie_key': 'BrightcoveNew',
|
||||
'season_number': season_number,
|
||||
'episode_number': episode_number,
|
||||
'timestamp': timestamp,
|
||||
'release_date': release_date,
|
||||
}
|
||||
|
||||
63
yt_dlp/extractor/novaplay.py
Normal file
63
yt_dlp/extractor/novaplay.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# coding: utf-8
|
||||
from .common import InfoExtractor
|
||||
from ..utils import int_or_none, parse_duration, parse_iso8601
|
||||
|
||||
|
||||
class NovaPlayIE(InfoExtractor):
|
||||
_VALID_URL = r'https://play.nova\.bg/video/.*/(?P<id>\d+)'
|
||||
_TESTS = [
|
||||
{
|
||||
'url': 'https://play.nova.bg/video/bratya/season-3/bratq-2021-10-08/548677',
|
||||
'md5': 'b1127a84e61bed1632b7c2ca9cbb4153',
|
||||
'info_dict': {
|
||||
'id': '548677',
|
||||
'ext': 'mp4',
|
||||
'title': 'Братя',
|
||||
'alt_title': 'bratya/season-3/bratq-2021-10-08',
|
||||
'duration': 1603.0,
|
||||
'timestamp': 1633724150,
|
||||
'upload_date': '20211008',
|
||||
'thumbnail': 'https://nbg-img.fite.tv/img/548677_460x260.jpg',
|
||||
'description': 'Сезон 3 Епизод 25'
|
||||
},
|
||||
},
|
||||
{
|
||||
'url': 'https://play.nova.bg/video/igri-na-volqta/season-3/igri-na-volqta-2021-09-20-1/548227',
|
||||
'md5': '5fd61b8ecbe582fc021019d570965d58',
|
||||
'info_dict': {
|
||||
'id': '548227',
|
||||
'ext': 'mp4',
|
||||
'title': 'Игри на волята: България (20.09.2021) - част 1',
|
||||
'alt_title': 'gri-na-volqta/season-3/igri-na-volqta-2021-09-20-1',
|
||||
'duration': 4060.0,
|
||||
'timestamp': 1632167564,
|
||||
'upload_date': '20210920',
|
||||
'thumbnail': 'https://nbg-img.fite.tv/img/548227_460x260.jpg',
|
||||
'description': 'Сезон 3 Епизод 13'
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
video_props = self._parse_json(self._search_regex(
|
||||
r'<script\s?id=\"__NEXT_DATA__\"\s?type=\"application/json\">({.+})</script>',
|
||||
webpage, 'video_props'), video_id)['props']['pageProps']['video']
|
||||
m3u8_url = self._download_json(
|
||||
f'https://nbg-api.fite.tv/api/v2/videos/{video_id}/streams',
|
||||
video_id, headers={'x-flipps-user-agent': 'Flipps/75/9.7'})[0]['url']
|
||||
formats = self._extract_m3u8_formats(m3u8_url, video_id, 'mp4', m3u8_id='hls')
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': video_props['title'],
|
||||
'alt_title': video_props.get('slug'),
|
||||
'thumbnail': self._og_search_thumbnail(webpage),
|
||||
'description': self._og_search_description(webpage),
|
||||
'formats': formats,
|
||||
'duration': parse_duration(video_props['duration']),
|
||||
'timestamp': parse_iso8601(video_props['published_at']),
|
||||
'view_count': int_or_none(video_props['view_count']),
|
||||
}
|
||||
@@ -1,71 +1,73 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
parse_duration,
|
||||
int_or_none,
|
||||
try_get,
|
||||
)
|
||||
|
||||
|
||||
class NuvidIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www|m)\.nuvid\.com/video/(?P<id>[0-9]+)'
|
||||
_TEST = {
|
||||
'url': 'http://m.nuvid.com/video/1310741/',
|
||||
'md5': 'eab207b7ac4fccfb4e23c86201f11277',
|
||||
_TESTS = [{
|
||||
'url': 'https://www.nuvid.com/video/6513023/italian-babe',
|
||||
'md5': '772d2f8288f3d3c5c45f7a41761c7844',
|
||||
'info_dict': {
|
||||
'id': '1310741',
|
||||
'id': '6513023',
|
||||
'ext': 'mp4',
|
||||
'title': 'Horny babes show their awesome bodeis and',
|
||||
'duration': 129,
|
||||
'title': 'italian babe',
|
||||
'duration': 321.0,
|
||||
'age_limit': 18,
|
||||
}
|
||||
}
|
||||
}, {
|
||||
'url': 'https://m.nuvid.com/video/6523263',
|
||||
'info_dict': {
|
||||
'id': '6523263',
|
||||
'ext': 'mp4',
|
||||
'age_limit': 18,
|
||||
'title': 'Slut brunette college student anal dorm',
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
|
||||
page_url = 'http://m.nuvid.com/video/%s' % video_id
|
||||
webpage = self._download_webpage(
|
||||
page_url, video_id, 'Downloading video page')
|
||||
# When dwnld_speed exists and has a value larger than the MP4 file's
|
||||
# bitrate, Nuvid returns the MP4 URL
|
||||
# It's unit is 100bytes/millisecond, see mobile-nuvid-min.js for the algorithm
|
||||
self._set_cookie('nuvid.com', 'dwnld_speed', '10.0')
|
||||
mp4_webpage = self._download_webpage(
|
||||
page_url, video_id, 'Downloading video page for MP4 format')
|
||||
qualities = {
|
||||
'lq': '360p',
|
||||
'hq': '720p',
|
||||
}
|
||||
|
||||
html5_video_re = r'(?s)<(?:video|audio)[^<]*(?:>.*?<source[^>]*)?\s+src=["\'](.*?)["\']',
|
||||
video_url = self._html_search_regex(html5_video_re, webpage, video_id)
|
||||
mp4_video_url = self._html_search_regex(html5_video_re, mp4_webpage, video_id)
|
||||
formats = [{
|
||||
'url': video_url,
|
||||
}]
|
||||
if mp4_video_url != video_url:
|
||||
formats.append({
|
||||
'url': mp4_video_url,
|
||||
json_url = f'https://www.nuvid.com/player_config_json/?vid={video_id}&aid=0&domain_id=0&embed=0&check_speed=0'
|
||||
video_data = self._download_json(
|
||||
json_url, video_id, headers={
|
||||
'Accept': 'application/json, text/javascript, */*; q = 0.01',
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||
})
|
||||
|
||||
title = self._html_search_regex(
|
||||
[r'<span title="([^"]+)">',
|
||||
r'<div class="thumb-holder video">\s*<h5[^>]*>([^<]+)</h5>',
|
||||
r'<span[^>]+class="title_thumb">([^<]+)</span>'], webpage, 'title').strip()
|
||||
thumbnails = [
|
||||
{
|
||||
'url': thumb_url,
|
||||
} for thumb_url in re.findall(r'<img src="([^"]+)" alt="" />', webpage)
|
||||
]
|
||||
thumbnail = thumbnails[0]['url'] if thumbnails else None
|
||||
duration = parse_duration(self._html_search_regex(
|
||||
[r'<i class="fa fa-clock-o"></i>\s*(\d{2}:\d{2})',
|
||||
r'<span[^>]+class="view_time">([^<]+)</span>'], webpage, 'duration', fatal=False))
|
||||
formats = [{
|
||||
'url': source,
|
||||
'format_id': qualities.get(quality),
|
||||
'height': int_or_none(qualities.get(quality)[:-1]),
|
||||
} for quality, source in video_data.get('files').items() if source]
|
||||
|
||||
self._check_formats(formats, video_id)
|
||||
self._sort_formats(formats)
|
||||
|
||||
title = video_data.get('title')
|
||||
thumbnail_base_url = try_get(video_data, lambda x: x['thumbs']['url'])
|
||||
thumbnail_extension = try_get(video_data, lambda x: x['thumbs']['extension'])
|
||||
thumbnail_id = self._search_regex(
|
||||
r'/media/videos/tmb/6523263/preview/(/d+)' + thumbnail_extension, video_data.get('poster', ''), 'thumbnail id', default=19)
|
||||
thumbnail = f'{thumbnail_base_url}player/{thumbnail_id}{thumbnail_extension}'
|
||||
duration = parse_duration(video_data.get('duration') or video_data.get('duration_format'))
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'formats': formats,
|
||||
'title': title,
|
||||
'thumbnails': thumbnails,
|
||||
'thumbnail': thumbnail,
|
||||
'duration': duration,
|
||||
'age_limit': 18,
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
98
yt_dlp/extractor/nzherald.py
Normal file
98
yt_dlp/extractor/nzherald.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .brightcove import BrightcoveNewIE
|
||||
from .common import InfoExtractor
|
||||
|
||||
from ..compat import compat_str
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
traverse_obj
|
||||
)
|
||||
|
||||
|
||||
class NZHeraldIE(InfoExtractor):
|
||||
IE_NAME = 'nzherald'
|
||||
_VALID_URL = r'https?://(?:www\.)?nzherald\.co\.nz/[\w\/-]+\/(?P<id>[A-Z0-9]+)'
|
||||
_TESTS = [
|
||||
{
|
||||
'url': 'https://www.nzherald.co.nz/nz/weather-heavy-rain-gales-across-nz-most-days-this-week/PTG7QWY4E2225YHZ5NAIRBTYTQ/',
|
||||
'info_dict': {
|
||||
'id': '6271084466001',
|
||||
'ext': 'mp4',
|
||||
'title': 'MetService severe weather warning: September 6th - 7th',
|
||||
'timestamp': 1630891576,
|
||||
'upload_date': '20210906',
|
||||
'uploader_id': '1308227299001',
|
||||
'description': 'md5:db6ca335a22e2cdf37ab9d2bcda52902'
|
||||
}
|
||||
|
||||
}, {
|
||||
# Webpage has brightcove embed player url
|
||||
'url': 'https://www.nzherald.co.nz/travel/pencarrow-coastal-trail/HDVTPJEPP46HJ2UEMK4EGD2DFI/',
|
||||
'info_dict': {
|
||||
'id': '6261791733001',
|
||||
'ext': 'mp4',
|
||||
'title': 'Pencarrow Coastal Trail',
|
||||
'timestamp': 1625102897,
|
||||
'upload_date': '20210701',
|
||||
'uploader_id': '1308227299001',
|
||||
'description': 'md5:d361aaa0c6498f7ac1bc4fc0a0aec1e4'
|
||||
}
|
||||
|
||||
}, {
|
||||
# two video embeds of the same video
|
||||
'url': 'https://www.nzherald.co.nz/nz/truck-driver-captured-cutting-off-motorist-on-state-highway-1-in-canterbury/FIHNJB7PLLPHWQPK4S7ZBDUC4I/',
|
||||
'info_dict': {
|
||||
'id': '6251114530001',
|
||||
'ext': 'mp4',
|
||||
'title': 'Truck travelling north from Rakaia runs car off road',
|
||||
'timestamp': 1619730509,
|
||||
'upload_date': '20210429',
|
||||
'uploader_id': '1308227299001',
|
||||
'description': 'md5:4cae7dfb7613ac4c73b9e73a75c6b5d7'
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.nzherald.co.nz/kahu/kaupapa-companies-my-taiao-supporting-maori-in-study-and-business/PQBO2J25WCG77VGRX7W7BVYEAI/',
|
||||
'only_matching': True
|
||||
}, {
|
||||
'url': 'https://nzherald.co.nz/the-country/video/focus-nzs-first-mass-covid-19-vaccination-event/N5I7IL3BRFLZSD33TLDLYJDGK4/',
|
||||
'only_matching': True
|
||||
}, {
|
||||
'url': 'https://www.nzherald.co.nz/the-vision-is-clear/news/tvic-damian-roper-planting-trees-an-addiction/AN2AAEPNRK5VLISDWQAJZB6ATQ',
|
||||
'only_matching': True
|
||||
}
|
||||
]
|
||||
|
||||
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/1308227299001/S1BXZn8t_default/index.html?videoId=%s'
|
||||
|
||||
def _extract_bc_embed_url(self, webpage):
|
||||
"""The initial webpage may include the brightcove player embed url"""
|
||||
bc_url = BrightcoveNewIE._extract_url(self, webpage)
|
||||
return bc_url or self._search_regex(
|
||||
r'(?:embedUrl)\"\s*:\s*\"(?P<embed_url>%s)' % BrightcoveNewIE._VALID_URL,
|
||||
webpage, 'embed url', default=None, group='embed_url')
|
||||
|
||||
def _real_extract(self, url):
|
||||
article_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, article_id)
|
||||
bc_url = self._extract_bc_embed_url(webpage)
|
||||
|
||||
if not bc_url:
|
||||
fusion_metadata = self._parse_json(
|
||||
self._search_regex(r'Fusion\.globalContent\s*=\s*({.+?})\s*;', webpage, 'fusion metadata'), article_id)
|
||||
|
||||
video_metadata = fusion_metadata.get('video')
|
||||
bc_video_id = traverse_obj(
|
||||
video_metadata or fusion_metadata, # fusion metadata is the video metadata for video-only pages
|
||||
'brightcoveId', ('content_elements', ..., 'referent', 'id'),
|
||||
get_all=False, expected_type=compat_str)
|
||||
|
||||
if not bc_video_id:
|
||||
if isinstance(video_metadata, dict) and len(video_metadata) == 0:
|
||||
raise ExtractorError('This article does not have a video.', expected=True)
|
||||
else:
|
||||
raise ExtractorError('Failed to extract brightcove video id')
|
||||
bc_url = self.BRIGHTCOVE_URL_TEMPLATE % bc_video_id
|
||||
|
||||
return self.url_result(bc_url, 'BrightcoveNew')
|
||||
56
yt_dlp/extractor/olympics.py
Normal file
56
yt_dlp/extractor/olympics.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import unified_strdate
|
||||
|
||||
|
||||
class OlympicsReplayIE(InfoExtractor):
|
||||
_VALID_URL = r'(?:https?://)(?:www\.)?olympics\.com/tokyo-2020/(?:[a-z]{2}/)?replay/(?P<id>[^/#&?]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://olympics.com/tokyo-2020/en/replay/300622eb-abc0-43ea-b03b-c5f2d429ec7b/jumping-team-qualifier',
|
||||
'info_dict': {
|
||||
'id': '300622eb-abc0-43ea-b03b-c5f2d429ec7b',
|
||||
'ext': 'mp4',
|
||||
'title': 'Jumping Team Qualifier',
|
||||
'release_date': '20210806',
|
||||
'upload_date': '20210713',
|
||||
},
|
||||
'params': {
|
||||
'format': 'bv',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://olympics.com/tokyo-2020/en/replay/bd242924-4b22-49a5-a846-f1d4c809250d/mens-bronze-medal-match-hun-esp',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
id = self._match_id(url)
|
||||
# The parameters are hardcoded in the webpage, it's not necessary to download the webpage just for these parameters.
|
||||
# If in downloading webpage serves other functions aswell, then extract these parameters from it.
|
||||
token_url = 'https://appovptok.ovpobs.tv/api/identity/app/token?api_key=OTk5NDcxOjpvY3N3LWFwaXVzZXI%3D&api_secret=ODY4ODM2MjE3ODMwYmVjNTAxMWZlMDJiMTYxZmY0MjFiMjMwMjllMjJmNDA1YWRiYzA5ODcxYTZjZTljZDkxOTo6NTM2NWIzNjRlMTM1ZmI2YWNjNmYzMGMzOGM3NzZhZTY%3D'
|
||||
token = self._download_webpage(token_url, id)
|
||||
headers = {'x-obs-app-token': token}
|
||||
data_json = self._download_json(f'https://appocswtok.ovpobs.tv/api/schedule-sessions/{id}?include=stream',
|
||||
id, headers=headers)
|
||||
meta_data = data_json['data']['attributes']
|
||||
for t_dict in data_json['included']:
|
||||
if t_dict.get('type') == 'Stream':
|
||||
stream_data = t_dict['attributes']
|
||||
m3u8_url = self._download_json(
|
||||
'https://meteringtok.ovpobs.tv/api/playback-sessions', id, headers=headers, query={
|
||||
'alias': stream_data['alias'],
|
||||
'stream': stream_data['stream'],
|
||||
'type': 'vod'
|
||||
})['data']['attributes']['url']
|
||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, id)
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': id,
|
||||
'title': meta_data['title'],
|
||||
'release_date': unified_strdate(meta_data.get('start') or meta_data.get('broadcastPublished')),
|
||||
'upload_date': unified_strdate(meta_data.get('publishedAt')),
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
}
|
||||
@@ -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'),
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user