Compare commits
304 Commits
2016.05.21
...
2016.06.20
Author | SHA1 | Date | |
---|---|---|---|
![]() |
19e2d1cdea | ||
![]() |
8369a4fe76 | ||
![]() |
1f749b6658 | ||
![]() |
819707920a | ||
![]() |
43518503a6 | ||
![]() |
5839d556e4 | ||
![]() |
6c83e583b3 | ||
![]() |
6aeb64b673 | ||
![]() |
6cd64b6806 | ||
![]() |
e154c65128 | ||
![]() |
a50fd6e026 | ||
![]() |
6a55bb66ee | ||
![]() |
7c05097633 | ||
![]() |
589568789f | ||
![]() |
7577d849a6 | ||
![]() |
cb23192bc4 | ||
![]() |
41c1023300 | ||
![]() |
90b6288cce | ||
![]() |
c1823c8ad9 | ||
![]() |
d7c6c656c5 | ||
![]() |
b0b128049a | ||
![]() |
e8f13f2637 | ||
![]() |
b5aad37f6b | ||
![]() |
6d0d4fc26d | ||
![]() |
0278aa443f | ||
![]() |
1f35745758 | ||
![]() |
573c35272f | ||
![]() |
09e3f91e40 | ||
![]() |
1b6cf16be7 | ||
![]() |
26264cb056 | ||
![]() |
a72df5f36f | ||
![]() |
c878e635de | ||
![]() |
0f47cc2e92 | ||
![]() |
5fc2757682 | ||
![]() |
e3944c2621 | ||
![]() |
667d96480b | ||
![]() |
e6fe993c31 | ||
![]() |
d0d93f76ea | ||
![]() |
20a6a154fe | ||
![]() |
f011876076 | ||
![]() |
6929569403 | ||
![]() |
eb451890da | ||
![]() |
ded7511a70 | ||
![]() |
d2161cade5 | ||
![]() |
27e5fa8198 | ||
![]() |
efbd1eb51a | ||
![]() |
369ff75081 | ||
![]() |
47212f7bcb | ||
![]() |
4c93ee8d14 | ||
![]() |
8bc4dbb1af | ||
![]() |
6c3760292c | ||
![]() |
4cef70db6c | ||
![]() |
ff4af6ec59 | ||
![]() |
d01fb21d4c | ||
![]() |
a4ea28eee6 | ||
![]() |
bc2a871f3e | ||
![]() |
1759672eed | ||
![]() |
fea55ef4a9 | ||
![]() |
16b6bd01d2 | ||
![]() |
14d0f4e0f3 | ||
![]() |
778f969447 | ||
![]() |
79cd8b3d8a | ||
![]() |
b4663f12b1 | ||
![]() |
b50e02c1e4 | ||
![]() |
33b72ce64e | ||
![]() |
cf2bf840ba | ||
![]() |
bccdac6874 | ||
![]() |
e69f9f5d68 | ||
![]() |
77a9a9c295 | ||
![]() |
84dcd1c4e4 | ||
![]() |
971e3b7520 | ||
![]() |
4e79011729 | ||
![]() |
a936ac321c | ||
![]() |
98960c911c | ||
![]() |
329ca3bef6 | ||
![]() |
2c3322e36e | ||
![]() |
80ae228b34 | ||
![]() |
6d28c408cf | ||
![]() |
c83b35d4aa | ||
![]() |
94e5d6aedb | ||
![]() |
531a74968c | ||
![]() |
c5edd147d1 | ||
![]() |
856150d056 | ||
![]() |
03ebea89b0 | ||
![]() |
15d106787e | ||
![]() |
7aab3696dd | ||
![]() |
47787efa2b | ||
![]() |
4a420119a6 | ||
![]() |
33751818d3 | ||
![]() |
698f127c1a | ||
![]() |
fe458b6596 | ||
![]() |
21ac1a8ac3 | ||
![]() |
79027c0ea0 | ||
![]() |
4cad2929cd | ||
![]() |
62666af99f | ||
![]() |
9ddc289f88 | ||
![]() |
6626c214e1 | ||
![]() |
d845622b2e | ||
![]() |
1058f56e96 | ||
![]() |
0434358823 | ||
![]() |
3841256c2c | ||
![]() |
bdf16f8140 | ||
![]() |
836ab0c554 | ||
![]() |
6c0376fe4f | ||
![]() |
1fa309da40 | ||
![]() |
daa0df9e8b | ||
![]() |
09728d5fbc | ||
![]() |
c16f8a4659 | ||
![]() |
a225238530 | ||
![]() |
55b2f099c0 | ||
![]() |
9631a94fb5 | ||
![]() |
cc4444662c | ||
![]() |
de3eb07ed6 | ||
![]() |
5de008e8c3 | ||
![]() |
3e74b444e7 | ||
![]() |
e1e0a10c56 | ||
![]() |
436214baf7 | ||
![]() |
506d0e9693 | ||
![]() |
55290788d3 | ||
![]() |
bc7e7adf51 | ||
![]() |
b0aebe702c | ||
![]() |
416878f41f | ||
![]() |
c0fed3bda5 | ||
![]() |
bb1e44cc8e | ||
![]() |
21efee5f8b | ||
![]() |
e2713d32f4 | ||
![]() |
e21c26daf9 | ||
![]() |
1594a4932f | ||
![]() |
6869d634c6 | ||
![]() |
50918c4ee0 | ||
![]() |
6c33d24b46 | ||
![]() |
be6217b261 | ||
![]() |
9d51a0a9a1 | ||
![]() |
39da509f67 | ||
![]() |
a479b8f687 | ||
![]() |
48a5eabc48 | ||
![]() |
11380753b5 | ||
![]() |
411c590a1f | ||
![]() |
6da8d7de69 | ||
![]() |
c6308b3153 | ||
![]() |
fc0a45fa41 | ||
![]() |
e6e90515db | ||
![]() |
22a0a95247 | ||
![]() |
50ce1c331c | ||
![]() |
7264e38591 | ||
![]() |
33d9f3707c | ||
![]() |
a26a9d6239 | ||
![]() |
a4a8201c02 | ||
![]() |
a6571f1073 | ||
![]() |
57b6e9652e | ||
![]() |
3d9b3605a3 | ||
![]() |
74193838f7 | ||
![]() |
fb94e260b5 | ||
![]() |
345dec937f | ||
![]() |
4315f74fa8 | ||
![]() |
e67f688025 | ||
![]() |
db59b37d0b | ||
![]() |
244fe977fe | ||
![]() |
7b0d1c2859 | ||
![]() |
21d0a8e48b | ||
![]() |
47f12ad3e3 | ||
![]() |
8f1aaa97a1 | ||
![]() |
9d78524cbe | ||
![]() |
bc270284b5 | ||
![]() |
c93b4eaceb | ||
![]() |
71b9cb3107 | ||
![]() |
633b444fd2 | ||
![]() |
51c4d85ce7 | ||
![]() |
631d4c87ee | ||
![]() |
1e236d7e23 | ||
![]() |
2c34735267 | ||
![]() |
39b32571df | ||
![]() |
db56f281d9 | ||
![]() |
e92b552a10 | ||
![]() |
1ae6c83bce | ||
![]() |
0fc832e1b2 | ||
![]() |
7def35712a | ||
![]() |
cad88f96dc | ||
![]() |
762d44c956 | ||
![]() |
4d8856d511 | ||
![]() |
c917106be4 | ||
![]() |
76e9cd7f24 | ||
![]() |
bf4c6a38e1 | ||
![]() |
7f3c3dfa52 | ||
![]() |
9c3c447eb3 | ||
![]() |
ad73083ff0 | ||
![]() |
1e8b59243f | ||
![]() |
c88270271e | ||
![]() |
b96f007eeb | ||
![]() |
9a4aec8b7e | ||
![]() |
54fb199681 | ||
![]() |
8c32e5dc32 | ||
![]() |
0ea590076f | ||
![]() |
4a684895c0 | ||
![]() |
f4e4aa9b6b | ||
![]() |
5e3856a2c5 | ||
![]() |
6e6b9f600f | ||
![]() |
6a1df4fb5f | ||
![]() |
dde1ce7c06 | ||
![]() |
811586ebcf | ||
![]() |
0ff3749bfe | ||
![]() |
28bab13348 | ||
![]() |
877032314f | ||
![]() |
e7d85c4ef7 | ||
![]() |
8ec2b2c41c | ||
![]() |
197a5da1d0 | ||
![]() |
abbb2938fa | ||
![]() |
f657b1a5f2 | ||
![]() |
86a52881c6 | ||
![]() |
8267423652 | ||
![]() |
917a3196f8 | ||
![]() |
56bd028a0f | ||
![]() |
681b923b5c | ||
![]() |
9ed6d8c6c5 | ||
![]() |
f3fb420b82 | ||
![]() |
165e3561e9 | ||
![]() |
27f17c0eab | ||
![]() |
44c8892369 | ||
![]() |
f574103d7c | ||
![]() |
6d138e98e3 | ||
![]() |
2a329110b9 | ||
![]() |
2bee7b25f3 | ||
![]() |
92cf872a48 | ||
![]() |
6461f2b7ec | ||
![]() |
807cf7b07f | ||
![]() |
de7d76af52 | ||
![]() |
11c70deba7 | ||
![]() |
f36532404d | ||
![]() |
77b8b4e696 | ||
![]() |
2615fa7584 | ||
![]() |
3a686853e1 | ||
![]() |
949fc42e00 | ||
![]() |
33a1ff7113 | ||
![]() |
bec2c14f2c | ||
![]() |
37f972954d | ||
![]() |
3874e6ea66 | ||
![]() |
fac2af3c51 | ||
![]() |
6f8cb24219 | ||
![]() |
448bb5f333 | ||
![]() |
293c255688 | ||
![]() |
ac88d2316e | ||
![]() |
5950cb1d6d | ||
![]() |
761052db92 | ||
![]() |
240b60453e | ||
![]() |
85b0fe7d64 | ||
![]() |
0a5685b26f | ||
![]() |
6f748df43f | ||
![]() |
b410cb83d4 | ||
![]() |
da9d82840a | ||
![]() |
4ee0b8afdb | ||
![]() |
1de32771e1 | ||
![]() |
688c634b7d | ||
![]() |
0d6ee97508 | ||
![]() |
6b43132ce9 | ||
![]() |
a4690b3244 | ||
![]() |
444417edb5 | ||
![]() |
277c7465f5 | ||
![]() |
25bcd3550e | ||
![]() |
a4760d204f | ||
![]() |
e8593f346a | ||
![]() |
05b651e3a5 | ||
![]() |
42a7439717 | ||
![]() |
b1e9ebd080 | ||
![]() |
0c50eeb987 | ||
![]() |
4b464a6a78 | ||
![]() |
5db9df622f | ||
![]() |
5181759c0d | ||
![]() |
e54373204a | ||
![]() |
102810ef04 | ||
![]() |
78d3b3e213 | ||
![]() |
7a46542f97 | ||
![]() |
eb7941e3e6 | ||
![]() |
db3b8b2103 | ||
![]() |
c5f5155100 | ||
![]() |
4a12077855 | ||
![]() |
a4a7c44bd3 | ||
![]() |
70346165fe | ||
![]() |
c776b99691 | ||
![]() |
e9297256d4 | ||
![]() |
e5871c672b | ||
![]() |
9b06b0fb92 | ||
![]() |
4f3a25c2b4 | ||
![]() |
21a19aa94d | ||
![]() |
c6b9cf05e1 | ||
![]() |
4d8819d249 | ||
![]() |
898f4b49cc | ||
![]() |
0150a00f33 | ||
![]() |
c8831015f4 | ||
![]() |
92d221ad48 | ||
![]() |
0db9a05f88 | ||
![]() |
e03b35b8f9 | ||
![]() |
d2fee3c99e | ||
![]() |
93fdb14177 | ||
![]() |
370d4eb8ad | ||
![]() |
3452c3a27c | ||
![]() |
81f35fee2f | ||
![]() |
0fdbe3146c | ||
![]() |
8d93c21466 | ||
![]() |
1dbfd78754 | ||
![]() |
22e35adefd | ||
![]() |
833b644fff | ||
![]() |
57cf9b7f06 | ||
![]() |
14f7a2b8af | ||
![]() |
c0837a12c8 |
6
.github/ISSUE_TEMPLATE.md
vendored
6
.github/ISSUE_TEMPLATE.md
vendored
@@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Make sure you are using the *latest* version: run `youtube-dl --version` and ensure your version is *2016.05.21.1*. If it's not read [this FAQ entry](https://github.com/rg3/youtube-dl/blob/master/README.md#how-do-i-update-youtube-dl) and update. Issues with outdated version will be rejected.
|
### Make sure you are using the *latest* version: run `youtube-dl --version` and ensure your version is *2016.06.20*. If it's not read [this FAQ entry](https://github.com/rg3/youtube-dl/blob/master/README.md#how-do-i-update-youtube-dl) and update. Issues with outdated version will be rejected.
|
||||||
- [ ] I've **verified** and **I assure** that I'm running youtube-dl **2016.05.21.1**
|
- [ ] I've **verified** and **I assure** that I'm running youtube-dl **2016.06.20**
|
||||||
|
|
||||||
### Before submitting an *issue* make sure you have:
|
### Before submitting an *issue* make sure you have:
|
||||||
- [ ] At least skimmed through [README](https://github.com/rg3/youtube-dl/blob/master/README.md) and **most notably** [FAQ](https://github.com/rg3/youtube-dl#faq) and [BUGS](https://github.com/rg3/youtube-dl#bugs) sections
|
- [ ] At least skimmed through [README](https://github.com/rg3/youtube-dl/blob/master/README.md) and **most notably** [FAQ](https://github.com/rg3/youtube-dl#faq) and [BUGS](https://github.com/rg3/youtube-dl#bugs) sections
|
||||||
@@ -35,7 +35,7 @@ $ youtube-dl -v <your command line>
|
|||||||
[debug] User config: []
|
[debug] User config: []
|
||||||
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
|
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
|
||||||
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
||||||
[debug] youtube-dl version 2016.05.21.1
|
[debug] youtube-dl version 2016.06.20
|
||||||
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
|
[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] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -28,12 +28,16 @@ updates_key.pem
|
|||||||
*.mp4
|
*.mp4
|
||||||
*.m4a
|
*.m4a
|
||||||
*.m4v
|
*.m4v
|
||||||
|
*.mp3
|
||||||
*.part
|
*.part
|
||||||
*.swp
|
*.swp
|
||||||
test/testdata
|
test/testdata
|
||||||
test/local_parameters.json
|
test/local_parameters.json
|
||||||
.tox
|
.tox
|
||||||
youtube-dl.zsh
|
youtube-dl.zsh
|
||||||
|
|
||||||
|
# IntelliJ related files
|
||||||
.idea
|
.idea
|
||||||
.idea/*
|
*.iml
|
||||||
|
|
||||||
tmp/
|
tmp/
|
||||||
|
@@ -14,7 +14,6 @@ script: nosetests test --verbose
|
|||||||
notifications:
|
notifications:
|
||||||
email:
|
email:
|
||||||
- filippo.valsorda@gmail.com
|
- filippo.valsorda@gmail.com
|
||||||
- phihag@phihag.de
|
|
||||||
- yasoob.khld@gmail.com
|
- yasoob.khld@gmail.com
|
||||||
# irc:
|
# irc:
|
||||||
# channels:
|
# channels:
|
||||||
|
3
AUTHORS
3
AUTHORS
@@ -172,3 +172,6 @@ blahgeek
|
|||||||
Kevin Deldycke
|
Kevin Deldycke
|
||||||
inondle
|
inondle
|
||||||
Tomáš Čech
|
Tomáš Čech
|
||||||
|
Déstin Reed
|
||||||
|
Roman Tsiupa
|
||||||
|
Artur Krysiak
|
||||||
|
@@ -142,9 +142,9 @@ After you have ensured this site is distributing it's content legally, you can f
|
|||||||
```
|
```
|
||||||
5. Add an import in [`youtube_dl/extractor/extractors.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/extractors.py).
|
5. Add an import in [`youtube_dl/extractor/extractors.py`](https://github.com/rg3/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.
|
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.
|
||||||
7. Have a look at [`youtube_dl/extractor/common.py`](https://github.com/rg3/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/rg3/youtube-dl/blob/58525c94d547be1c8167d16c298bdd75506db328/youtube_dl/extractor/common.py#L68-L226). Add tests and code for as many as you want.
|
7. Have a look at [`youtube_dl/extractor/common.py`](https://github.com/rg3/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/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py#L74-L252). Add tests and code for as many as you want.
|
||||||
8. Keep in mind that the only mandatory fields in info dict for successful extraction process are `id`, `title` and either `url` or `formats`, i.e. these are the critical data the extraction does not make any sense without. This means that [any field](https://github.com/rg3/youtube-dl/blob/58525c94d547be1c8167d16c298bdd75506db328/youtube_dl/extractor/common.py#L138-L226) apart from aforementioned mandatory ones should be treated **as optional** and extraction should be **tolerate** to situations when sources for these fields can potentially be unavailable (even if they always available at the moment) and **future-proof** in order not to break the extraction of general purpose mandatory fields. For example, if you have some intermediate dict `meta` that is a source of metadata and it has a key `summary` that you want to extract and put into resulting info dict as `description`, you should be ready that this key may be missing from the `meta` dict, i.e. you should extract it as `meta.get('summary')` and not `meta['summary']`. Similarly, you should pass `fatal=False` when extracting data from a webpage with `_search_regex/_html_search_regex`.
|
8. Keep in mind that the only mandatory fields in info dict for successful extraction process are `id`, `title` and either `url` or `formats`, i.e. these are the critical data the extraction does not make any sense without. This means that [any field](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py#L148-L252) apart from aforementioned mandatory ones should be treated **as optional** and extraction should be **tolerate** to situations when sources for these fields can potentially be unavailable (even if they always available at the moment) and **future-proof** in order not to break the extraction of general purpose mandatory fields. For example, if you have some intermediate dict `meta` that is a source of metadata and it has a key `summary` that you want to extract and put into resulting info dict as `description`, you should be ready that this key may be missing from the `meta` dict, i.e. you should extract it as `meta.get('summary')` and not `meta['summary']`. Similarly, you should pass `fatal=False` when extracting data from a webpage with `_search_regex/_html_search_regex`.
|
||||||
9. Check the code with [flake8](https://pypi.python.org/pypi/flake8).
|
9. Check the code with [flake8](https://pypi.python.org/pypi/flake8). Also make sure your code works under all [Python](http://www.python.org/) versions claimed supported by youtube-dl, namely 2.6, 2.7, and 3.2+.
|
||||||
10. When the tests pass, [add](http://git-scm.com/docs/git-add) the new files and [commit](http://git-scm.com/docs/git-commit) them and [push](http://git-scm.com/docs/git-push) the result, like this:
|
10. When the tests pass, [add](http://git-scm.com/docs/git-add) the new files and [commit](http://git-scm.com/docs/git-commit) them and [push](http://git-scm.com/docs/git-push) the result, like this:
|
||||||
|
|
||||||
$ git add youtube_dl/extractor/extractors.py
|
$ git add youtube_dl/extractor/extractors.py
|
||||||
|
4
Makefile
4
Makefile
@@ -1,7 +1,7 @@
|
|||||||
all: youtube-dl README.md CONTRIBUTING.md README.txt youtube-dl.1 youtube-dl.bash-completion youtube-dl.zsh youtube-dl.fish supportedsites
|
all: youtube-dl README.md CONTRIBUTING.md README.txt youtube-dl.1 youtube-dl.bash-completion youtube-dl.zsh youtube-dl.fish supportedsites
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf youtube-dl.1.temp.md youtube-dl.1 youtube-dl.bash-completion README.txt MANIFEST build/ dist/ .coverage cover/ youtube-dl.tar.gz youtube-dl.zsh youtube-dl.fish youtube_dl/extractor/lazy_extractors.py *.dump *.part *.info.json *.mp4 *.flv *.mp3 *.avi *.mkv *.webm *.jpg *.png CONTRIBUTING.md.tmp ISSUE_TEMPLATE.md.tmp youtube-dl youtube-dl.exe
|
rm -rf youtube-dl.1.temp.md youtube-dl.1 youtube-dl.bash-completion README.txt MANIFEST build/ dist/ .coverage cover/ youtube-dl.tar.gz youtube-dl.zsh youtube-dl.fish youtube_dl/extractor/lazy_extractors.py *.dump *.part *.info.json *.mp4 *.m4a *.flv *.mp3 *.avi *.mkv *.webm *.jpg *.png CONTRIBUTING.md.tmp ISSUE_TEMPLATE.md.tmp youtube-dl youtube-dl.exe
|
||||||
find . -name "*.pyc" -delete
|
find . -name "*.pyc" -delete
|
||||||
find . -name "*.class" -delete
|
find . -name "*.class" -delete
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ README.txt: README.md
|
|||||||
pandoc -f markdown -t plain README.md -o README.txt
|
pandoc -f markdown -t plain README.md -o README.txt
|
||||||
|
|
||||||
youtube-dl.1: README.md
|
youtube-dl.1: README.md
|
||||||
$(PYTHON) devscripts/prepare_manpage.py >youtube-dl.1.temp.md
|
$(PYTHON) devscripts/prepare_manpage.py youtube-dl.1.temp.md
|
||||||
pandoc -s -f markdown -t man youtube-dl.1.temp.md -o youtube-dl.1
|
pandoc -s -f markdown -t man youtube-dl.1.temp.md -o youtube-dl.1
|
||||||
rm -f youtube-dl.1.temp.md
|
rm -f youtube-dl.1.temp.md
|
||||||
|
|
||||||
|
72
README.md
72
README.md
@@ -17,7 +17,7 @@ youtube-dl - download videos from youtube.com or other video platforms
|
|||||||
|
|
||||||
To install it right away for all UNIX users (Linux, OS X, etc.), type:
|
To install it right away for all UNIX users (Linux, OS X, etc.), type:
|
||||||
|
|
||||||
sudo curl https://yt-dl.org/latest/youtube-dl -o /usr/local/bin/youtube-dl
|
sudo curl -L https://yt-dl.org/latest/youtube-dl -o /usr/local/bin/youtube-dl
|
||||||
sudo chmod a+rx /usr/local/bin/youtube-dl
|
sudo chmod a+rx /usr/local/bin/youtube-dl
|
||||||
|
|
||||||
If you do not have curl, you can alternatively use a recent wget:
|
If you do not have curl, you can alternatively use a recent wget:
|
||||||
@@ -25,20 +25,26 @@ If you do not have curl, you can alternatively use a recent wget:
|
|||||||
sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl
|
sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl
|
||||||
sudo chmod a+rx /usr/local/bin/youtube-dl
|
sudo chmod a+rx /usr/local/bin/youtube-dl
|
||||||
|
|
||||||
Windows users can [download a .exe file](https://yt-dl.org/latest/youtube-dl.exe) and place it in their home directory or any other location on their [PATH](http://en.wikipedia.org/wiki/PATH_%28variable%29).
|
Windows users can [download an .exe file](https://yt-dl.org/latest/youtube-dl.exe) and place it in any location on their [PATH](http://en.wikipedia.org/wiki/PATH_%28variable%29) except for `%SYSTEMROOT%\System32` (e.g. **do not** put in `C:\Windows\System32`).
|
||||||
|
|
||||||
OS X users can install **youtube-dl** with [Homebrew](http://brew.sh/).
|
|
||||||
|
|
||||||
brew install youtube-dl
|
|
||||||
|
|
||||||
You can also use pip:
|
You can also use pip:
|
||||||
|
|
||||||
sudo pip install youtube-dl
|
sudo pip install --upgrade youtube-dl
|
||||||
|
|
||||||
|
This command will update youtube-dl if you have already installed it. See the [pypi page](https://pypi.python.org/pypi/youtube_dl) for more information.
|
||||||
|
|
||||||
|
OS X users can install youtube-dl with [Homebrew](http://brew.sh/):
|
||||||
|
|
||||||
|
brew install youtube-dl
|
||||||
|
|
||||||
|
Or with [MacPorts](https://www.macports.org/):
|
||||||
|
|
||||||
|
sudo port install youtube-dl
|
||||||
|
|
||||||
Alternatively, refer to the [developer instructions](#developer-instructions) for how to check out and work with the git repository. For further options, including PGP signatures, see the [youtube-dl Download Page](https://rg3.github.io/youtube-dl/download.html).
|
Alternatively, refer to the [developer instructions](#developer-instructions) for how to check out and work with the git repository. For further options, including PGP signatures, see the [youtube-dl Download Page](https://rg3.github.io/youtube-dl/download.html).
|
||||||
|
|
||||||
# DESCRIPTION
|
# DESCRIPTION
|
||||||
**youtube-dl** is a small command-line program to download videos from
|
**youtube-dl** is a command-line program to download videos from
|
||||||
YouTube.com and a few more sites. It requires the Python interpreter, version
|
YouTube.com and a few more sites. It requires the Python interpreter, version
|
||||||
2.6, 2.7, or 3.2+, and it is not platform specific. It should work on
|
2.6, 2.7, or 3.2+, and it is not platform specific. It should work on
|
||||||
your Unix box, on Windows or on Mac OS X. It is released to the public domain,
|
your Unix box, on Windows or on Mac OS X. It is released to the public domain,
|
||||||
@@ -73,8 +79,8 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
repairs broken URLs, but emits an error if
|
repairs broken URLs, but emits an error if
|
||||||
this is not possible instead of searching.
|
this is not possible instead of searching.
|
||||||
--ignore-config Do not read configuration files. When given
|
--ignore-config Do not read configuration files. When given
|
||||||
in the global configuration file /etc
|
in the global configuration file
|
||||||
/youtube-dl.conf: Do not read the user
|
/etc/youtube-dl.conf: Do not read the user
|
||||||
configuration in ~/.config/youtube-
|
configuration in ~/.config/youtube-
|
||||||
dl/config (%APPDATA%/youtube-dl/config.txt
|
dl/config (%APPDATA%/youtube-dl/config.txt
|
||||||
on Windows)
|
on Windows)
|
||||||
@@ -162,7 +168,7 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
(experimental)
|
(experimental)
|
||||||
|
|
||||||
## Download Options:
|
## Download Options:
|
||||||
-r, --rate-limit LIMIT Maximum download rate in bytes per second
|
-r, --limit-rate RATE Maximum download rate in bytes per second
|
||||||
(e.g. 50K or 4.2M)
|
(e.g. 50K or 4.2M)
|
||||||
-R, --retries RETRIES Number of retries (default is 10), or
|
-R, --retries RETRIES Number of retries (default is 10), or
|
||||||
"infinite".
|
"infinite".
|
||||||
@@ -249,18 +255,19 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
--write-info-json Write video metadata to a .info.json file
|
--write-info-json Write video metadata to a .info.json file
|
||||||
--write-annotations Write video annotations to a
|
--write-annotations Write video annotations to a
|
||||||
.annotations.xml file
|
.annotations.xml file
|
||||||
--load-info FILE JSON file containing the video information
|
--load-info-json FILE JSON file containing the video information
|
||||||
(created with the "--write-info-json"
|
(created with the "--write-info-json"
|
||||||
option)
|
option)
|
||||||
--cookies FILE File to read cookies from and dump cookie
|
--cookies FILE File to read cookies from and dump cookie
|
||||||
jar in
|
jar in
|
||||||
--cache-dir DIR Location in the filesystem where youtube-dl
|
--cache-dir DIR Location in the filesystem where youtube-dl
|
||||||
can store some downloaded information
|
can store some downloaded information
|
||||||
permanently. By default $XDG_CACHE_HOME
|
permanently. By default
|
||||||
/youtube-dl or ~/.cache/youtube-dl . At the
|
$XDG_CACHE_HOME/youtube-dl or
|
||||||
moment, only YouTube player files (for
|
~/.cache/youtube-dl . At the moment, only
|
||||||
videos with obfuscated signatures) are
|
YouTube player files (for videos with
|
||||||
cached, but that may change.
|
obfuscated signatures) are cached, but that
|
||||||
|
may change.
|
||||||
--no-cache-dir Disable filesystem caching
|
--no-cache-dir Disable filesystem caching
|
||||||
--rm-cache-dir Delete all filesystem cache files
|
--rm-cache-dir Delete all filesystem cache files
|
||||||
|
|
||||||
@@ -433,7 +440,7 @@ You can use `--ignore-config` if you want to disable the configuration file for
|
|||||||
|
|
||||||
### Authentication with `.netrc` file
|
### 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 youtube-dl execution and prevent tracking plain text passwords in the shell command history. You can achieve this using a [`.netrc` file](http://stackoverflow.com/tags/.netrc/info) on per extractor basis. For that you will need to create a`.netrc` file in your `$HOME` and restrict permissions to read/write by you only:
|
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 youtube-dl execution and prevent tracking plain text passwords in the shell command history. You can achieve this using a [`.netrc` file](http://stackoverflow.com/tags/.netrc/info) on per extractor basis. For that you will need to create a `.netrc` file in your `$HOME` and restrict permissions to read/write by you only:
|
||||||
```
|
```
|
||||||
touch $HOME/.netrc
|
touch $HOME/.netrc
|
||||||
chmod a-rwx,u+rw $HOME/.netrc
|
chmod a-rwx,u+rw $HOME/.netrc
|
||||||
@@ -504,6 +511,9 @@ The basic usage is not to set any template arguments when downloading a single f
|
|||||||
- `autonumber`: Five-digit number that will be increased with each download, starting at zero
|
- `autonumber`: Five-digit number that will be increased with each download, starting at zero
|
||||||
- `playlist`: Name or id of the playlist that contains the video
|
- `playlist`: Name or id of the playlist that contains the video
|
||||||
- `playlist_index`: Index of the video in the playlist padded with leading zeros according to the total length of the playlist
|
- `playlist_index`: Index of the video in the playlist padded with leading zeros according to the total length of the playlist
|
||||||
|
- `playlist_id`: Playlist identifier
|
||||||
|
- `playlist_title`: Playlist title
|
||||||
|
|
||||||
|
|
||||||
Available for the video that belongs to some logical chapter or section:
|
Available for the video that belongs to some logical chapter or section:
|
||||||
- `chapter`: Name or title of the chapter the video belongs to
|
- `chapter`: Name or title of the chapter the video belongs to
|
||||||
@@ -543,6 +553,10 @@ The current default template is `%(title)s-%(id)s.%(ext)s`.
|
|||||||
|
|
||||||
In some cases, you don't want special characters such as 中, spaces, or &, such as when transferring the downloaded filename to a Windows system or the filename through an 8bit-unsafe channel. In these cases, add the `--restrict-filenames` flag to get a shorter title:
|
In some cases, you don't want special characters such as 中, spaces, or &, such as when transferring the downloaded filename to a Windows system or the filename through an 8bit-unsafe channel. In these cases, add the `--restrict-filenames` flag to get a shorter title:
|
||||||
|
|
||||||
|
#### Output template and Windows batch files
|
||||||
|
|
||||||
|
If you are using output template inside a Windows batch file then you must escape plain percent characters (`%`) by doubling, so that `-o "%(title)s-%(id)s.%(ext)s"` should become `-o "%%(title)s-%%(id)s.%%(ext)s"`. However you should not touch `%`'s that are not plain characters, e.g. environment variables for expansion should stay intact: `-o "C:\%HOMEPATH%\Desktop\%%(title)s.%%(ext)s"`.
|
||||||
|
|
||||||
#### Output template examples
|
#### Output template examples
|
||||||
|
|
||||||
Note on Windows you may need to use double quotes instead of single.
|
Note on Windows you may need to use double quotes instead of single.
|
||||||
@@ -693,6 +707,10 @@ hash -r
|
|||||||
|
|
||||||
Again, from then on you'll be able to update with `sudo youtube-dl -U`.
|
Again, from then on you'll be able to update with `sudo youtube-dl -U`.
|
||||||
|
|
||||||
|
### youtube-dl is extremely slow to start on Windows
|
||||||
|
|
||||||
|
Add a file exclusion for `youtube-dl.exe` in Windows Defender settings.
|
||||||
|
|
||||||
### I'm getting an error `Unable to extract OpenGraph title` on YouTube playlists
|
### I'm getting an error `Unable to extract OpenGraph title` on YouTube playlists
|
||||||
|
|
||||||
YouTube changed their playlist format in March 2014 and later on, so you'll need at least youtube-dl 2014.07.25 to download all YouTube videos.
|
YouTube changed their playlist format in March 2014 and later on, so you'll need at least youtube-dl 2014.07.25 to download all YouTube videos.
|
||||||
@@ -780,9 +798,9 @@ means you're using an outdated version of Python. Please update to Python 2.6 or
|
|||||||
|
|
||||||
Since June 2012 ([#342](https://github.com/rg3/youtube-dl/issues/342)) youtube-dl is packed as an executable zipfile, simply unzip it (might need renaming to `youtube-dl.zip` first on some systems) or clone the git repository, as laid out above. If you modify the code, you can run it by executing the `__main__.py` file. To recompile the executable, run `make youtube-dl`.
|
Since June 2012 ([#342](https://github.com/rg3/youtube-dl/issues/342)) youtube-dl is packed as an executable zipfile, simply unzip it (might need renaming to `youtube-dl.zip` first on some systems) or clone the git repository, as laid out above. If you modify the code, you can run it by executing the `__main__.py` file. To recompile the executable, run `make youtube-dl`.
|
||||||
|
|
||||||
### The exe throws a *Runtime error from Visual C++*
|
### The exe throws an error due to missing `MSVCR100.dll`
|
||||||
|
|
||||||
To run the exe you need to install first the [Microsoft Visual C++ 2008 Redistributable Package](http://www.microsoft.com/en-us/download/details.aspx?id=29).
|
To run the exe you need to install first the [Microsoft Visual C++ 2010 Redistributable Package (x86)](https://www.microsoft.com/en-US/download/details.aspx?id=5555).
|
||||||
|
|
||||||
### On Windows, how should I set up ffmpeg and youtube-dl? Where should I put the exe files?
|
### On Windows, how should I set up ffmpeg and youtube-dl? Where should I put the exe files?
|
||||||
|
|
||||||
@@ -837,6 +855,12 @@ It is *not* possible to detect whether a URL is supported or not. That's because
|
|||||||
|
|
||||||
If you want to find out whether a given URL is supported, simply call youtube-dl with it. If you get no videos back, chances are the URL is either not referring to a video or unsupported. You can find out which by examining the output (if you run youtube-dl on the console) or catching an `UnsupportedError` exception if you run it from a Python program.
|
If you want to find out whether a given URL is supported, simply call youtube-dl with it. If you get no videos back, chances are the URL is either not referring to a video or unsupported. You can find out which by examining the output (if you run youtube-dl on the console) or catching an `UnsupportedError` exception if you run it from a Python program.
|
||||||
|
|
||||||
|
# Why do I need to go through that much red tape when filing bugs?
|
||||||
|
|
||||||
|
Before we had the issue template, despite our extensive [bug reporting instructions](#bugs), about 80% of the issue reports we got were useless, for instance because people used ancient versions hundreds of releases old, because of simple syntactic errors (not in youtube-dl but in general shell usage), because the problem was alrady reported multiple times before, because people did not actually read an error message, even if it said "please install ffmpeg", because people did not mention the URL they were trying to download and many more simple, easy-to-avoid problems, many of whom were totally unrelated to youtube-dl.
|
||||||
|
|
||||||
|
youtube-dl is an open-source project manned by too few volunteers, so we'd rather spend time fixing bugs where we are certain none of those simple problems apply, and where we can be reasonably confident to be able to reproduce the issue without asking the reporter repeatedly. As such, the output of `youtube-dl -v YOUR_URL_HERE` is really all that's required to file an issue. The issue template also guides you through some basic steps you can do, such as checking that your version of youtube-dl is current.
|
||||||
|
|
||||||
# DEVELOPER INSTRUCTIONS
|
# DEVELOPER INSTRUCTIONS
|
||||||
|
|
||||||
Most users do not need to build youtube-dl and can [download the builds](http://rg3.github.io/youtube-dl/download.html) or get them from their distribution.
|
Most users do not need to build youtube-dl and can [download the builds](http://rg3.github.io/youtube-dl/download.html) or get them from their distribution.
|
||||||
@@ -911,9 +935,9 @@ After you have ensured this site is distributing it's content legally, you can f
|
|||||||
```
|
```
|
||||||
5. Add an import in [`youtube_dl/extractor/extractors.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/extractors.py).
|
5. Add an import in [`youtube_dl/extractor/extractors.py`](https://github.com/rg3/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.
|
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.
|
||||||
7. Have a look at [`youtube_dl/extractor/common.py`](https://github.com/rg3/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/rg3/youtube-dl/blob/58525c94d547be1c8167d16c298bdd75506db328/youtube_dl/extractor/common.py#L68-L226). Add tests and code for as many as you want.
|
7. Have a look at [`youtube_dl/extractor/common.py`](https://github.com/rg3/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/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py#L74-L252). Add tests and code for as many as you want.
|
||||||
8. Keep in mind that the only mandatory fields in info dict for successful extraction process are `id`, `title` and either `url` or `formats`, i.e. these are the critical data the extraction does not make any sense without. This means that [any field](https://github.com/rg3/youtube-dl/blob/58525c94d547be1c8167d16c298bdd75506db328/youtube_dl/extractor/common.py#L138-L226) apart from aforementioned mandatory ones should be treated **as optional** and extraction should be **tolerate** to situations when sources for these fields can potentially be unavailable (even if they always available at the moment) and **future-proof** in order not to break the extraction of general purpose mandatory fields. For example, if you have some intermediate dict `meta` that is a source of metadata and it has a key `summary` that you want to extract and put into resulting info dict as `description`, you should be ready that this key may be missing from the `meta` dict, i.e. you should extract it as `meta.get('summary')` and not `meta['summary']`. Similarly, you should pass `fatal=False` when extracting data from a webpage with `_search_regex/_html_search_regex`.
|
8. Keep in mind that the only mandatory fields in info dict for successful extraction process are `id`, `title` and either `url` or `formats`, i.e. these are the critical data the extraction does not make any sense without. This means that [any field](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py#L148-L252) apart from aforementioned mandatory ones should be treated **as optional** and extraction should be **tolerate** to situations when sources for these fields can potentially be unavailable (even if they always available at the moment) and **future-proof** in order not to break the extraction of general purpose mandatory fields. For example, if you have some intermediate dict `meta` that is a source of metadata and it has a key `summary` that you want to extract and put into resulting info dict as `description`, you should be ready that this key may be missing from the `meta` dict, i.e. you should extract it as `meta.get('summary')` and not `meta['summary']`. Similarly, you should pass `fatal=False` when extracting data from a webpage with `_search_regex/_html_search_regex`.
|
||||||
9. Check the code with [flake8](https://pypi.python.org/pypi/flake8).
|
9. Check the code with [flake8](https://pypi.python.org/pypi/flake8). Also make sure your code works under all [Python](http://www.python.org/) versions claimed supported by youtube-dl, namely 2.6, 2.7, and 3.2+.
|
||||||
10. When the tests pass, [add](http://git-scm.com/docs/git-add) the new files and [commit](http://git-scm.com/docs/git-commit) them and [push](http://git-scm.com/docs/git-push) the result, like this:
|
10. When the tests pass, [add](http://git-scm.com/docs/git-add) the new files and [commit](http://git-scm.com/docs/git-commit) them and [push](http://git-scm.com/docs/git-push) the result, like this:
|
||||||
|
|
||||||
$ git add youtube_dl/extractor/extractors.py
|
$ git add youtube_dl/extractor/extractors.py
|
||||||
@@ -940,7 +964,7 @@ with youtube_dl.YoutubeDL(ydl_opts) as ydl:
|
|||||||
ydl.download(['http://www.youtube.com/watch?v=BaW_jenozKc'])
|
ydl.download(['http://www.youtube.com/watch?v=BaW_jenozKc'])
|
||||||
```
|
```
|
||||||
|
|
||||||
Most likely, you'll want to use various options. For a list of what can be done, have a look at [`youtube_dl/YoutubeDL.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/YoutubeDL.py#L121-L269). For a start, if you want to intercept youtube-dl's output, set a `logger` object.
|
Most likely, you'll want to use various options. For a list of options available, have a look at [`youtube_dl/YoutubeDL.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/YoutubeDL.py#L128-L278). For a start, if you want to intercept youtube-dl's output, set a `logger` object.
|
||||||
|
|
||||||
Here's a more complete example of a program that outputs only errors (and a short message after the download is finished), and downloads/converts the video to an mp3 file:
|
Here's a more complete example of a program that outputs only errors (and a short message after the download is finished), and downloads/converts the video to an mp3 file:
|
||||||
|
|
||||||
|
@@ -1,17 +1,38 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
||||||
from socketserver import ThreadingMixIn
|
|
||||||
import argparse
|
import argparse
|
||||||
import ctypes
|
import ctypes
|
||||||
import functools
|
import functools
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname((os.path.abspath(__file__)))))
|
||||||
|
from youtube_dl.compat import (
|
||||||
|
compat_input,
|
||||||
|
compat_http_server,
|
||||||
|
compat_str,
|
||||||
|
compat_urlparse,
|
||||||
|
)
|
||||||
|
|
||||||
class BuildHTTPServer(ThreadingMixIn, HTTPServer):
|
# These are not used outside of buildserver.py thus not in compat.py
|
||||||
|
|
||||||
|
try:
|
||||||
|
import winreg as compat_winreg
|
||||||
|
except ImportError: # Python 2
|
||||||
|
import _winreg as compat_winreg
|
||||||
|
|
||||||
|
try:
|
||||||
|
import socketserver as compat_socketserver
|
||||||
|
except ImportError: # Python 2
|
||||||
|
import SocketServer as compat_socketserver
|
||||||
|
|
||||||
|
|
||||||
|
class BuildHTTPServer(compat_socketserver.ThreadingMixIn, compat_http_server.HTTPServer):
|
||||||
allow_reuse_address = True
|
allow_reuse_address = True
|
||||||
|
|
||||||
|
|
||||||
@@ -191,7 +212,7 @@ def main(args=None):
|
|||||||
action='store_const', dest='action', const='service',
|
action='store_const', dest='action', const='service',
|
||||||
help='Run as a Windows service')
|
help='Run as a Windows service')
|
||||||
parser.add_argument('-b', '--bind', metavar='<host:port>',
|
parser.add_argument('-b', '--bind', metavar='<host:port>',
|
||||||
action='store', default='localhost:8142',
|
action='store', default='0.0.0.0:8142',
|
||||||
help='Bind to host:port (default %default)')
|
help='Bind to host:port (default %default)')
|
||||||
options = parser.parse_args(args=args)
|
options = parser.parse_args(args=args)
|
||||||
|
|
||||||
@@ -216,7 +237,7 @@ def main(args=None):
|
|||||||
srv = BuildHTTPServer((host, port), BuildHTTPRequestHandler)
|
srv = BuildHTTPServer((host, port), BuildHTTPRequestHandler)
|
||||||
thr = threading.Thread(target=srv.serve_forever)
|
thr = threading.Thread(target=srv.serve_forever)
|
||||||
thr.start()
|
thr.start()
|
||||||
input('Press ENTER to shut down')
|
compat_input('Press ENTER to shut down')
|
||||||
srv.shutdown()
|
srv.shutdown()
|
||||||
thr.join()
|
thr.join()
|
||||||
|
|
||||||
@@ -231,8 +252,6 @@ def rmtree(path):
|
|||||||
os.remove(fname)
|
os.remove(fname)
|
||||||
os.rmdir(path)
|
os.rmdir(path)
|
||||||
|
|
||||||
#==============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
class BuildError(Exception):
|
class BuildError(Exception):
|
||||||
def __init__(self, output, code=500):
|
def __init__(self, output, code=500):
|
||||||
@@ -249,15 +268,25 @@ class HTTPError(BuildError):
|
|||||||
|
|
||||||
class PythonBuilder(object):
|
class PythonBuilder(object):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
pythonVersion = kwargs.pop('python', '2.7')
|
python_version = kwargs.pop('python', '3.4')
|
||||||
try:
|
python_path = None
|
||||||
key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\Python\PythonCore\%s\InstallPath' % pythonVersion)
|
for node in ('Wow6432Node\\', ''):
|
||||||
try:
|
try:
|
||||||
self.pythonPath, _ = _winreg.QueryValueEx(key, '')
|
key = compat_winreg.OpenKey(
|
||||||
finally:
|
compat_winreg.HKEY_LOCAL_MACHINE,
|
||||||
_winreg.CloseKey(key)
|
r'SOFTWARE\%sPython\PythonCore\%s\InstallPath' % (node, python_version))
|
||||||
except Exception:
|
try:
|
||||||
raise BuildError('No such Python version: %s' % pythonVersion)
|
python_path, _ = compat_winreg.QueryValueEx(key, '')
|
||||||
|
finally:
|
||||||
|
compat_winreg.CloseKey(key)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not python_path:
|
||||||
|
raise BuildError('No such Python version: %s' % python_version)
|
||||||
|
|
||||||
|
self.pythonPath = python_path
|
||||||
|
|
||||||
super(PythonBuilder, self).__init__(**kwargs)
|
super(PythonBuilder, self).__init__(**kwargs)
|
||||||
|
|
||||||
@@ -305,8 +334,10 @@ class YoutubeDLBuilder(object):
|
|||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
try:
|
try:
|
||||||
subprocess.check_output([os.path.join(self.pythonPath, 'python.exe'), 'setup.py', 'py2exe'],
|
proc = subprocess.Popen([os.path.join(self.pythonPath, 'python.exe'), 'setup.py', 'py2exe'], stdin=subprocess.PIPE, cwd=self.buildPath)
|
||||||
cwd=self.buildPath)
|
proc.wait()
|
||||||
|
#subprocess.check_output([os.path.join(self.pythonPath, 'python.exe'), 'setup.py', 'py2exe'],
|
||||||
|
# cwd=self.buildPath)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
raise BuildError(e.output)
|
raise BuildError(e.output)
|
||||||
|
|
||||||
@@ -369,12 +400,12 @@ class Builder(PythonBuilder, GITBuilder, YoutubeDLBuilder, DownloadBuilder, Clea
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BuildHTTPRequestHandler(BaseHTTPRequestHandler):
|
class BuildHTTPRequestHandler(compat_http_server.BaseHTTPRequestHandler):
|
||||||
actionDict = {'build': Builder, 'download': Builder} # They're the same, no more caching.
|
actionDict = {'build': Builder, 'download': Builder} # They're the same, no more caching.
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
path = urlparse.urlparse(self.path)
|
path = compat_urlparse.urlparse(self.path)
|
||||||
paramDict = dict([(key, value[0]) for key, value in urlparse.parse_qs(path.query).items()])
|
paramDict = dict([(key, value[0]) for key, value in compat_urlparse.parse_qs(path.query).items()])
|
||||||
action, _, path = path.path.strip('/').partition('/')
|
action, _, path = path.path.strip('/').partition('/')
|
||||||
if path:
|
if path:
|
||||||
path = path.split('/')
|
path = path.split('/')
|
||||||
@@ -388,7 +419,7 @@ class BuildHTTPRequestHandler(BaseHTTPRequestHandler):
|
|||||||
builder.close()
|
builder.close()
|
||||||
except BuildError as e:
|
except BuildError as e:
|
||||||
self.send_response(e.code)
|
self.send_response(e.code)
|
||||||
msg = unicode(e).encode('UTF-8')
|
msg = compat_str(e).encode('UTF-8')
|
||||||
self.send_header('Content-Type', 'text/plain; charset=UTF-8')
|
self.send_header('Content-Type', 'text/plain; charset=UTF-8')
|
||||||
self.send_header('Content-Length', len(msg))
|
self.send_header('Content-Length', len(msg))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
@@ -400,7 +431,5 @@ class BuildHTTPRequestHandler(BaseHTTPRequestHandler):
|
|||||||
else:
|
else:
|
||||||
self.send_response(500, 'Malformed URL')
|
self.send_response(500, 'Malformed URL')
|
||||||
|
|
||||||
#==============================================================================
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
111
devscripts/create-github-release.py
Normal file
111
devscripts/create-github-release.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import mimetypes
|
||||||
|
import netrc
|
||||||
|
import optparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from youtube_dl.compat import (
|
||||||
|
compat_basestring,
|
||||||
|
compat_input,
|
||||||
|
compat_getpass,
|
||||||
|
compat_print,
|
||||||
|
compat_urllib_request,
|
||||||
|
)
|
||||||
|
from youtube_dl.utils import (
|
||||||
|
make_HTTPS_handler,
|
||||||
|
sanitized_Request,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GitHubReleaser(object):
|
||||||
|
_API_URL = 'https://api.github.com/repos/rg3/youtube-dl/releases'
|
||||||
|
_UPLOADS_URL = 'https://uploads.github.com/repos/rg3/youtube-dl/releases/%s/assets?name=%s'
|
||||||
|
_NETRC_MACHINE = 'github.com'
|
||||||
|
|
||||||
|
def __init__(self, debuglevel=0):
|
||||||
|
self._init_github_account()
|
||||||
|
https_handler = make_HTTPS_handler({}, debuglevel=debuglevel)
|
||||||
|
self._opener = compat_urllib_request.build_opener(https_handler)
|
||||||
|
|
||||||
|
def _init_github_account(self):
|
||||||
|
try:
|
||||||
|
info = netrc.netrc().authenticators(self._NETRC_MACHINE)
|
||||||
|
if info is not None:
|
||||||
|
self._username = info[0]
|
||||||
|
self._password = info[2]
|
||||||
|
compat_print('Using GitHub credentials found in .netrc...')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
compat_print('No GitHub credentials found in .netrc')
|
||||||
|
except (IOError, netrc.NetrcParseError):
|
||||||
|
compat_print('Unable to parse .netrc')
|
||||||
|
self._username = compat_input(
|
||||||
|
'Type your GitHub username or email address and press [Return]: ')
|
||||||
|
self._password = compat_getpass(
|
||||||
|
'Type your GitHub password and press [Return]: ')
|
||||||
|
|
||||||
|
def _call(self, req):
|
||||||
|
if isinstance(req, compat_basestring):
|
||||||
|
req = sanitized_Request(req)
|
||||||
|
# Authorizing manually since GitHub does not response with 401 with
|
||||||
|
# WWW-Authenticate header set (see
|
||||||
|
# https://developer.github.com/v3/#basic-authentication)
|
||||||
|
b64 = base64.b64encode(
|
||||||
|
('%s:%s' % (self._username, self._password)).encode('utf-8')).decode('ascii')
|
||||||
|
req.add_header('Authorization', 'Basic %s' % b64)
|
||||||
|
response = self._opener.open(req).read().decode('utf-8')
|
||||||
|
return json.loads(response)
|
||||||
|
|
||||||
|
def list_releases(self):
|
||||||
|
return self._call(self._API_URL)
|
||||||
|
|
||||||
|
def create_release(self, tag_name, name=None, body='', draft=False, prerelease=False):
|
||||||
|
data = {
|
||||||
|
'tag_name': tag_name,
|
||||||
|
'target_commitish': 'master',
|
||||||
|
'name': name,
|
||||||
|
'body': body,
|
||||||
|
'draft': draft,
|
||||||
|
'prerelease': prerelease,
|
||||||
|
}
|
||||||
|
req = sanitized_Request(self._API_URL, json.dumps(data).encode('utf-8'))
|
||||||
|
return self._call(req)
|
||||||
|
|
||||||
|
def create_asset(self, release_id, asset):
|
||||||
|
asset_name = os.path.basename(asset)
|
||||||
|
url = self._UPLOADS_URL % (release_id, asset_name)
|
||||||
|
# Our files are small enough to be loaded directly into memory.
|
||||||
|
data = open(asset, 'rb').read()
|
||||||
|
req = sanitized_Request(url, data)
|
||||||
|
mime_type, _ = mimetypes.guess_type(asset_name)
|
||||||
|
req.add_header('Content-Type', mime_type or 'application/octet-stream')
|
||||||
|
return self._call(req)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = optparse.OptionParser(usage='%prog VERSION BUILDPATH')
|
||||||
|
options, args = parser.parse_args()
|
||||||
|
if len(args) != 2:
|
||||||
|
parser.error('Expected a version and a build directory')
|
||||||
|
|
||||||
|
version, build_path = args
|
||||||
|
|
||||||
|
releaser = GitHubReleaser()
|
||||||
|
|
||||||
|
new_release = releaser.create_release(version, name='youtube-dl %s' % version)
|
||||||
|
release_id = new_release['id']
|
||||||
|
|
||||||
|
for asset in os.listdir(build_path):
|
||||||
|
compat_print('Uploading %s...' % asset)
|
||||||
|
releaser.create_asset(release_id, os.path.join(build_path, asset))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@@ -1,13 +1,46 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
import optparse
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
README_FILE = os.path.join(ROOT_DIR, 'README.md')
|
README_FILE = os.path.join(ROOT_DIR, 'README.md')
|
||||||
|
|
||||||
|
PREFIX = '''%YOUTUBE-DL(1)
|
||||||
|
|
||||||
|
# NAME
|
||||||
|
|
||||||
|
youtube\-dl \- download videos from youtube.com or other video platforms
|
||||||
|
|
||||||
|
# SYNOPSIS
|
||||||
|
|
||||||
|
**youtube-dl** \[OPTIONS\] URL [URL...]
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = optparse.OptionParser(usage='%prog OUTFILE.md')
|
||||||
|
options, args = parser.parse_args()
|
||||||
|
if len(args) != 1:
|
||||||
|
parser.error('Expected an output filename')
|
||||||
|
|
||||||
|
outfile, = args
|
||||||
|
|
||||||
|
with io.open(README_FILE, encoding='utf-8') as f:
|
||||||
|
readme = f.read()
|
||||||
|
|
||||||
|
readme = re.sub(r'(?s)^.*?(?=# DESCRIPTION)', '', readme)
|
||||||
|
readme = re.sub(r'\s+youtube-dl \[OPTIONS\] URL \[URL\.\.\.\]', '', readme)
|
||||||
|
readme = PREFIX + readme
|
||||||
|
|
||||||
|
readme = filter_options(readme)
|
||||||
|
|
||||||
|
with io.open(outfile, 'w', encoding='utf-8') as outf:
|
||||||
|
outf.write(readme)
|
||||||
|
|
||||||
|
|
||||||
def filter_options(readme):
|
def filter_options(readme):
|
||||||
ret = ''
|
ret = ''
|
||||||
@@ -37,27 +70,5 @@ def filter_options(readme):
|
|||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
with io.open(README_FILE, encoding='utf-8') as f:
|
if __name__ == '__main__':
|
||||||
readme = f.read()
|
main()
|
||||||
|
|
||||||
PREFIX = '''%YOUTUBE-DL(1)
|
|
||||||
|
|
||||||
# NAME
|
|
||||||
|
|
||||||
youtube\-dl \- download videos from youtube.com or other video platforms
|
|
||||||
|
|
||||||
# SYNOPSIS
|
|
||||||
|
|
||||||
**youtube-dl** \[OPTIONS\] URL [URL...]
|
|
||||||
|
|
||||||
'''
|
|
||||||
readme = re.sub(r'(?s)^.*?(?=# DESCRIPTION)', '', readme)
|
|
||||||
readme = re.sub(r'\s+youtube-dl \[OPTIONS\] URL \[URL\.\.\.\]', '', readme)
|
|
||||||
readme = PREFIX + readme
|
|
||||||
|
|
||||||
readme = filter_options(readme)
|
|
||||||
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
print(readme.encode('utf-8'))
|
|
||||||
else:
|
|
||||||
print(readme)
|
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
# * the git config user.signingkey is properly set
|
# * the git config user.signingkey is properly set
|
||||||
|
|
||||||
# You will need
|
# You will need
|
||||||
# pip install coverage nose rsa
|
# pip install coverage nose rsa wheel
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
# release notes
|
# release notes
|
||||||
@@ -15,10 +15,33 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
skip_tests=true
|
skip_tests=true
|
||||||
if [ "$1" = '--run-tests' ]; then
|
gpg_sign_commits=""
|
||||||
skip_tests=false
|
buildserver='localhost:8142'
|
||||||
shift
|
|
||||||
fi
|
while true
|
||||||
|
do
|
||||||
|
case "$1" in
|
||||||
|
--run-tests)
|
||||||
|
skip_tests=false
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--gpg-sign-commits|-S)
|
||||||
|
gpg_sign_commits="-S"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--buildserver)
|
||||||
|
buildserver="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--*)
|
||||||
|
echo "ERROR: unknown option $1"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
if [ -z "$1" ]; then echo "ERROR: specify version number like this: $0 1994.09.06"; exit 1; fi
|
if [ -z "$1" ]; then echo "ERROR: specify version number like this: $0 1994.09.06"; exit 1; fi
|
||||||
version="$1"
|
version="$1"
|
||||||
@@ -33,7 +56,9 @@ if [ ! -z "`git status --porcelain | grep -v CHANGELOG`" ]; then echo 'ERROR: th
|
|||||||
useless_files=$(find youtube_dl -type f -not -name '*.py')
|
useless_files=$(find youtube_dl -type f -not -name '*.py')
|
||||||
if [ ! -z "$useless_files" ]; then echo "ERROR: Non-.py files in youtube_dl: $useless_files"; exit 1; fi
|
if [ ! -z "$useless_files" ]; then echo "ERROR: Non-.py files in youtube_dl: $useless_files"; exit 1; fi
|
||||||
if [ ! -f "updates_key.pem" ]; then echo 'ERROR: updates_key.pem missing'; exit 1; fi
|
if [ ! -f "updates_key.pem" ]; then echo 'ERROR: updates_key.pem missing'; exit 1; fi
|
||||||
if ! type pandoc 2>/dev/null; then echo 'ERROR: pandoc is missing'; exit 1; fi
|
if ! type pandoc >/dev/null 2>/dev/null; then echo 'ERROR: pandoc is missing'; exit 1; fi
|
||||||
|
if ! python3 -c 'import rsa' 2>/dev/null; then echo 'ERROR: python3-rsa is missing'; exit 1; fi
|
||||||
|
if ! python3 -c 'import wheel' 2>/dev/null; then echo 'ERROR: wheel is missing'; exit 1; fi
|
||||||
|
|
||||||
/bin/echo -e "\n### First of all, testing..."
|
/bin/echo -e "\n### First of all, testing..."
|
||||||
make clean
|
make clean
|
||||||
@@ -49,7 +74,7 @@ sed -i "s/__version__ = '.*'/__version__ = '$version'/" youtube_dl/version.py
|
|||||||
/bin/echo -e "\n### Committing documentation, templates and youtube_dl/version.py..."
|
/bin/echo -e "\n### Committing documentation, templates and youtube_dl/version.py..."
|
||||||
make README.md CONTRIBUTING.md .github/ISSUE_TEMPLATE.md supportedsites
|
make README.md CONTRIBUTING.md .github/ISSUE_TEMPLATE.md supportedsites
|
||||||
git add README.md CONTRIBUTING.md .github/ISSUE_TEMPLATE.md docs/supportedsites.md youtube_dl/version.py
|
git add README.md CONTRIBUTING.md .github/ISSUE_TEMPLATE.md docs/supportedsites.md youtube_dl/version.py
|
||||||
git commit -m "release $version"
|
git commit $gpg_sign_commits -m "release $version"
|
||||||
|
|
||||||
/bin/echo -e "\n### Now tagging, signing and pushing..."
|
/bin/echo -e "\n### Now tagging, signing and pushing..."
|
||||||
git tag -s -m "Release $version" "$version"
|
git tag -s -m "Release $version" "$version"
|
||||||
@@ -65,7 +90,7 @@ git push origin "$version"
|
|||||||
REV=$(git rev-parse HEAD)
|
REV=$(git rev-parse HEAD)
|
||||||
make youtube-dl youtube-dl.tar.gz
|
make youtube-dl youtube-dl.tar.gz
|
||||||
read -p "VM running? (y/n) " -n 1
|
read -p "VM running? (y/n) " -n 1
|
||||||
wget "http://localhost:8142/build/rg3/youtube-dl/youtube-dl.exe?rev=$REV" -O youtube-dl.exe
|
wget "http://$buildserver/build/rg3/youtube-dl/youtube-dl.exe?rev=$REV" -O youtube-dl.exe
|
||||||
mkdir -p "build/$version"
|
mkdir -p "build/$version"
|
||||||
mv youtube-dl youtube-dl.exe "build/$version"
|
mv youtube-dl youtube-dl.exe "build/$version"
|
||||||
mv youtube-dl.tar.gz "build/$version/youtube-dl-$version.tar.gz"
|
mv youtube-dl.tar.gz "build/$version/youtube-dl-$version.tar.gz"
|
||||||
@@ -75,15 +100,16 @@ RELEASE_FILES="youtube-dl youtube-dl.exe youtube-dl-$version.tar.gz"
|
|||||||
(cd build/$version/ && sha256sum $RELEASE_FILES > SHA2-256SUMS)
|
(cd build/$version/ && sha256sum $RELEASE_FILES > SHA2-256SUMS)
|
||||||
(cd build/$version/ && sha512sum $RELEASE_FILES > SHA2-512SUMS)
|
(cd build/$version/ && sha512sum $RELEASE_FILES > SHA2-512SUMS)
|
||||||
|
|
||||||
/bin/echo -e "\n### Signing and uploading the new binaries to yt-dl.org ..."
|
/bin/echo -e "\n### Signing and uploading the new binaries to GitHub..."
|
||||||
for f in $RELEASE_FILES; do gpg --passphrase-repeat 5 --detach-sig "build/$version/$f"; done
|
for f in $RELEASE_FILES; do gpg --passphrase-repeat 5 --detach-sig "build/$version/$f"; done
|
||||||
scp -r "build/$version" ytdl@yt-dl.org:html/tmp/
|
|
||||||
ssh ytdl@yt-dl.org "mv html/tmp/$version html/downloads/"
|
ROOT=$(pwd)
|
||||||
|
python devscripts/create-github-release.py $version "$ROOT/build/$version"
|
||||||
|
|
||||||
ssh ytdl@yt-dl.org "sh html/update_latest.sh $version"
|
ssh ytdl@yt-dl.org "sh html/update_latest.sh $version"
|
||||||
|
|
||||||
/bin/echo -e "\n### Now switching to gh-pages..."
|
/bin/echo -e "\n### Now switching to gh-pages..."
|
||||||
git clone --branch gh-pages --single-branch . build/gh-pages
|
git clone --branch gh-pages --single-branch . build/gh-pages
|
||||||
ROOT=$(pwd)
|
|
||||||
(
|
(
|
||||||
set -e
|
set -e
|
||||||
ORIGIN_URL=$(git config --get remote.origin.url)
|
ORIGIN_URL=$(git config --get remote.origin.url)
|
||||||
@@ -95,7 +121,7 @@ ROOT=$(pwd)
|
|||||||
"$ROOT/devscripts/gh-pages/update-copyright.py"
|
"$ROOT/devscripts/gh-pages/update-copyright.py"
|
||||||
"$ROOT/devscripts/gh-pages/update-sites.py"
|
"$ROOT/devscripts/gh-pages/update-sites.py"
|
||||||
git add *.html *.html.in update
|
git add *.html *.html.in update
|
||||||
git commit -m "release $version"
|
git commit $gpg_sign_commits -m "release $version"
|
||||||
git push "$ROOT" gh-pages
|
git push "$ROOT" gh-pages
|
||||||
git push "$ORIGIN_URL" gh-pages
|
git push "$ORIGIN_URL" gh-pages
|
||||||
)
|
)
|
||||||
|
@@ -28,6 +28,7 @@
|
|||||||
- **AdobeTVVideo**
|
- **AdobeTVVideo**
|
||||||
- **AdultSwim**
|
- **AdultSwim**
|
||||||
- **aenetworks**: A+E Networks: A&E, Lifetime, History.com, FYI Network
|
- **aenetworks**: A+E Networks: A&E, Lifetime, History.com, FYI Network
|
||||||
|
- **AfreecaTV**: afreecatv.com
|
||||||
- **Aftonbladet**
|
- **Aftonbladet**
|
||||||
- **AirMozilla**
|
- **AirMozilla**
|
||||||
- **AlJazeera**
|
- **AlJazeera**
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
- **arte.tv:future**
|
- **arte.tv:future**
|
||||||
- **arte.tv:info**
|
- **arte.tv:info**
|
||||||
- **arte.tv:magazine**
|
- **arte.tv:magazine**
|
||||||
|
- **arte.tv:playlist**
|
||||||
- **AtresPlayer**
|
- **AtresPlayer**
|
||||||
- **ATTTechChannel**
|
- **ATTTechChannel**
|
||||||
- **AudiMedia**
|
- **AudiMedia**
|
||||||
@@ -72,6 +74,8 @@
|
|||||||
- **bbc**: BBC
|
- **bbc**: BBC
|
||||||
- **bbc.co.uk**: BBC iPlayer
|
- **bbc.co.uk**: BBC iPlayer
|
||||||
- **bbc.co.uk:article**: BBC articles
|
- **bbc.co.uk:article**: BBC articles
|
||||||
|
- **bbc.co.uk:iplayer:playlist**
|
||||||
|
- **bbc.co.uk:playlist**
|
||||||
- **BeatportPro**
|
- **BeatportPro**
|
||||||
- **Beeg**
|
- **Beeg**
|
||||||
- **BehindKink**
|
- **BehindKink**
|
||||||
@@ -102,6 +106,8 @@
|
|||||||
- **canalc2.tv**
|
- **canalc2.tv**
|
||||||
- **Canalplus**: canalplus.fr, piwiplus.fr and d8.tv
|
- **Canalplus**: canalplus.fr, piwiplus.fr and d8.tv
|
||||||
- **Canvas**
|
- **Canvas**
|
||||||
|
- **CarambaTV**
|
||||||
|
- **CarambaTVPage**
|
||||||
- **CBC**
|
- **CBC**
|
||||||
- **CBCPlayer**
|
- **CBCPlayer**
|
||||||
- **CBS**
|
- **CBS**
|
||||||
@@ -122,6 +128,7 @@
|
|||||||
- **cliphunter**
|
- **cliphunter**
|
||||||
- **ClipRs**
|
- **ClipRs**
|
||||||
- **Clipsyndicate**
|
- **Clipsyndicate**
|
||||||
|
- **CloserToTruth**
|
||||||
- **cloudtime**: CloudTime
|
- **cloudtime**: CloudTime
|
||||||
- **Cloudy**
|
- **Cloudy**
|
||||||
- **Clubic**
|
- **Clubic**
|
||||||
@@ -136,6 +143,7 @@
|
|||||||
- **ComedyCentral**
|
- **ComedyCentral**
|
||||||
- **ComedyCentralShows**: The Daily Show / The Colbert Report
|
- **ComedyCentralShows**: The Daily Show / The Colbert Report
|
||||||
- **CondeNast**: Condé Nast media group: Allure, Architectural Digest, Ars Technica, Bon Appétit, Brides, Condé Nast, Condé Nast Traveler, Details, Epicurious, GQ, Glamour, Golf Digest, SELF, Teen Vogue, The New Yorker, Vanity Fair, Vogue, W Magazine, WIRED
|
- **CondeNast**: Condé Nast media group: Allure, Architectural Digest, Ars Technica, Bon Appétit, Brides, Condé Nast, Condé Nast Traveler, Details, Epicurious, GQ, Glamour, Golf Digest, SELF, Teen Vogue, The New Yorker, Vanity Fair, Vogue, W Magazine, WIRED
|
||||||
|
- **Coub**
|
||||||
- **Cracked**
|
- **Cracked**
|
||||||
- **Crackle**
|
- **Crackle**
|
||||||
- **Criterion**
|
- **Criterion**
|
||||||
@@ -205,6 +213,7 @@
|
|||||||
- **exfm**: ex.fm
|
- **exfm**: ex.fm
|
||||||
- **ExpoTV**
|
- **ExpoTV**
|
||||||
- **ExtremeTube**
|
- **ExtremeTube**
|
||||||
|
- **EyedoTV**
|
||||||
- **facebook**
|
- **facebook**
|
||||||
- **faz.net**
|
- **faz.net**
|
||||||
- **fc2**
|
- **fc2**
|
||||||
@@ -250,6 +259,7 @@
|
|||||||
- **Globo**
|
- **Globo**
|
||||||
- **GloboArticle**
|
- **GloboArticle**
|
||||||
- **GodTube**
|
- **GodTube**
|
||||||
|
- **GodTV**
|
||||||
- **GoldenMoustache**
|
- **GoldenMoustache**
|
||||||
- **Golem**
|
- **Golem**
|
||||||
- **GoogleDrive**
|
- **GoogleDrive**
|
||||||
@@ -326,8 +336,8 @@
|
|||||||
- **LePlaylist**
|
- **LePlaylist**
|
||||||
- **LetvCloud**: 乐视云
|
- **LetvCloud**: 乐视云
|
||||||
- **Libsyn**
|
- **Libsyn**
|
||||||
|
- **life**: Life.ru
|
||||||
- **life:embed**
|
- **life:embed**
|
||||||
- **lifenews**: LIFE | NEWS
|
|
||||||
- **limelight**
|
- **limelight**
|
||||||
- **limelight:channel**
|
- **limelight:channel**
|
||||||
- **limelight:channel_list**
|
- **limelight:channel_list**
|
||||||
@@ -336,6 +346,7 @@
|
|||||||
- **livestream**
|
- **livestream**
|
||||||
- **livestream:original**
|
- **livestream:original**
|
||||||
- **LnkGo**
|
- **LnkGo**
|
||||||
|
- **loc**: Library of Congress
|
||||||
- **LocalNews8**
|
- **LocalNews8**
|
||||||
- **LoveHomePorn**
|
- **LoveHomePorn**
|
||||||
- **lrt.lt**
|
- **lrt.lt**
|
||||||
@@ -426,6 +437,7 @@
|
|||||||
- **nhl.com:videocenter**
|
- **nhl.com:videocenter**
|
||||||
- **nhl.com:videocenter:category**: NHL videocenter category
|
- **nhl.com:videocenter:category**: NHL videocenter category
|
||||||
- **nick.com**
|
- **nick.com**
|
||||||
|
- **nick.de**
|
||||||
- **niconico**: ニコニコ動画
|
- **niconico**: ニコニコ動画
|
||||||
- **NiconicoPlaylist**
|
- **NiconicoPlaylist**
|
||||||
- **njoy**: N-JOY
|
- **njoy**: N-JOY
|
||||||
@@ -510,8 +522,11 @@
|
|||||||
- **qqmusic:singer**: QQ音乐 - 歌手
|
- **qqmusic:singer**: QQ音乐 - 歌手
|
||||||
- **qqmusic:toplist**: QQ音乐 - 排行榜
|
- **qqmusic:toplist**: QQ音乐 - 排行榜
|
||||||
- **R7**
|
- **R7**
|
||||||
|
- **R7Article**
|
||||||
- **radio.de**
|
- **radio.de**
|
||||||
- **radiobremen**
|
- **radiobremen**
|
||||||
|
- **radiocanada**
|
||||||
|
- **RadioCanadaAudioVideo**
|
||||||
- **radiofrance**
|
- **radiofrance**
|
||||||
- **RadioJavan**
|
- **RadioJavan**
|
||||||
- **Rai**
|
- **Rai**
|
||||||
@@ -521,10 +536,13 @@
|
|||||||
- **RedTube**
|
- **RedTube**
|
||||||
- **RegioTV**
|
- **RegioTV**
|
||||||
- **Restudy**
|
- **Restudy**
|
||||||
|
- **Reuters**
|
||||||
- **ReverbNation**
|
- **ReverbNation**
|
||||||
- **Revision3**
|
- **revision**
|
||||||
|
- **revision3:embed**
|
||||||
- **RICE**
|
- **RICE**
|
||||||
- **RingTV**
|
- **RingTV**
|
||||||
|
- **RockstarGames**
|
||||||
- **RottenTomatoes**
|
- **RottenTomatoes**
|
||||||
- **Roxwel**
|
- **Roxwel**
|
||||||
- **RTBF**
|
- **RTBF**
|
||||||
@@ -561,6 +579,7 @@
|
|||||||
- **ScreencastOMatic**
|
- **ScreencastOMatic**
|
||||||
- **ScreenJunkies**
|
- **ScreenJunkies**
|
||||||
- **ScreenwaveMedia**
|
- **ScreenwaveMedia**
|
||||||
|
- **Seeker**
|
||||||
- **SenateISVP**
|
- **SenateISVP**
|
||||||
- **SendtoNews**
|
- **SendtoNews**
|
||||||
- **ServingSys**
|
- **ServingSys**
|
||||||
@@ -636,6 +655,7 @@
|
|||||||
- **Telegraaf**
|
- **Telegraaf**
|
||||||
- **TeleMB**
|
- **TeleMB**
|
||||||
- **TeleTask**
|
- **TeleTask**
|
||||||
|
- **Telewebion**
|
||||||
- **TF1**
|
- **TF1**
|
||||||
- **TheIntercept**
|
- **TheIntercept**
|
||||||
- **ThePlatform**
|
- **ThePlatform**
|
||||||
@@ -682,11 +702,12 @@
|
|||||||
- **TVCArticle**
|
- **TVCArticle**
|
||||||
- **tvigle**: Интернет-телевидение Tvigle.ru
|
- **tvigle**: Интернет-телевидение Tvigle.ru
|
||||||
- **tvland.com**
|
- **tvland.com**
|
||||||
- **tvp.pl**
|
- **tvp**: Telewizja Polska
|
||||||
- **tvp.pl:Series**
|
- **tvp:series**
|
||||||
- **TVPlay**: TV3Play and related services
|
- **TVPlay**: TV3Play and related services
|
||||||
- **Tweakers**
|
- **Tweakers**
|
||||||
- **twitch:chapter**
|
- **twitch:chapter**
|
||||||
|
- **twitch:clips**
|
||||||
- **twitch:past_broadcasts**
|
- **twitch:past_broadcasts**
|
||||||
- **twitch:profile**
|
- **twitch:profile**
|
||||||
- **twitch:stream**
|
- **twitch:stream**
|
||||||
@@ -729,6 +750,7 @@
|
|||||||
- **VideoPremium**
|
- **VideoPremium**
|
||||||
- **VideoTt**: video.tt - Your True Tube (Currently broken)
|
- **VideoTt**: video.tt - Your True Tube (Currently broken)
|
||||||
- **videoweed**: VideoWeed
|
- **videoweed**: VideoWeed
|
||||||
|
- **Vidio**
|
||||||
- **vidme**
|
- **vidme**
|
||||||
- **vidme:user**
|
- **vidme:user**
|
||||||
- **vidme:user:likes**
|
- **vidme:user:likes**
|
||||||
@@ -764,17 +786,15 @@
|
|||||||
- **VRT**
|
- **VRT**
|
||||||
- **vube**: Vube.com
|
- **vube**: Vube.com
|
||||||
- **VuClip**
|
- **VuClip**
|
||||||
- **vulture.com**
|
|
||||||
- **Walla**
|
- **Walla**
|
||||||
- **WashingtonPost**
|
- **washingtonpost**
|
||||||
|
- **washingtonpost:article**
|
||||||
- **wat.tv**
|
- **wat.tv**
|
||||||
- **WatchIndianPorn**: Watch Indian Porn
|
- **WatchIndianPorn**: Watch Indian Porn
|
||||||
- **WDR**
|
- **WDR**
|
||||||
- **wdr:mobile**
|
- **wdr:mobile**
|
||||||
- **WDRMaus**: Sendung mit der Maus
|
|
||||||
- **WebOfStories**
|
- **WebOfStories**
|
||||||
- **WebOfStoriesPlaylist**
|
- **WebOfStoriesPlaylist**
|
||||||
- **Weibo**
|
|
||||||
- **WeiqiTV**: WQTV
|
- **WeiqiTV**: WQTV
|
||||||
- **wholecloud**: WholeCloud
|
- **wholecloud**: WholeCloud
|
||||||
- **Wimp**
|
- **Wimp**
|
||||||
@@ -782,10 +802,11 @@
|
|||||||
- **WNL**
|
- **WNL**
|
||||||
- **WorldStarHipHop**
|
- **WorldStarHipHop**
|
||||||
- **wrzuta.pl**
|
- **wrzuta.pl**
|
||||||
|
- **wrzuta.pl:playlist**
|
||||||
- **WSJ**: Wall Street Journal
|
- **WSJ**: Wall Street Journal
|
||||||
- **XBef**
|
- **XBef**
|
||||||
- **XboxClips**
|
- **XboxClips**
|
||||||
- **XFileShare**: XFileShare based sites: DaClips, FileHoot, GorillaVid, MovPod, PowerWatch, Rapidvideo.ws, TheVideoBee, Vidto, Streamin.To
|
- **XFileShare**: XFileShare based sites: DaClips, FileHoot, GorillaVid, MovPod, PowerWatch, Rapidvideo.ws, TheVideoBee, Vidto, Streamin.To, XVIDSTAGE
|
||||||
- **XHamster**
|
- **XHamster**
|
||||||
- **XHamsterEmbed**
|
- **XHamsterEmbed**
|
||||||
- **xiami:album**: 虾米音乐 - 专辑
|
- **xiami:album**: 虾米音乐 - 专辑
|
||||||
@@ -810,6 +831,7 @@
|
|||||||
- **Ynet**
|
- **Ynet**
|
||||||
- **YouJizz**
|
- **YouJizz**
|
||||||
- **youku**: 优酷
|
- **youku**: 优酷
|
||||||
|
- **youku:show**
|
||||||
- **YouPorn**
|
- **YouPorn**
|
||||||
- **YourUpload**
|
- **YourUpload**
|
||||||
- **youtube**: YouTube.com
|
- **youtube**: YouTube.com
|
||||||
|
1
setup.py
1
setup.py
@@ -122,6 +122,7 @@ setup(
|
|||||||
"Programming Language :: Python :: 3.2",
|
"Programming Language :: Python :: 3.2",
|
||||||
"Programming Language :: Python :: 3.3",
|
"Programming Language :: Python :: 3.3",
|
||||||
"Programming Language :: Python :: 3.4",
|
"Programming Language :: Python :: 3.4",
|
||||||
|
"Programming Language :: Python :: 3.5",
|
||||||
],
|
],
|
||||||
|
|
||||||
cmdclass={'build_lazy_extractors': build_lazy_extractors},
|
cmdclass={'build_lazy_extractors': build_lazy_extractors},
|
||||||
|
@@ -103,6 +103,12 @@ class TestCompat(unittest.TestCase):
|
|||||||
self.assertTrue(isinstance(doc.find('chinese').text, compat_str))
|
self.assertTrue(isinstance(doc.find('chinese').text, compat_str))
|
||||||
self.assertTrue(isinstance(doc.find('foo/bar').text, compat_str))
|
self.assertTrue(isinstance(doc.find('foo/bar').text, compat_str))
|
||||||
|
|
||||||
|
def test_compat_etree_fromstring_doctype(self):
|
||||||
|
xml = '''<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE smil PUBLIC "-//W3C//DTD SMIL 2.0//EN" "http://www.w3.org/2001/SMIL20/SMIL20.dtd">
|
||||||
|
<smil xmlns="http://www.w3.org/2001/SMIL20/Language"></smil>'''
|
||||||
|
compat_etree_fromstring(xml)
|
||||||
|
|
||||||
def test_struct_unpack(self):
|
def test_struct_unpack(self):
|
||||||
self.assertEqual(compat_struct_unpack('!B', b'\x00'), (0,))
|
self.assertEqual(compat_struct_unpack('!B', b'\x00'), (0,))
|
||||||
|
|
||||||
|
@@ -16,6 +16,15 @@ import threading
|
|||||||
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
def http_server_port(httpd):
|
||||||
|
if os.name == 'java' and isinstance(httpd.socket, ssl.SSLSocket):
|
||||||
|
# In Jython SSLSocket is not a subclass of socket.socket
|
||||||
|
sock = httpd.socket.sock
|
||||||
|
else:
|
||||||
|
sock = httpd.socket
|
||||||
|
return sock.getsockname()[1]
|
||||||
|
|
||||||
|
|
||||||
class HTTPTestRequestHandler(compat_http_server.BaseHTTPRequestHandler):
|
class HTTPTestRequestHandler(compat_http_server.BaseHTTPRequestHandler):
|
||||||
def log_message(self, format, *args):
|
def log_message(self, format, *args):
|
||||||
pass
|
pass
|
||||||
@@ -31,6 +40,22 @@ class HTTPTestRequestHandler(compat_http_server.BaseHTTPRequestHandler):
|
|||||||
self.send_header('Content-Type', 'video/mp4')
|
self.send_header('Content-Type', 'video/mp4')
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(b'\x00\x00\x00\x00\x20\x66\x74[video]')
|
self.wfile.write(b'\x00\x00\x00\x00\x20\x66\x74[video]')
|
||||||
|
elif self.path == '/302':
|
||||||
|
if sys.version_info[0] == 3:
|
||||||
|
# XXX: Python 3 http server does not allow non-ASCII header values
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
|
||||||
|
new_url = 'http://localhost:%d/中文.html' % http_server_port(self.server)
|
||||||
|
self.send_response(302)
|
||||||
|
self.send_header(b'Location', new_url.encode('utf-8'))
|
||||||
|
self.end_headers()
|
||||||
|
elif self.path == '/%E4%B8%AD%E6%96%87.html':
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-Type', 'text/html; charset=utf-8')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b'<html><video src="/vid.mp4" /></html>')
|
||||||
else:
|
else:
|
||||||
assert False
|
assert False
|
||||||
|
|
||||||
@@ -47,18 +72,32 @@ class FakeLogger(object):
|
|||||||
|
|
||||||
|
|
||||||
class TestHTTP(unittest.TestCase):
|
class TestHTTP(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.httpd = compat_http_server.HTTPServer(
|
||||||
|
('localhost', 0), HTTPTestRequestHandler)
|
||||||
|
self.port = http_server_port(self.httpd)
|
||||||
|
self.server_thread = threading.Thread(target=self.httpd.serve_forever)
|
||||||
|
self.server_thread.daemon = True
|
||||||
|
self.server_thread.start()
|
||||||
|
|
||||||
|
def test_unicode_path_redirection(self):
|
||||||
|
# XXX: Python 3 http server does not allow non-ASCII header values
|
||||||
|
if sys.version_info[0] == 3:
|
||||||
|
return
|
||||||
|
|
||||||
|
ydl = YoutubeDL({'logger': FakeLogger()})
|
||||||
|
r = ydl.extract_info('http://localhost:%d/302' % self.port)
|
||||||
|
self.assertEqual(r['url'], 'http://localhost:%d/vid.mp4' % self.port)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHTTPS(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
certfn = os.path.join(TEST_DIR, 'testcert.pem')
|
certfn = os.path.join(TEST_DIR, 'testcert.pem')
|
||||||
self.httpd = compat_http_server.HTTPServer(
|
self.httpd = compat_http_server.HTTPServer(
|
||||||
('localhost', 0), HTTPTestRequestHandler)
|
('localhost', 0), HTTPTestRequestHandler)
|
||||||
self.httpd.socket = ssl.wrap_socket(
|
self.httpd.socket = ssl.wrap_socket(
|
||||||
self.httpd.socket, certfile=certfn, server_side=True)
|
self.httpd.socket, certfile=certfn, server_side=True)
|
||||||
if os.name == 'java':
|
self.port = http_server_port(self.httpd)
|
||||||
# In Jython SSLSocket is not a subclass of socket.socket
|
|
||||||
sock = self.httpd.socket.sock
|
|
||||||
else:
|
|
||||||
sock = self.httpd.socket
|
|
||||||
self.port = sock.getsockname()[1]
|
|
||||||
self.server_thread = threading.Thread(target=self.httpd.serve_forever)
|
self.server_thread = threading.Thread(target=self.httpd.serve_forever)
|
||||||
self.server_thread.daemon = True
|
self.server_thread.daemon = True
|
||||||
self.server_thread.start()
|
self.server_thread.start()
|
||||||
@@ -94,14 +133,14 @@ class TestProxy(unittest.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.proxy = compat_http_server.HTTPServer(
|
self.proxy = compat_http_server.HTTPServer(
|
||||||
('localhost', 0), _build_proxy_handler('normal'))
|
('localhost', 0), _build_proxy_handler('normal'))
|
||||||
self.port = self.proxy.socket.getsockname()[1]
|
self.port = http_server_port(self.proxy)
|
||||||
self.proxy_thread = threading.Thread(target=self.proxy.serve_forever)
|
self.proxy_thread = threading.Thread(target=self.proxy.serve_forever)
|
||||||
self.proxy_thread.daemon = True
|
self.proxy_thread.daemon = True
|
||||||
self.proxy_thread.start()
|
self.proxy_thread.start()
|
||||||
|
|
||||||
self.cn_proxy = compat_http_server.HTTPServer(
|
self.cn_proxy = compat_http_server.HTTPServer(
|
||||||
('localhost', 0), _build_proxy_handler('cn'))
|
('localhost', 0), _build_proxy_handler('cn'))
|
||||||
self.cn_port = self.cn_proxy.socket.getsockname()[1]
|
self.cn_port = http_server_port(self.cn_proxy)
|
||||||
self.cn_proxy_thread = threading.Thread(target=self.cn_proxy.serve_forever)
|
self.cn_proxy_thread = threading.Thread(target=self.cn_proxy.serve_forever)
|
||||||
self.cn_proxy_thread.daemon = True
|
self.cn_proxy_thread.daemon = True
|
||||||
self.cn_proxy_thread.start()
|
self.cn_proxy_thread.start()
|
||||||
|
@@ -157,8 +157,8 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertTrue(sanitize_filename(':', restricted=True) != '')
|
self.assertTrue(sanitize_filename(':', restricted=True) != '')
|
||||||
|
|
||||||
self.assertEqual(sanitize_filename(
|
self.assertEqual(sanitize_filename(
|
||||||
'ÂÃÄÀÁÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØŒÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøœùúûüýþÿ', restricted=True),
|
'ÂÃÄÀÁÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖŐØŒÙÚÛÜŰÝÞßàáâãäåæçèéêëìíîïðñòóôõöőøœùúûüűýþÿ', restricted=True),
|
||||||
'AAAAAAAECEEEEIIIIDNOOOOOOOEUUUUYPssaaaaaaaeceeeeiiiionoooooooeuuuuypy')
|
'AAAAAAAECEEEEIIIIDNOOOOOOOOEUUUUUYPssaaaaaaaeceeeeiiiionooooooooeuuuuuypy')
|
||||||
|
|
||||||
def test_sanitize_ids(self):
|
def test_sanitize_ids(self):
|
||||||
self.assertEqual(sanitize_filename('_n_cd26wFpw', is_id=True), '_n_cd26wFpw')
|
self.assertEqual(sanitize_filename('_n_cd26wFpw', is_id=True), '_n_cd26wFpw')
|
||||||
@@ -249,6 +249,8 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(unescapeHTML('/'), '/')
|
self.assertEqual(unescapeHTML('/'), '/')
|
||||||
self.assertEqual(unescapeHTML('é'), 'é')
|
self.assertEqual(unescapeHTML('é'), 'é')
|
||||||
self.assertEqual(unescapeHTML('�'), '�')
|
self.assertEqual(unescapeHTML('�'), '�')
|
||||||
|
# HTML5 entities
|
||||||
|
self.assertEqual(unescapeHTML('.''), '.\'')
|
||||||
|
|
||||||
def test_date_from_str(self):
|
def test_date_from_str(self):
|
||||||
self.assertEqual(date_from_str('yesterday'), date_from_str('now-1day'))
|
self.assertEqual(date_from_str('yesterday'), date_from_str('now-1day'))
|
||||||
@@ -638,6 +640,9 @@ class TestUtil(unittest.TestCase):
|
|||||||
"1":{"src":"skipped", "type": "application/vnd.apple.mpegURL"}
|
"1":{"src":"skipped", "type": "application/vnd.apple.mpegURL"}
|
||||||
}''')
|
}''')
|
||||||
|
|
||||||
|
inp = '''{"foo":101}'''
|
||||||
|
self.assertEqual(js_to_json(inp), '''{"foo":101}''')
|
||||||
|
|
||||||
def test_js_to_json_edgecases(self):
|
def test_js_to_json_edgecases(self):
|
||||||
on = js_to_json("{abc_def:'1\\'\\\\2\\\\\\'3\"4'}")
|
on = js_to_json("{abc_def:'1\\'\\\\2\\\\\\'3\"4'}")
|
||||||
self.assertEqual(json.loads(on), {"abc_def": "1'\\2\\'3\"4"})
|
self.assertEqual(json.loads(on), {"abc_def": "1'\\2\\'3\"4"})
|
||||||
|
@@ -1223,6 +1223,10 @@ class YoutubeDL(object):
|
|||||||
if 'title' not in info_dict:
|
if 'title' not in info_dict:
|
||||||
raise ExtractorError('Missing "title" field in extractor result')
|
raise ExtractorError('Missing "title" field in extractor result')
|
||||||
|
|
||||||
|
if not isinstance(info_dict['id'], compat_str):
|
||||||
|
self.report_warning('"id" field is not a string - forcing string conversion')
|
||||||
|
info_dict['id'] = compat_str(info_dict['id'])
|
||||||
|
|
||||||
if 'playlist' not in info_dict:
|
if 'playlist' not in info_dict:
|
||||||
# It isn't part of a playlist
|
# It isn't part of a playlist
|
||||||
info_dict['playlist'] = None
|
info_dict['playlist'] = None
|
||||||
|
@@ -18,7 +18,6 @@ from .options import (
|
|||||||
from .compat import (
|
from .compat import (
|
||||||
compat_expanduser,
|
compat_expanduser,
|
||||||
compat_getpass,
|
compat_getpass,
|
||||||
compat_print,
|
|
||||||
compat_shlex_split,
|
compat_shlex_split,
|
||||||
workaround_optparse_bug9161,
|
workaround_optparse_bug9161,
|
||||||
)
|
)
|
||||||
@@ -76,7 +75,7 @@ def _real_main(argv=None):
|
|||||||
|
|
||||||
# Dump user agent
|
# Dump user agent
|
||||||
if opts.dump_user_agent:
|
if opts.dump_user_agent:
|
||||||
compat_print(std_headers['User-Agent'])
|
write_string(std_headers['User-Agent'] + '\n', out=sys.stdout)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# Batch file verification
|
# Batch file verification
|
||||||
@@ -101,10 +100,10 @@ def _real_main(argv=None):
|
|||||||
|
|
||||||
if opts.list_extractors:
|
if opts.list_extractors:
|
||||||
for ie in list_extractors(opts.age_limit):
|
for ie in list_extractors(opts.age_limit):
|
||||||
compat_print(ie.IE_NAME + (' (CURRENTLY BROKEN)' if not ie._WORKING else ''))
|
write_string(ie.IE_NAME + (' (CURRENTLY BROKEN)' if not ie._WORKING else '') + '\n', out=sys.stdout)
|
||||||
matchedUrls = [url for url in all_urls if ie.suitable(url)]
|
matchedUrls = [url for url in all_urls if ie.suitable(url)]
|
||||||
for mu in matchedUrls:
|
for mu in matchedUrls:
|
||||||
compat_print(' ' + mu)
|
write_string(' ' + mu + '\n', out=sys.stdout)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
if opts.list_extractor_descriptions:
|
if opts.list_extractor_descriptions:
|
||||||
for ie in list_extractors(opts.age_limit):
|
for ie in list_extractors(opts.age_limit):
|
||||||
@@ -117,7 +116,7 @@ def _real_main(argv=None):
|
|||||||
_SEARCHES = ('cute kittens', 'slithering pythons', 'falling cat', 'angry poodle', 'purple fish', 'running tortoise', 'sleeping bunny', 'burping cow')
|
_SEARCHES = ('cute kittens', 'slithering pythons', 'falling cat', 'angry poodle', 'purple fish', 'running tortoise', 'sleeping bunny', 'burping cow')
|
||||||
_COUNTS = ('', '5', '10', 'all')
|
_COUNTS = ('', '5', '10', 'all')
|
||||||
desc += ' (Example: "%s%s:%s" )' % (ie.SEARCH_KEY, random.choice(_COUNTS), random.choice(_SEARCHES))
|
desc += ' (Example: "%s%s:%s" )' % (ie.SEARCH_KEY, random.choice(_COUNTS), random.choice(_SEARCHES))
|
||||||
compat_print(desc)
|
write_string(desc + '\n', out=sys.stdout)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# Conflicting, missing and erroneous options
|
# Conflicting, missing and erroneous options
|
||||||
|
2263
youtube_dl/compat.py
2263
youtube_dl/compat.py
File diff suppressed because it is too large
Load Diff
@@ -85,7 +85,7 @@ class ExternalFD(FileDownloader):
|
|||||||
cmd, stderr=subprocess.PIPE)
|
cmd, stderr=subprocess.PIPE)
|
||||||
_, stderr = p.communicate()
|
_, stderr = p.communicate()
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
self.to_stderr(stderr)
|
self.to_stderr(stderr.decode('utf-8', 'replace'))
|
||||||
return p.returncode
|
return p.returncode
|
||||||
|
|
||||||
|
|
||||||
@@ -210,6 +210,7 @@ class FFmpegFD(ExternalFD):
|
|||||||
# args += ['-http_proxy', proxy]
|
# args += ['-http_proxy', proxy]
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
compat_setenv('HTTP_PROXY', proxy, env=env)
|
compat_setenv('HTTP_PROXY', proxy, env=env)
|
||||||
|
compat_setenv('http_proxy', proxy, env=env)
|
||||||
|
|
||||||
protocol = info_dict.get('protocol')
|
protocol = info_dict.get('protocol')
|
||||||
|
|
||||||
|
@@ -319,7 +319,7 @@ class F4mFD(FragmentFD):
|
|||||||
doc = compat_etree_fromstring(manifest)
|
doc = compat_etree_fromstring(manifest)
|
||||||
formats = [(int(f.attrib.get('bitrate', -1)), f)
|
formats = [(int(f.attrib.get('bitrate', -1)), f)
|
||||||
for f in self._get_unencrypted_media(doc)]
|
for f in self._get_unencrypted_media(doc)]
|
||||||
if requested_bitrate is None:
|
if requested_bitrate is None or len(formats) == 1:
|
||||||
# get the best format
|
# get the best format
|
||||||
formats = sorted(formats, key=lambda f: f[0])
|
formats = sorted(formats, key=lambda f: f[0])
|
||||||
rate, media = formats[-1]
|
rate, media = formats[-1]
|
||||||
|
@@ -2,14 +2,24 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
|
import binascii
|
||||||
|
try:
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
can_decrypt_frag = True
|
||||||
|
except ImportError:
|
||||||
|
can_decrypt_frag = False
|
||||||
|
|
||||||
from .fragment import FragmentFD
|
from .fragment import FragmentFD
|
||||||
from .external import FFmpegFD
|
from .external import FFmpegFD
|
||||||
|
|
||||||
from ..compat import compat_urlparse
|
from ..compat import (
|
||||||
|
compat_urlparse,
|
||||||
|
compat_struct_pack,
|
||||||
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
sanitize_open,
|
sanitize_open,
|
||||||
|
parse_m3u8_attributes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -21,19 +31,27 @@ class HlsFD(FragmentFD):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def can_download(manifest):
|
def can_download(manifest):
|
||||||
UNSUPPORTED_FEATURES = (
|
UNSUPPORTED_FEATURES = (
|
||||||
r'#EXT-X-KEY:METHOD=(?!NONE)', # encrypted streams [1]
|
r'#EXT-X-KEY:METHOD=(?!NONE|AES-128)', # encrypted streams [1]
|
||||||
r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [2]
|
r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [2]
|
||||||
|
|
||||||
# Live streams heuristic does not always work (e.g. geo restricted to Germany
|
# Live streams heuristic does not always work (e.g. geo restricted to Germany
|
||||||
# http://hls-geo.daserste.de/i/videoportal/Film/c_620000/622873/format,716451,716457,716450,716458,716459,.mp4.csmil/index_4_av.m3u8?null=0)
|
# http://hls-geo.daserste.de/i/videoportal/Film/c_620000/622873/format,716451,716457,716450,716458,716459,.mp4.csmil/index_4_av.m3u8?null=0)
|
||||||
# r'#EXT-X-MEDIA-SEQUENCE:(?!0$)', # live streams [3]
|
# r'#EXT-X-MEDIA-SEQUENCE:(?!0$)', # live streams [3]
|
||||||
r'#EXT-X-PLAYLIST-TYPE:EVENT', # media segments may be appended to the end of
|
|
||||||
# event media playlists [4]
|
# This heuristic also is not correct since segments may not be appended as well.
|
||||||
|
# Twitch vods of finished streams have EXT-X-PLAYLIST-TYPE:EVENT despite
|
||||||
|
# no segments will definitely be appended to the end of the playlist.
|
||||||
|
# r'#EXT-X-PLAYLIST-TYPE:EVENT', # media segments may be appended to the end of
|
||||||
|
# # event media playlists [4]
|
||||||
|
|
||||||
# 1. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.4
|
# 1. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.4
|
||||||
# 2. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.2
|
# 2. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.2
|
||||||
# 3. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3.2
|
# 3. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3.2
|
||||||
# 4. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3.5
|
# 4. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3.5
|
||||||
)
|
)
|
||||||
return all(not re.search(feature, manifest) for feature in UNSUPPORTED_FEATURES)
|
check_results = [not re.search(feature, manifest) for feature in UNSUPPORTED_FEATURES]
|
||||||
|
check_results.append(can_decrypt_frag or '#EXT-X-KEY:METHOD=AES-128' not in manifest)
|
||||||
|
return all(check_results)
|
||||||
|
|
||||||
def real_download(self, filename, info_dict):
|
def real_download(self, filename, info_dict):
|
||||||
man_url = info_dict['url']
|
man_url = info_dict['url']
|
||||||
@@ -51,36 +69,60 @@ class HlsFD(FragmentFD):
|
|||||||
fd.add_progress_hook(ph)
|
fd.add_progress_hook(ph)
|
||||||
return fd.real_download(filename, info_dict)
|
return fd.real_download(filename, info_dict)
|
||||||
|
|
||||||
fragment_urls = []
|
total_frags = 0
|
||||||
for line in s.splitlines():
|
for line in s.splitlines():
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if line and not line.startswith('#'):
|
if line and not line.startswith('#'):
|
||||||
segment_url = (
|
total_frags += 1
|
||||||
line
|
|
||||||
if re.match(r'^https?://', line)
|
|
||||||
else compat_urlparse.urljoin(man_url, line))
|
|
||||||
fragment_urls.append(segment_url)
|
|
||||||
# We only download the first fragment during the test
|
|
||||||
if self.params.get('test', False):
|
|
||||||
break
|
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
'filename': filename,
|
'filename': filename,
|
||||||
'total_frags': len(fragment_urls),
|
'total_frags': total_frags,
|
||||||
}
|
}
|
||||||
|
|
||||||
self._prepare_and_start_frag_download(ctx)
|
self._prepare_and_start_frag_download(ctx)
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
media_sequence = 0
|
||||||
|
decrypt_info = {'METHOD': 'NONE'}
|
||||||
frags_filenames = []
|
frags_filenames = []
|
||||||
for i, frag_url in enumerate(fragment_urls):
|
for line in s.splitlines():
|
||||||
frag_filename = '%s-Frag%d' % (ctx['tmpfilename'], i)
|
line = line.strip()
|
||||||
success = ctx['dl'].download(frag_filename, {'url': frag_url})
|
if line:
|
||||||
if not success:
|
if not line.startswith('#'):
|
||||||
return False
|
frag_url = (
|
||||||
down, frag_sanitized = sanitize_open(frag_filename, 'rb')
|
line
|
||||||
ctx['dest_stream'].write(down.read())
|
if re.match(r'^https?://', line)
|
||||||
down.close()
|
else compat_urlparse.urljoin(man_url, line))
|
||||||
frags_filenames.append(frag_sanitized)
|
frag_filename = '%s-Frag%d' % (ctx['tmpfilename'], i)
|
||||||
|
success = ctx['dl'].download(frag_filename, {'url': frag_url})
|
||||||
|
if not success:
|
||||||
|
return False
|
||||||
|
down, frag_sanitized = sanitize_open(frag_filename, 'rb')
|
||||||
|
frag_content = down.read()
|
||||||
|
down.close()
|
||||||
|
if decrypt_info['METHOD'] == 'AES-128':
|
||||||
|
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', media_sequence)
|
||||||
|
frag_content = AES.new(
|
||||||
|
decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(frag_content)
|
||||||
|
ctx['dest_stream'].write(frag_content)
|
||||||
|
frags_filenames.append(frag_sanitized)
|
||||||
|
# We only download the first fragment during the test
|
||||||
|
if self.params.get('test', False):
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
media_sequence += 1
|
||||||
|
elif line.startswith('#EXT-X-KEY'):
|
||||||
|
decrypt_info = parse_m3u8_attributes(line[11:])
|
||||||
|
if decrypt_info['METHOD'] == 'AES-128':
|
||||||
|
if 'IV' in decrypt_info:
|
||||||
|
decrypt_info['IV'] = binascii.unhexlify(decrypt_info['IV'][2:])
|
||||||
|
if not re.match(r'^https?://', decrypt_info['URI']):
|
||||||
|
decrypt_info['URI'] = compat_urlparse.urljoin(
|
||||||
|
man_url, decrypt_info['URI'])
|
||||||
|
decrypt_info['KEY'] = self.ydl.urlopen(decrypt_info['URI']).read()
|
||||||
|
elif line.startswith('#EXT-X-MEDIA-SEQUENCE'):
|
||||||
|
media_sequence = int(line[22:])
|
||||||
|
|
||||||
self._finish_frag_download(ctx)
|
self._finish_frag_download(ctx)
|
||||||
|
|
||||||
|
@@ -156,7 +156,10 @@ class AdobeTVVideoIE(InfoExtractor):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
video_data = self._download_json(url + '?format=json', video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
video_data = self._parse_json(self._search_regex(
|
||||||
|
r'var\s+bridge\s*=\s*([^;]+);', webpage, 'bridged data'), video_id)
|
||||||
|
|
||||||
formats = [{
|
formats = [{
|
||||||
'format_id': '%s-%s' % (determine_ext(source['src']), source.get('height')),
|
'format_id': '%s-%s' % (determine_ext(source['src']), source.get('height')),
|
||||||
|
133
youtube_dl/extractor/afreecatv.py
Normal file
133
youtube_dl/extractor/afreecatv.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import (
|
||||||
|
compat_urllib_parse_urlparse,
|
||||||
|
compat_urlparse,
|
||||||
|
)
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
int_or_none,
|
||||||
|
xpath_element,
|
||||||
|
xpath_text,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AfreecaTVIE(InfoExtractor):
|
||||||
|
IE_DESC = 'afreecatv.com'
|
||||||
|
_VALID_URL = r'''(?x)^
|
||||||
|
https?://(?:(live|afbbs|www)\.)?afreeca(?:tv)?\.com(?::\d+)?
|
||||||
|
(?:
|
||||||
|
/app/(?:index|read_ucc_bbs)\.cgi|
|
||||||
|
/player/[Pp]layer\.(?:swf|html))
|
||||||
|
\?.*?\bnTitleNo=(?P<id>\d+)'''
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://live.afreecatv.com:8079/app/index.cgi?szType=read_ucc_bbs&szBjId=dailyapril&nStationNo=16711924&nBbsNo=18605867&nTitleNo=36164052&szSkin=',
|
||||||
|
'md5': 'f72c89fe7ecc14c1b5ce506c4996046e',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '36164052',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '데일리 에이프릴 요정들의 시상식!',
|
||||||
|
'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$',
|
||||||
|
'uploader': 'dailyapril',
|
||||||
|
'uploader_id': 'dailyapril',
|
||||||
|
'upload_date': '20160503',
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'http://afbbs.afreecatv.com:8080/app/read_ucc_bbs.cgi?nStationNo=16711924&nTitleNo=36153164&szBjId=dailyapril&nBbsNo=18605867',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '36153164',
|
||||||
|
'title': "BJ유트루와 함께하는 '팅커벨 메이크업!'",
|
||||||
|
'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$',
|
||||||
|
'uploader': 'dailyapril',
|
||||||
|
'uploader_id': 'dailyapril',
|
||||||
|
},
|
||||||
|
'playlist_count': 2,
|
||||||
|
'playlist': [{
|
||||||
|
'md5': 'd8b7c174568da61d774ef0203159bf97',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '36153164_1',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': "BJ유트루와 함께하는 '팅커벨 메이크업!'",
|
||||||
|
'upload_date': '20160502',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'md5': '58f2ce7f6044e34439ab2d50612ab02b',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '36153164_2',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': "BJ유트루와 함께하는 '팅커벨 메이크업!'",
|
||||||
|
'upload_date': '20160502',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.afreecatv.com/player/Player.swf?szType=szBjId=djleegoon&nStationNo=11273158&nBbsNo=13161095&nTitleNo=36327652',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_video_key(key):
|
||||||
|
video_key = {}
|
||||||
|
m = re.match(r'^(?P<upload_date>\d{8})_\w+_(?P<part>\d+)$', key)
|
||||||
|
if m:
|
||||||
|
video_key['upload_date'] = m.group('upload_date')
|
||||||
|
video_key['part'] = m.group('part')
|
||||||
|
return video_key
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
parsed_url = compat_urllib_parse_urlparse(url)
|
||||||
|
info_url = compat_urlparse.urlunparse(parsed_url._replace(
|
||||||
|
netloc='afbbs.afreecatv.com:8080',
|
||||||
|
path='/api/video/get_video_info.php'))
|
||||||
|
video_xml = self._download_xml(info_url, video_id)
|
||||||
|
|
||||||
|
if xpath_element(video_xml, './track/video/file') is None:
|
||||||
|
raise ExtractorError('Specified AfreecaTV video does not exist',
|
||||||
|
expected=True)
|
||||||
|
|
||||||
|
title = xpath_text(video_xml, './track/title', 'title')
|
||||||
|
uploader = xpath_text(video_xml, './track/nickname', 'uploader')
|
||||||
|
uploader_id = xpath_text(video_xml, './track/bj_id', 'uploader id')
|
||||||
|
duration = int_or_none(xpath_text(video_xml, './track/duration',
|
||||||
|
'duration'))
|
||||||
|
thumbnail = xpath_text(video_xml, './track/titleImage', 'thumbnail')
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for i, video_file in enumerate(video_xml.findall('./track/video/file')):
|
||||||
|
video_key = self.parse_video_key(video_file.get('key', ''))
|
||||||
|
if not video_key:
|
||||||
|
continue
|
||||||
|
entries.append({
|
||||||
|
'id': '%s_%s' % (video_id, video_key.get('part', i + 1)),
|
||||||
|
'title': title,
|
||||||
|
'upload_date': video_key.get('upload_date'),
|
||||||
|
'duration': int_or_none(video_file.get('duration')),
|
||||||
|
'url': video_file.text,
|
||||||
|
})
|
||||||
|
|
||||||
|
info = {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'uploader': uploader,
|
||||||
|
'uploader_id': uploader_id,
|
||||||
|
'duration': duration,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entries) > 1:
|
||||||
|
info['_type'] = 'multi_video'
|
||||||
|
info['entries'] = entries
|
||||||
|
elif len(entries) == 1:
|
||||||
|
info['url'] = entries[0]['url']
|
||||||
|
info['upload_date'] = entries[0].get('upload_date')
|
||||||
|
else:
|
||||||
|
raise ExtractorError(
|
||||||
|
'No files found for the specified AfreecaTV video, either'
|
||||||
|
' the URL is incorrect or the video has been made private.',
|
||||||
|
expected=True)
|
||||||
|
|
||||||
|
return info
|
@@ -24,10 +24,10 @@ class AftonbladetIE(InfoExtractor):
|
|||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
# find internal video meta data
|
# find internal video meta data
|
||||||
meta_url = 'http://aftonbladet-play.drlib.aptoma.no/video/%s.json'
|
meta_url = 'http://aftonbladet-play-metadata.cdn.drvideo.aptoma.no/video/%s.json'
|
||||||
player_config = self._parse_json(self._html_search_regex(
|
player_config = self._parse_json(self._html_search_regex(
|
||||||
r'data-player-config="([^"]+)"', webpage, 'player config'), video_id)
|
r'data-player-config="([^"]+)"', webpage, 'player config'), video_id)
|
||||||
internal_meta_id = player_config['videoId']
|
internal_meta_id = player_config['aptomaVideoId']
|
||||||
internal_meta_url = meta_url % internal_meta_id
|
internal_meta_url = meta_url % internal_meta_id
|
||||||
internal_meta_json = self._download_json(
|
internal_meta_json = self._download_json(
|
||||||
internal_meta_url, video_id, 'Downloading video meta data')
|
internal_meta_url, video_id, 'Downloading video meta data')
|
||||||
|
@@ -8,7 +8,6 @@ from .generic import GenericIE
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
determine_ext,
|
determine_ext,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
get_element_by_attribute,
|
|
||||||
qualities,
|
qualities,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
@@ -274,41 +273,3 @@ class ARDIE(InfoExtractor):
|
|||||||
'upload_date': upload_date,
|
'upload_date': upload_date,
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SportschauIE(ARDMediathekIE):
|
|
||||||
IE_NAME = 'Sportschau'
|
|
||||||
_VALID_URL = r'(?P<baseurl>https?://(?:www\.)?sportschau\.de/(?:[^/]+/)+video(?P<id>[^/#?]+))\.html'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'http://www.sportschau.de/tourdefrance/videoseppeltkokainhatnichtsmitklassischemdopingzutun100.html',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'seppeltkokainhatnichtsmitklassischemdopingzutun100',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Seppelt: "Kokain hat nichts mit klassischem Doping zu tun"',
|
|
||||||
'thumbnail': 're:^https?://.*\.jpg$',
|
|
||||||
'description': 'Der ARD-Doping Experte Hajo Seppelt gibt seine Einschätzung zum ersten Dopingfall der diesjährigen Tour de France um den Italiener Luca Paolini ab.',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
mobj = re.match(self._VALID_URL, url)
|
|
||||||
video_id = mobj.group('id')
|
|
||||||
base_url = mobj.group('baseurl')
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
|
||||||
title = get_element_by_attribute('class', 'headline', webpage)
|
|
||||||
description = self._html_search_meta('description', webpage, 'description')
|
|
||||||
|
|
||||||
info = self._extract_media_info(
|
|
||||||
base_url + '-mc_defaultQuality-h.json', webpage, video_id)
|
|
||||||
|
|
||||||
info.update({
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
})
|
|
||||||
|
|
||||||
return info
|
|
||||||
|
@@ -61,10 +61,7 @@ class ArteTvIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ArteTVPlus7IE(InfoExtractor):
|
class ArteTVBaseIE(InfoExtractor):
|
||||||
IE_NAME = 'arte.tv:+7'
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?arte\.tv/guide/(?P<lang>fr|de|en|es)/(?:(?:sendungen|emissions|embed)/)?(?P<id>[^/]+)/(?P<name>[^/?#&]+)'
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _extract_url_info(cls, url):
|
def _extract_url_info(cls, url):
|
||||||
mobj = re.match(cls._VALID_URL, url)
|
mobj = re.match(cls._VALID_URL, url)
|
||||||
@@ -78,60 +75,6 @@ class ArteTVPlus7IE(InfoExtractor):
|
|||||||
video_id = mobj.group('id')
|
video_id = mobj.group('id')
|
||||||
return video_id, lang
|
return video_id, lang
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id, lang = self._extract_url_info(url)
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
|
||||||
return self._extract_from_webpage(webpage, video_id, lang)
|
|
||||||
|
|
||||||
def _extract_from_webpage(self, webpage, video_id, lang):
|
|
||||||
patterns_templates = (r'arte_vp_url=["\'](.*?%s.*?)["\']', r'data-url=["\']([^"]+%s[^"]+)["\']')
|
|
||||||
ids = (video_id, '')
|
|
||||||
# some pages contain multiple videos (like
|
|
||||||
# http://www.arte.tv/guide/de/sendungen/XEN/xenius/?vid=055918-015_PLUS7-D),
|
|
||||||
# so we first try to look for json URLs that contain the video id from
|
|
||||||
# the 'vid' parameter.
|
|
||||||
patterns = [t % re.escape(_id) for _id in ids for t in patterns_templates]
|
|
||||||
json_url = self._html_search_regex(
|
|
||||||
patterns, webpage, 'json vp url', default=None)
|
|
||||||
if not json_url:
|
|
||||||
def find_iframe_url(webpage, default=NO_DEFAULT):
|
|
||||||
return self._html_search_regex(
|
|
||||||
r'<iframe[^>]+src=(["\'])(?P<url>.+\bjson_url=.+?)\1',
|
|
||||||
webpage, 'iframe url', group='url', default=default)
|
|
||||||
|
|
||||||
iframe_url = find_iframe_url(webpage, None)
|
|
||||||
if not iframe_url:
|
|
||||||
embed_url = self._html_search_regex(
|
|
||||||
r'arte_vp_url_oembed=\'([^\']+?)\'', webpage, 'embed url', default=None)
|
|
||||||
if embed_url:
|
|
||||||
player = self._download_json(
|
|
||||||
embed_url, video_id, 'Downloading player page')
|
|
||||||
iframe_url = find_iframe_url(player['html'])
|
|
||||||
# en and es URLs produce react-based pages with different layout (e.g.
|
|
||||||
# http://www.arte.tv/guide/en/053330-002-A/carnival-italy?zone=world)
|
|
||||||
if not iframe_url:
|
|
||||||
program = self._search_regex(
|
|
||||||
r'program\s*:\s*({.+?["\']embed_html["\'].+?}),?\s*\n',
|
|
||||||
webpage, 'program', default=None)
|
|
||||||
if program:
|
|
||||||
embed_html = self._parse_json(program, video_id)
|
|
||||||
if embed_html:
|
|
||||||
iframe_url = find_iframe_url(embed_html['embed_html'])
|
|
||||||
if iframe_url:
|
|
||||||
json_url = compat_parse_qs(
|
|
||||||
compat_urllib_parse_urlparse(iframe_url).query)['json_url'][0]
|
|
||||||
if json_url:
|
|
||||||
title = self._search_regex(
|
|
||||||
r'<h3[^>]+title=(["\'])(?P<title>.+?)\1',
|
|
||||||
webpage, 'title', default=None, group='title')
|
|
||||||
return self._extract_from_json_url(json_url, video_id, lang, title=title)
|
|
||||||
# Different kind of embed URL (e.g.
|
|
||||||
# http://www.arte.tv/magazine/trepalium/fr/episode-0406-replay-trepalium)
|
|
||||||
embed_url = self._search_regex(
|
|
||||||
r'<iframe[^>]+src=(["\'])(?P<url>.+?)\1',
|
|
||||||
webpage, 'embed url', group='url')
|
|
||||||
return self.url_result(embed_url)
|
|
||||||
|
|
||||||
def _extract_from_json_url(self, json_url, video_id, lang, title=None):
|
def _extract_from_json_url(self, json_url, video_id, lang, title=None):
|
||||||
info = self._download_json(json_url, video_id)
|
info = self._download_json(json_url, video_id)
|
||||||
player_info = info['videoJsonPlayer']
|
player_info = info['videoJsonPlayer']
|
||||||
@@ -235,28 +178,94 @@ class ArteTVPlus7IE(InfoExtractor):
|
|||||||
return info_dict
|
return info_dict
|
||||||
|
|
||||||
|
|
||||||
|
class ArteTVPlus7IE(ArteTVBaseIE):
|
||||||
|
IE_NAME = 'arte.tv:+7'
|
||||||
|
_VALID_URL = r'https?://(?:(?:www|sites)\.)?arte\.tv/[^/]+/(?P<lang>fr|de|en|es)/(?:[^/]+/)*(?P<id>[^/?#&]+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://www.arte.tv/guide/de/sendungen/XEN/xenius/?vid=055918-015_PLUS7-D',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://sites.arte.tv/karambolage/de/video/karambolage-22',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def suitable(cls, url):
|
||||||
|
return False if ArteTVPlaylistIE.suitable(url) else super(ArteTVPlus7IE, cls).suitable(url)
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id, lang = self._extract_url_info(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
return self._extract_from_webpage(webpage, video_id, lang)
|
||||||
|
|
||||||
|
def _extract_from_webpage(self, webpage, video_id, lang):
|
||||||
|
patterns_templates = (r'arte_vp_url=["\'](.*?%s.*?)["\']', r'data-url=["\']([^"]+%s[^"]+)["\']')
|
||||||
|
ids = (video_id, '')
|
||||||
|
# some pages contain multiple videos (like
|
||||||
|
# http://www.arte.tv/guide/de/sendungen/XEN/xenius/?vid=055918-015_PLUS7-D),
|
||||||
|
# so we first try to look for json URLs that contain the video id from
|
||||||
|
# the 'vid' parameter.
|
||||||
|
patterns = [t % re.escape(_id) for _id in ids for t in patterns_templates]
|
||||||
|
json_url = self._html_search_regex(
|
||||||
|
patterns, webpage, 'json vp url', default=None)
|
||||||
|
if not json_url:
|
||||||
|
def find_iframe_url(webpage, default=NO_DEFAULT):
|
||||||
|
return self._html_search_regex(
|
||||||
|
r'<iframe[^>]+src=(["\'])(?P<url>.+\bjson_url=.+?)\1',
|
||||||
|
webpage, 'iframe url', group='url', default=default)
|
||||||
|
|
||||||
|
iframe_url = find_iframe_url(webpage, None)
|
||||||
|
if not iframe_url:
|
||||||
|
embed_url = self._html_search_regex(
|
||||||
|
r'arte_vp_url_oembed=\'([^\']+?)\'', webpage, 'embed url', default=None)
|
||||||
|
if embed_url:
|
||||||
|
player = self._download_json(
|
||||||
|
embed_url, video_id, 'Downloading player page')
|
||||||
|
iframe_url = find_iframe_url(player['html'])
|
||||||
|
# en and es URLs produce react-based pages with different layout (e.g.
|
||||||
|
# http://www.arte.tv/guide/en/053330-002-A/carnival-italy?zone=world)
|
||||||
|
if not iframe_url:
|
||||||
|
program = self._search_regex(
|
||||||
|
r'program\s*:\s*({.+?["\']embed_html["\'].+?}),?\s*\n',
|
||||||
|
webpage, 'program', default=None)
|
||||||
|
if program:
|
||||||
|
embed_html = self._parse_json(program, video_id)
|
||||||
|
if embed_html:
|
||||||
|
iframe_url = find_iframe_url(embed_html['embed_html'])
|
||||||
|
if iframe_url:
|
||||||
|
json_url = compat_parse_qs(
|
||||||
|
compat_urllib_parse_urlparse(iframe_url).query)['json_url'][0]
|
||||||
|
if json_url:
|
||||||
|
title = self._search_regex(
|
||||||
|
r'<h3[^>]+title=(["\'])(?P<title>.+?)\1',
|
||||||
|
webpage, 'title', default=None, group='title')
|
||||||
|
return self._extract_from_json_url(json_url, video_id, lang, title=title)
|
||||||
|
# Different kind of embed URL (e.g.
|
||||||
|
# http://www.arte.tv/magazine/trepalium/fr/episode-0406-replay-trepalium)
|
||||||
|
entries = [
|
||||||
|
self.url_result(url)
|
||||||
|
for _, url in re.findall(r'<iframe[^>]+src=(["\'])(?P<url>.+?)\1', webpage)]
|
||||||
|
return self.playlist_result(entries)
|
||||||
|
|
||||||
|
|
||||||
# It also uses the arte_vp_url url from the webpage to extract the information
|
# It also uses the arte_vp_url url from the webpage to extract the information
|
||||||
class ArteTVCreativeIE(ArteTVPlus7IE):
|
class ArteTVCreativeIE(ArteTVPlus7IE):
|
||||||
IE_NAME = 'arte.tv:creative'
|
IE_NAME = 'arte.tv:creative'
|
||||||
_VALID_URL = r'https?://creative\.arte\.tv/(?P<lang>fr|de|en|es)/(?:[^/]+/)*(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https?://creative\.arte\.tv/(?P<lang>fr|de|en|es)/(?:[^/]+/)*(?P<id>[^/?#&]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://creative.arte.tv/de/magazin/agentur-amateur-corporate-design',
|
'url': 'http://creative.arte.tv/fr/episode/osmosis-episode-1',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '72176',
|
'id': '057405-001-A',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Folge 2 - Corporate Design',
|
'title': 'OSMOSIS - N\'AYEZ PLUS PEUR D\'AIMER (1)',
|
||||||
'upload_date': '20131004',
|
'upload_date': '20150716',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://creative.arte.tv/fr/Monty-Python-Reunion',
|
'url': 'http://creative.arte.tv/fr/Monty-Python-Reunion',
|
||||||
'info_dict': {
|
'playlist_count': 11,
|
||||||
'id': '160676',
|
'add_ie': ['Youtube'],
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Monty Python live (mostly)',
|
|
||||||
'description': 'Événement ! Quarante-cinq ans après leurs premiers succès, les légendaires Monty Python remontent sur scène.\n',
|
|
||||||
'upload_date': '20140805',
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://creative.arte.tv/de/episode/agentur-amateur-4-der-erste-kunde',
|
'url': 'http://creative.arte.tv/de/episode/agentur-amateur-4-der-erste-kunde',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@@ -267,7 +276,7 @@ class ArteTVInfoIE(ArteTVPlus7IE):
|
|||||||
IE_NAME = 'arte.tv:info'
|
IE_NAME = 'arte.tv:info'
|
||||||
_VALID_URL = r'https?://info\.arte\.tv/(?P<lang>fr|de|en|es)/(?:[^/]+/)*(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https?://info\.arte\.tv/(?P<lang>fr|de|en|es)/(?:[^/]+/)*(?P<id>[^/?#&]+)'
|
||||||
|
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://info.arte.tv/fr/service-civique-un-cache-misere',
|
'url': 'http://info.arte.tv/fr/service-civique-un-cache-misere',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '067528-000-A',
|
'id': '067528-000-A',
|
||||||
@@ -275,7 +284,7 @@ class ArteTVInfoIE(ArteTVPlus7IE):
|
|||||||
'title': 'Service civique, un cache misère ?',
|
'title': 'Service civique, un cache misère ?',
|
||||||
'upload_date': '20160403',
|
'upload_date': '20160403',
|
||||||
},
|
},
|
||||||
}
|
}]
|
||||||
|
|
||||||
|
|
||||||
class ArteTVFutureIE(ArteTVPlus7IE):
|
class ArteTVFutureIE(ArteTVPlus7IE):
|
||||||
@@ -300,6 +309,8 @@ class ArteTVDDCIE(ArteTVPlus7IE):
|
|||||||
IE_NAME = 'arte.tv:ddc'
|
IE_NAME = 'arte.tv:ddc'
|
||||||
_VALID_URL = r'https?://ddc\.arte\.tv/(?P<lang>emission|folge)/(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https?://ddc\.arte\.tv/(?P<lang>emission|folge)/(?P<id>[^/?#&]+)'
|
||||||
|
|
||||||
|
_TESTS = []
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id, lang = self._extract_url_info(url)
|
video_id, lang = self._extract_url_info(url)
|
||||||
if lang == 'folge':
|
if lang == 'folge':
|
||||||
@@ -318,7 +329,7 @@ class ArteTVConcertIE(ArteTVPlus7IE):
|
|||||||
IE_NAME = 'arte.tv:concert'
|
IE_NAME = 'arte.tv:concert'
|
||||||
_VALID_URL = r'https?://concert\.arte\.tv/(?P<lang>fr|de|en|es)/(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https?://concert\.arte\.tv/(?P<lang>fr|de|en|es)/(?P<id>[^/?#&]+)'
|
||||||
|
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://concert.arte.tv/de/notwist-im-pariser-konzertclub-divan-du-monde',
|
'url': 'http://concert.arte.tv/de/notwist-im-pariser-konzertclub-divan-du-monde',
|
||||||
'md5': '9ea035b7bd69696b67aa2ccaaa218161',
|
'md5': '9ea035b7bd69696b67aa2ccaaa218161',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -328,24 +339,23 @@ class ArteTVConcertIE(ArteTVPlus7IE):
|
|||||||
'upload_date': '20140128',
|
'upload_date': '20140128',
|
||||||
'description': 'md5:486eb08f991552ade77439fe6d82c305',
|
'description': 'md5:486eb08f991552ade77439fe6d82c305',
|
||||||
},
|
},
|
||||||
}
|
}]
|
||||||
|
|
||||||
|
|
||||||
class ArteTVCinemaIE(ArteTVPlus7IE):
|
class ArteTVCinemaIE(ArteTVPlus7IE):
|
||||||
IE_NAME = 'arte.tv:cinema'
|
IE_NAME = 'arte.tv:cinema'
|
||||||
_VALID_URL = r'https?://cinema\.arte\.tv/(?P<lang>fr|de|en|es)/(?P<id>.+)'
|
_VALID_URL = r'https?://cinema\.arte\.tv/(?P<lang>fr|de|en|es)/(?P<id>.+)'
|
||||||
|
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://cinema.arte.tv/de/node/38291',
|
'url': 'http://cinema.arte.tv/fr/article/les-ailes-du-desir-de-julia-reck',
|
||||||
'md5': '6b275511a5107c60bacbeeda368c3aa1',
|
'md5': 'a5b9dd5575a11d93daf0e3f404f45438',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '055876-000_PWA12025-D',
|
'id': '062494-000-A',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Tod auf dem Nil',
|
'title': 'Film lauréat du concours web - "Les ailes du désir" de Julia Reck',
|
||||||
'upload_date': '20160122',
|
'upload_date': '20150807',
|
||||||
'description': 'md5:7f749bbb77d800ef2be11d54529b96bc',
|
|
||||||
},
|
},
|
||||||
}
|
}]
|
||||||
|
|
||||||
|
|
||||||
class ArteTVMagazineIE(ArteTVPlus7IE):
|
class ArteTVMagazineIE(ArteTVPlus7IE):
|
||||||
@@ -390,9 +400,41 @@ class ArteTVEmbedIE(ArteTVPlus7IE):
|
|||||||
)
|
)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
_TESTS = []
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
video_id = mobj.group('id')
|
video_id = mobj.group('id')
|
||||||
lang = mobj.group('lang')
|
lang = mobj.group('lang')
|
||||||
json_url = mobj.group('json_url')
|
json_url = mobj.group('json_url')
|
||||||
return self._extract_from_json_url(json_url, video_id, lang)
|
return self._extract_from_json_url(json_url, video_id, lang)
|
||||||
|
|
||||||
|
|
||||||
|
class ArteTVPlaylistIE(ArteTVBaseIE):
|
||||||
|
IE_NAME = 'arte.tv:playlist'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?arte\.tv/guide/(?P<lang>fr|de|en|es)/[^#]*#collection/(?P<id>PL-\d+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://www.arte.tv/guide/de/plus7/?country=DE#collection/PL-013263/ARTETV',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'PL-013263',
|
||||||
|
'title': 'Areva & Uramin',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 6,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.arte.tv/guide/de/playlists?country=DE#collection/PL-013190/ARTETV',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
playlist_id, lang = self._extract_url_info(url)
|
||||||
|
collection = self._download_json(
|
||||||
|
'https://api.arte.tv/api/player/v1/collectionData/%s/%s?source=videos'
|
||||||
|
% (lang, playlist_id), playlist_id)
|
||||||
|
title = collection.get('title')
|
||||||
|
description = collection.get('shortDescription') or collection.get('teaserText')
|
||||||
|
entries = [
|
||||||
|
self._extract_from_json_url(
|
||||||
|
video['jsonUrl'], video.get('programId') or playlist_id, lang)
|
||||||
|
for video in collection['videos'] if video.get('jsonUrl')]
|
||||||
|
return self.playlist_result(entries, playlist_id, title, description)
|
||||||
|
@@ -6,6 +6,7 @@ import time
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .soundcloud import SoundcloudIE
|
from .soundcloud import SoundcloudIE
|
||||||
|
from ..compat import compat_str
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
url_basename,
|
url_basename,
|
||||||
@@ -136,7 +137,7 @@ class AudiomackAlbumIE(InfoExtractor):
|
|||||||
result[resultkey] = api_response[apikey]
|
result[resultkey] = api_response[apikey]
|
||||||
song_id = url_basename(api_response['url']).rpartition('.')[0]
|
song_id = url_basename(api_response['url']).rpartition('.')[0]
|
||||||
result['entries'].append({
|
result['entries'].append({
|
||||||
'id': api_response.get('id', song_id),
|
'id': compat_str(api_response.get('id', song_id)),
|
||||||
'uploader': api_response.get('artist'),
|
'uploader': api_response.get('artist'),
|
||||||
'title': api_response.get('title', song_id),
|
'title': api_response.get('title', song_id),
|
||||||
'url': api_response['url'],
|
'url': api_response['url'],
|
||||||
|
@@ -46,6 +46,7 @@ class AzubuIE(InfoExtractor):
|
|||||||
'uploader_id': 272749,
|
'uploader_id': 272749,
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
},
|
},
|
||||||
|
'skip': 'Channel offline',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -56,22 +57,26 @@ class AzubuIE(InfoExtractor):
|
|||||||
'http://www.azubu.tv/api/video/%s' % video_id, video_id)['data']
|
'http://www.azubu.tv/api/video/%s' % video_id, video_id)['data']
|
||||||
|
|
||||||
title = data['title'].strip()
|
title = data['title'].strip()
|
||||||
description = data['description']
|
description = data.get('description')
|
||||||
thumbnail = data['thumbnail']
|
thumbnail = data.get('thumbnail')
|
||||||
view_count = data['view_count']
|
view_count = data.get('view_count')
|
||||||
uploader = data['user']['username']
|
user = data.get('user', {})
|
||||||
uploader_id = data['user']['id']
|
uploader = user.get('username')
|
||||||
|
uploader_id = user.get('id')
|
||||||
|
|
||||||
stream_params = json.loads(data['stream_params'])
|
stream_params = json.loads(data['stream_params'])
|
||||||
|
|
||||||
timestamp = float_or_none(stream_params['creationDate'], 1000)
|
timestamp = float_or_none(stream_params.get('creationDate'), 1000)
|
||||||
duration = float_or_none(stream_params['length'], 1000)
|
duration = float_or_none(stream_params.get('length'), 1000)
|
||||||
|
|
||||||
renditions = stream_params.get('renditions') or []
|
renditions = stream_params.get('renditions') or []
|
||||||
video = stream_params.get('FLVFullLength') or stream_params.get('videoFullLength')
|
video = stream_params.get('FLVFullLength') or stream_params.get('videoFullLength')
|
||||||
if video:
|
if video:
|
||||||
renditions.append(video)
|
renditions.append(video)
|
||||||
|
|
||||||
|
if not renditions and not user.get('channel', {}).get('is_live', True):
|
||||||
|
raise ExtractorError('%s said: channel is offline.' % self.IE_NAME, expected=True)
|
||||||
|
|
||||||
formats = [{
|
formats = [{
|
||||||
'url': fmt['url'],
|
'url': fmt['url'],
|
||||||
'width': fmt['frameWidth'],
|
'width': fmt['frameWidth'],
|
||||||
|
@@ -29,7 +29,7 @@ class BandcampIE(InfoExtractor):
|
|||||||
'_skip': 'There is a limit of 200 free downloads / month for the test song'
|
'_skip': 'There is a limit of 200 free downloads / month for the test song'
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://benprunty.bandcamp.com/track/lanius-battle',
|
'url': 'http://benprunty.bandcamp.com/track/lanius-battle',
|
||||||
'md5': '2b68e5851514c20efdff2afc5603b8b4',
|
'md5': '73d0b3171568232574e45652f8720b5c',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2650410135',
|
'id': '2650410135',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
@@ -48,6 +48,10 @@ class BandcampIE(InfoExtractor):
|
|||||||
if m_trackinfo:
|
if m_trackinfo:
|
||||||
json_code = m_trackinfo.group(1)
|
json_code = m_trackinfo.group(1)
|
||||||
data = json.loads(json_code)[0]
|
data = json.loads(json_code)[0]
|
||||||
|
track_id = compat_str(data['id'])
|
||||||
|
|
||||||
|
if not data.get('file'):
|
||||||
|
raise ExtractorError('Not streamable', video_id=track_id, expected=True)
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for format_id, format_url in data['file'].items():
|
for format_id, format_url in data['file'].items():
|
||||||
@@ -64,7 +68,7 @@ class BandcampIE(InfoExtractor):
|
|||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': compat_str(data['id']),
|
'id': track_id,
|
||||||
'title': data['title'],
|
'title': data['title'],
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'duration': float_or_none(data.get('duration')),
|
'duration': float_or_none(data.get('duration')),
|
||||||
|
@@ -31,7 +31,7 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
music/clips[/#]|
|
music/clips[/#]|
|
||||||
radio/player/
|
radio/player/
|
||||||
)
|
)
|
||||||
(?P<id>%s)
|
(?P<id>%s)(?!/(?:episodes|broadcasts|clips))
|
||||||
''' % _ID_REGEX
|
''' % _ID_REGEX
|
||||||
|
|
||||||
_MEDIASELECTOR_URLS = [
|
_MEDIASELECTOR_URLS = [
|
||||||
@@ -192,6 +192,7 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
# rtmp download
|
# rtmp download
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
'skip': 'Now it\'s really geo-restricted',
|
||||||
}, {
|
}, {
|
||||||
# compact player (https://github.com/rg3/youtube-dl/issues/8147)
|
# compact player (https://github.com/rg3/youtube-dl/issues/8147)
|
||||||
'url': 'http://www.bbc.co.uk/programmes/p028bfkf/player',
|
'url': 'http://www.bbc.co.uk/programmes/p028bfkf/player',
|
||||||
@@ -698,7 +699,9 @@ class BBCIE(BBCCoUkIE):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def suitable(cls, url):
|
def suitable(cls, url):
|
||||||
return False if BBCCoUkIE.suitable(url) or BBCCoUkArticleIE.suitable(url) else super(BBCIE, cls).suitable(url)
|
EXCLUDE_IE = (BBCCoUkIE, BBCCoUkArticleIE, BBCCoUkIPlayerPlaylistIE, BBCCoUkPlaylistIE)
|
||||||
|
return (False if any(ie.suitable(url) for ie in EXCLUDE_IE)
|
||||||
|
else super(BBCIE, cls).suitable(url))
|
||||||
|
|
||||||
def _extract_from_media_meta(self, media_meta, video_id):
|
def _extract_from_media_meta(self, media_meta, video_id):
|
||||||
# Direct links to media in media metadata (e.g.
|
# Direct links to media in media metadata (e.g.
|
||||||
@@ -975,3 +978,72 @@ class BBCCoUkArticleIE(InfoExtractor):
|
|||||||
r'<div[^>]+typeof="Clip"[^>]+resource="([^"]+)"', webpage)]
|
r'<div[^>]+typeof="Clip"[^>]+resource="([^"]+)"', webpage)]
|
||||||
|
|
||||||
return self.playlist_result(entries, playlist_id, title, description)
|
return self.playlist_result(entries, playlist_id, title, description)
|
||||||
|
|
||||||
|
|
||||||
|
class BBCCoUkPlaylistBaseIE(InfoExtractor):
|
||||||
|
def _real_extract(self, url):
|
||||||
|
playlist_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, playlist_id)
|
||||||
|
|
||||||
|
entries = [
|
||||||
|
self.url_result(self._URL_TEMPLATE % video_id, BBCCoUkIE.ie_key())
|
||||||
|
for video_id in re.findall(
|
||||||
|
self._VIDEO_ID_TEMPLATE % BBCCoUkIE._ID_REGEX, webpage)]
|
||||||
|
|
||||||
|
title, description = self._extract_title_and_description(webpage)
|
||||||
|
|
||||||
|
return self.playlist_result(entries, playlist_id, title, description)
|
||||||
|
|
||||||
|
|
||||||
|
class BBCCoUkIPlayerPlaylistIE(BBCCoUkPlaylistBaseIE):
|
||||||
|
IE_NAME = 'bbc.co.uk:iplayer:playlist'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?bbc\.co\.uk/iplayer/episodes/(?P<id>%s)' % BBCCoUkIE._ID_REGEX
|
||||||
|
_URL_TEMPLATE = 'http://www.bbc.co.uk/iplayer/episode/%s'
|
||||||
|
_VIDEO_ID_TEMPLATE = r'data-ip-id=["\'](%s)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://www.bbc.co.uk/iplayer/episodes/b05rcz9v',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'b05rcz9v',
|
||||||
|
'title': 'The Disappearance',
|
||||||
|
'description': 'French thriller serial about a missing teenager.',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _extract_title_and_description(self, webpage):
|
||||||
|
title = self._search_regex(r'<h1>([^<]+)</h1>', webpage, 'title', fatal=False)
|
||||||
|
description = self._search_regex(
|
||||||
|
r'<p[^>]+class=(["\'])subtitle\1[^>]*>(?P<value>[^<]+)</p>',
|
||||||
|
webpage, 'description', fatal=False, group='value')
|
||||||
|
return title, description
|
||||||
|
|
||||||
|
|
||||||
|
class BBCCoUkPlaylistIE(BBCCoUkPlaylistBaseIE):
|
||||||
|
IE_NAME = 'bbc.co.uk:playlist'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?bbc\.co\.uk/programmes/(?P<id>%s)/(?:episodes|broadcasts|clips)' % BBCCoUkIE._ID_REGEX
|
||||||
|
_URL_TEMPLATE = 'http://www.bbc.co.uk/programmes/%s'
|
||||||
|
_VIDEO_ID_TEMPLATE = r'data-pid=["\'](%s)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://www.bbc.co.uk/programmes/b05rcz9v/clips',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'b05rcz9v',
|
||||||
|
'title': 'The Disappearance - Clips - BBC Four',
|
||||||
|
'description': 'French thriller serial about a missing teenager.',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 7,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.bbc.co.uk/programmes/b05rcz9v/broadcasts/2016/06',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.bbc.co.uk/programmes/b05rcz9v/clips',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.bbc.co.uk/programmes/b055jkys/episodes/player',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _extract_title_and_description(self, webpage):
|
||||||
|
title = self._og_search_title(webpage, fatal=False)
|
||||||
|
description = self._og_search_description(webpage)
|
||||||
|
return title, description
|
||||||
|
@@ -1,31 +1,27 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .mtv import MTVServicesInfoExtractor
|
||||||
from ..compat import compat_urllib_parse_unquote
|
from ..utils import unified_strdate
|
||||||
from ..utils import (
|
from ..compat import compat_urllib_parse_urlencode
|
||||||
xpath_text,
|
|
||||||
xpath_with_ns,
|
|
||||||
int_or_none,
|
|
||||||
parse_iso8601,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BetIE(InfoExtractor):
|
class BetIE(MTVServicesInfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?bet\.com/(?:[^/]+/)+(?P<id>.+?)\.html'
|
_VALID_URL = r'https?://(?:www\.)?bet\.com/(?:[^/]+/)+(?P<id>.+?)\.html'
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
'url': 'http://www.bet.com/news/politics/2014/12/08/in-bet-exclusive-obama-talks-race-and-racism.html',
|
'url': 'http://www.bet.com/news/politics/2014/12/08/in-bet-exclusive-obama-talks-race-and-racism.html',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'news/national/2014/a-conversation-with-president-obama',
|
'id': '07e96bd3-8850-3051-b856-271b457f0ab8',
|
||||||
'display_id': 'in-bet-exclusive-obama-talks-race-and-racism',
|
'display_id': 'in-bet-exclusive-obama-talks-race-and-racism',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': 'A Conversation With President Obama',
|
'title': 'A Conversation With President Obama',
|
||||||
'description': 'md5:699d0652a350cf3e491cd15cc745b5da',
|
'description': 'President Obama urges persistence in confronting racism and bias.',
|
||||||
'duration': 1534,
|
'duration': 1534,
|
||||||
'timestamp': 1418075340,
|
|
||||||
'upload_date': '20141208',
|
'upload_date': '20141208',
|
||||||
'uploader': 'admin',
|
|
||||||
'thumbnail': 're:(?i)^https?://.*\.jpg$',
|
'thumbnail': 're:(?i)^https?://.*\.jpg$',
|
||||||
|
'subtitles': {
|
||||||
|
'en': 'mincount:2',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# rtmp download
|
# rtmp download
|
||||||
@@ -35,16 +31,17 @@ class BetIE(InfoExtractor):
|
|||||||
{
|
{
|
||||||
'url': 'http://www.bet.com/video/news/national/2014/justice-for-ferguson-a-community-reacts.html',
|
'url': 'http://www.bet.com/video/news/national/2014/justice-for-ferguson-a-community-reacts.html',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'news/national/2014/justice-for-ferguson-a-community-reacts',
|
'id': '9f516bf1-7543-39c4-8076-dd441b459ba9',
|
||||||
'display_id': 'justice-for-ferguson-a-community-reacts',
|
'display_id': 'justice-for-ferguson-a-community-reacts',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': 'Justice for Ferguson: A Community Reacts',
|
'title': 'Justice for Ferguson: A Community Reacts',
|
||||||
'description': 'A BET News special.',
|
'description': 'A BET News special.',
|
||||||
'duration': 1696,
|
'duration': 1696,
|
||||||
'timestamp': 1416942360,
|
|
||||||
'upload_date': '20141125',
|
'upload_date': '20141125',
|
||||||
'uploader': 'admin',
|
|
||||||
'thumbnail': 're:(?i)^https?://.*\.jpg$',
|
'thumbnail': 're:(?i)^https?://.*\.jpg$',
|
||||||
|
'subtitles': {
|
||||||
|
'en': 'mincount:2',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# rtmp download
|
# rtmp download
|
||||||
@@ -53,57 +50,32 @@ class BetIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
_FEED_URL = "http://feeds.mtvnservices.com/od/feed/bet-mrss-player"
|
||||||
|
|
||||||
|
def _get_feed_query(self, uri):
|
||||||
|
return compat_urllib_parse_urlencode({
|
||||||
|
'uuid': uri,
|
||||||
|
})
|
||||||
|
|
||||||
|
def _extract_mgid(self, webpage):
|
||||||
|
return self._search_regex(r'data-uri="([^"]+)', webpage, 'mgid')
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
|
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
mgid = self._extract_mgid(webpage)
|
||||||
|
videos_info = self._get_videos_info(mgid)
|
||||||
|
|
||||||
media_url = compat_urllib_parse_unquote(self._search_regex(
|
info_dict = videos_info['entries'][0]
|
||||||
[r'mediaURL\s*:\s*"([^"]+)"', r"var\s+mrssMediaUrl\s*=\s*'([^']+)'"],
|
|
||||||
webpage, 'media URL'))
|
|
||||||
|
|
||||||
video_id = self._search_regex(
|
upload_date = unified_strdate(self._html_search_meta('date', webpage))
|
||||||
r'/video/(.*)/_jcr_content/', media_url, 'video id')
|
description = self._html_search_meta('description', webpage)
|
||||||
|
|
||||||
mrss = self._download_xml(media_url, display_id)
|
info_dict.update({
|
||||||
|
|
||||||
item = mrss.find('./channel/item')
|
|
||||||
|
|
||||||
NS_MAP = {
|
|
||||||
'dc': 'http://purl.org/dc/elements/1.1/',
|
|
||||||
'media': 'http://search.yahoo.com/mrss/',
|
|
||||||
'ka': 'http://kickapps.com/karss',
|
|
||||||
}
|
|
||||||
|
|
||||||
title = xpath_text(item, './title', 'title')
|
|
||||||
description = xpath_text(
|
|
||||||
item, './description', 'description', fatal=False)
|
|
||||||
|
|
||||||
timestamp = parse_iso8601(xpath_text(
|
|
||||||
item, xpath_with_ns('./dc:date', NS_MAP),
|
|
||||||
'upload date', fatal=False))
|
|
||||||
uploader = xpath_text(
|
|
||||||
item, xpath_with_ns('./dc:creator', NS_MAP),
|
|
||||||
'uploader', fatal=False)
|
|
||||||
|
|
||||||
media_content = item.find(
|
|
||||||
xpath_with_ns('./media:content', NS_MAP))
|
|
||||||
duration = int_or_none(media_content.get('duration'))
|
|
||||||
smil_url = media_content.get('url')
|
|
||||||
|
|
||||||
thumbnail = media_content.find(
|
|
||||||
xpath_with_ns('./media:thumbnail', NS_MAP)).get('url')
|
|
||||||
|
|
||||||
formats = self._extract_smil_formats(smil_url, display_id)
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'title': title,
|
|
||||||
'description': description,
|
'description': description,
|
||||||
'thumbnail': thumbnail,
|
'upload_date': upload_date,
|
||||||
'timestamp': timestamp,
|
})
|
||||||
'uploader': uploader,
|
|
||||||
'duration': duration,
|
return info_dict
|
||||||
'formats': formats,
|
|
||||||
}
|
|
||||||
|
@@ -1,34 +1,42 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import calendar
|
||||||
|
import datetime
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_str
|
from ..compat import (
|
||||||
|
compat_etree_fromstring,
|
||||||
|
compat_str,
|
||||||
|
compat_parse_qs,
|
||||||
|
compat_xml_parse_error,
|
||||||
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
|
||||||
unescapeHTML,
|
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
int_or_none,
|
||||||
|
float_or_none,
|
||||||
xpath_text,
|
xpath_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BiliBiliIE(InfoExtractor):
|
class BiliBiliIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://www\.bilibili\.(?:tv|com)/video/av(?P<id>\d+)(?:/index_(?P<page_num>\d+).html)?'
|
_VALID_URL = r'https?://www\.bilibili\.(?:tv|com)/video/av(?P<id>\d+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.bilibili.tv/video/av1074402/',
|
'url': 'http://www.bilibili.tv/video/av1074402/',
|
||||||
'md5': '2c301e4dab317596e837c3e7633e7d86',
|
'md5': '5f7d29e1a2872f3df0cf76b1f87d3788',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1554319',
|
'id': '1554319',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': '【金坷垃】金泡沫',
|
'title': '【金坷垃】金泡沫',
|
||||||
'duration': 308313,
|
'description': 'md5:ce18c2a2d2193f0df2917d270f2e5923',
|
||||||
|
'duration': 308.067,
|
||||||
|
'timestamp': 1398012660,
|
||||||
'upload_date': '20140420',
|
'upload_date': '20140420',
|
||||||
'thumbnail': 're:^https?://.+\.jpg',
|
'thumbnail': 're:^https?://.+\.jpg',
|
||||||
'description': 'md5:ce18c2a2d2193f0df2917d270f2e5923',
|
|
||||||
'timestamp': 1397983878,
|
|
||||||
'uploader': '菊子桑',
|
'uploader': '菊子桑',
|
||||||
|
'uploader_id': '156160',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.bilibili.com/video/av1041170/',
|
'url': 'http://www.bilibili.com/video/av1041170/',
|
||||||
@@ -36,75 +44,186 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
'id': '1041170',
|
'id': '1041170',
|
||||||
'title': '【BD1080P】刀语【诸神&异域】',
|
'title': '【BD1080P】刀语【诸神&异域】',
|
||||||
'description': '这是个神奇的故事~每个人不留弹幕不给走哦~切利哦!~',
|
'description': '这是个神奇的故事~每个人不留弹幕不给走哦~切利哦!~',
|
||||||
'uploader': '枫叶逝去',
|
|
||||||
'timestamp': 1396501299,
|
|
||||||
},
|
},
|
||||||
'playlist_count': 9,
|
'playlist_count': 9,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.bilibili.com/video/av4808130/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4808130',
|
||||||
|
'title': '【长篇】哆啦A梦443【钉铛】',
|
||||||
|
'description': '(2016.05.27)来组合客人的脸吧&amp;寻母六千里锭 抱歉,又轮到周日上班现在才到家 封面www.pixiv.net/member_illust.php?mode=medium&amp;illust_id=56912929',
|
||||||
|
},
|
||||||
|
'playlist': [{
|
||||||
|
'md5': '55cdadedf3254caaa0d5d27cf20a8f9c',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4808130_part1',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': '【长篇】哆啦A梦443【钉铛】',
|
||||||
|
'description': '(2016.05.27)来组合客人的脸吧&amp;寻母六千里锭 抱歉,又轮到周日上班现在才到家 封面www.pixiv.net/member_illust.php?mode=medium&amp;illust_id=56912929',
|
||||||
|
'timestamp': 1464564180,
|
||||||
|
'upload_date': '20160529',
|
||||||
|
'uploader': '喜欢拉面',
|
||||||
|
'uploader_id': '151066',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'md5': '926f9f67d0c482091872fbd8eca7ea3d',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4808130_part2',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': '【长篇】哆啦A梦443【钉铛】',
|
||||||
|
'description': '(2016.05.27)来组合客人的脸吧&amp;寻母六千里锭 抱歉,又轮到周日上班现在才到家 封面www.pixiv.net/member_illust.php?mode=medium&amp;illust_id=56912929',
|
||||||
|
'timestamp': 1464564180,
|
||||||
|
'upload_date': '20160529',
|
||||||
|
'uploader': '喜欢拉面',
|
||||||
|
'uploader_id': '151066',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'md5': '4b7b225b968402d7c32348c646f1fd83',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4808130_part3',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': '【长篇】哆啦A梦443【钉铛】',
|
||||||
|
'description': '(2016.05.27)来组合客人的脸吧&amp;寻母六千里锭 抱歉,又轮到周日上班现在才到家 封面www.pixiv.net/member_illust.php?mode=medium&amp;illust_id=56912929',
|
||||||
|
'timestamp': 1464564180,
|
||||||
|
'upload_date': '20160529',
|
||||||
|
'uploader': '喜欢拉面',
|
||||||
|
'uploader_id': '151066',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'md5': '7b795e214166501e9141139eea236e91',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4808130_part4',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': '【长篇】哆啦A梦443【钉铛】',
|
||||||
|
'description': '(2016.05.27)来组合客人的脸吧&amp;寻母六千里锭 抱歉,又轮到周日上班现在才到家 封面www.pixiv.net/member_illust.php?mode=medium&amp;illust_id=56912929',
|
||||||
|
'timestamp': 1464564180,
|
||||||
|
'upload_date': '20160529',
|
||||||
|
'uploader': '喜欢拉面',
|
||||||
|
'uploader_id': '151066',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
}, {
|
||||||
|
# Missing upload time
|
||||||
|
'url': 'http://www.bilibili.com/video/av1867637/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2880301',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': '【HDTV】【喜剧】岳父岳母真难当 (2014)【法国票房冠军】',
|
||||||
|
'description': '一个信奉天主教的法国旧式传统资产阶级家庭中有四个女儿。三个女儿却分别找了阿拉伯、犹太、中国丈夫,老夫老妻唯独期盼剩下未嫁的小女儿能找一个信奉天主教的法国白人,结果没想到小女儿找了一位非裔黑人……【这次应该不会跳帧了】',
|
||||||
|
'uploader': '黑夜为猫',
|
||||||
|
'uploader_id': '610729',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# Just to test metadata extraction
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
'expected_warnings': ['upload time'],
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
# BiliBili blocks keys from time to time. The current key is extracted from
|
||||||
|
# the Android client
|
||||||
|
# TODO: find the sign algorithm used in the flash player
|
||||||
|
_APP_KEY = '86385cdc024c0f6c'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
video_id = mobj.group('id')
|
video_id = mobj.group('id')
|
||||||
page_num = mobj.group('page_num') or '1'
|
|
||||||
|
|
||||||
view_data = self._download_json(
|
webpage = self._download_webpage(url, video_id)
|
||||||
'http://api.bilibili.com/view?type=json&appkey=8e9fc618fbd41e28&id=%s&page=%s' % (video_id, page_num),
|
|
||||||
video_id)
|
|
||||||
if 'error' in view_data:
|
|
||||||
raise ExtractorError('%s said: %s' % (self.IE_NAME, view_data['error']), expected=True)
|
|
||||||
|
|
||||||
cid = view_data['cid']
|
params = compat_parse_qs(self._search_regex(
|
||||||
title = unescapeHTML(view_data['title'])
|
[r'EmbedPlayer\([^)]+,\s*"([^"]+)"\)',
|
||||||
|
r'<iframe[^>]+src="https://secure\.bilibili\.com/secure,([^"]+)"'],
|
||||||
|
webpage, 'player parameters'))
|
||||||
|
cid = params['cid'][0]
|
||||||
|
|
||||||
doc = self._download_xml(
|
info_xml_str = self._download_webpage(
|
||||||
'http://interface.bilibili.com/v_cdn_play?appkey=8e9fc618fbd41e28&cid=%s' % cid,
|
'http://interface.bilibili.com/v_cdn_play',
|
||||||
cid,
|
cid, query={'appkey': self._APP_KEY, 'cid': cid},
|
||||||
'Downloading page %s/%s' % (page_num, view_data['pages'])
|
note='Downloading video info page')
|
||||||
)
|
|
||||||
|
|
||||||
if xpath_text(doc, './result') == 'error':
|
err_msg = None
|
||||||
raise ExtractorError('%s said: %s' % (self.IE_NAME, xpath_text(doc, './message')), expected=True)
|
durls = None
|
||||||
|
info_xml = None
|
||||||
|
try:
|
||||||
|
info_xml = compat_etree_fromstring(info_xml_str.encode('utf-8'))
|
||||||
|
except compat_xml_parse_error:
|
||||||
|
info_json = self._parse_json(info_xml_str, video_id, fatal=False)
|
||||||
|
err_msg = (info_json or {}).get('error_text')
|
||||||
|
else:
|
||||||
|
err_msg = xpath_text(info_xml, './message')
|
||||||
|
|
||||||
|
if info_xml is not None:
|
||||||
|
durls = info_xml.findall('./durl')
|
||||||
|
if not durls:
|
||||||
|
if err_msg:
|
||||||
|
raise ExtractorError('%s said: %s' % (self.IE_NAME, err_msg), expected=True)
|
||||||
|
else:
|
||||||
|
raise ExtractorError('No videos found!')
|
||||||
|
|
||||||
entries = []
|
entries = []
|
||||||
|
|
||||||
for durl in doc.findall('./durl'):
|
for durl in durls:
|
||||||
size = xpath_text(durl, ['./filesize', './size'])
|
size = xpath_text(durl, ['./filesize', './size'])
|
||||||
formats = [{
|
formats = [{
|
||||||
'url': durl.find('./url').text,
|
'url': durl.find('./url').text,
|
||||||
'filesize': int_or_none(size),
|
'filesize': int_or_none(size),
|
||||||
'ext': 'flv',
|
|
||||||
}]
|
}]
|
||||||
backup_urls = durl.find('./backup_url')
|
for backup_url in durl.findall('./backup_url/url'):
|
||||||
if backup_urls is not None:
|
formats.append({
|
||||||
for backup_url in backup_urls.findall('./url'):
|
'url': backup_url.text,
|
||||||
formats.append({'url': backup_url.text})
|
# backup URLs have lower priorities
|
||||||
formats.reverse()
|
'preference': -2 if 'hd.mp4' in backup_url.text else -3,
|
||||||
|
})
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
entries.append({
|
entries.append({
|
||||||
'id': '%s_part%s' % (cid, xpath_text(durl, './order')),
|
'id': '%s_part%s' % (cid, xpath_text(durl, './order')),
|
||||||
'title': title,
|
|
||||||
'duration': int_or_none(xpath_text(durl, './length'), 1000),
|
'duration': int_or_none(xpath_text(durl, './length'), 1000),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
title = self._html_search_regex('<h1[^>]+title="([^"]+)">', webpage, 'title')
|
||||||
|
description = self._html_search_meta('description', webpage)
|
||||||
|
datetime_str = self._html_search_regex(
|
||||||
|
r'<time[^>]+datetime="([^"]+)"', webpage, 'upload time', fatal=False)
|
||||||
|
timestamp = None
|
||||||
|
if datetime_str:
|
||||||
|
timestamp = calendar.timegm(datetime.datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M').timetuple())
|
||||||
|
|
||||||
|
# TODO 'view_count' requires deobfuscating Javascript
|
||||||
info = {
|
info = {
|
||||||
'id': compat_str(cid),
|
'id': compat_str(cid),
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': view_data.get('description'),
|
'description': description,
|
||||||
'thumbnail': view_data.get('pic'),
|
'timestamp': timestamp,
|
||||||
'uploader': view_data.get('author'),
|
'thumbnail': self._html_search_meta('thumbnailUrl', webpage),
|
||||||
'timestamp': int_or_none(view_data.get('created')),
|
'duration': float_or_none(xpath_text(info_xml, './timelength'), scale=1000),
|
||||||
'view_count': int_or_none(view_data.get('play')),
|
|
||||||
'duration': int_or_none(xpath_text(doc, './timelength')),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploader_mobj = re.search(
|
||||||
|
r'<a[^>]+href="https?://space\.bilibili\.com/(?P<id>\d+)"[^>]+title="(?P<name>[^"]+)"',
|
||||||
|
webpage)
|
||||||
|
if uploader_mobj:
|
||||||
|
info.update({
|
||||||
|
'uploader': uploader_mobj.group('name'),
|
||||||
|
'uploader_id': uploader_mobj.group('id'),
|
||||||
|
})
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
entry.update(info)
|
||||||
|
|
||||||
if len(entries) == 1:
|
if len(entries) == 1:
|
||||||
entries[0].update(info)
|
|
||||||
return entries[0]
|
return entries[0]
|
||||||
else:
|
else:
|
||||||
info.update({
|
for idx, entry in enumerate(entries):
|
||||||
|
entry['id'] = '%s_part%d' % (video_id, (idx + 1))
|
||||||
|
|
||||||
|
return {
|
||||||
'_type': 'multi_video',
|
'_type': 'multi_video',
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
'entries': entries,
|
'entries': entries,
|
||||||
})
|
}
|
||||||
return info
|
|
||||||
|
@@ -29,7 +29,8 @@ class BRIE(InfoExtractor):
|
|||||||
'duration': 180,
|
'duration': 180,
|
||||||
'uploader': 'Reinhard Weber',
|
'uploader': 'Reinhard Weber',
|
||||||
'upload_date': '20150422',
|
'upload_date': '20150422',
|
||||||
}
|
},
|
||||||
|
'skip': '404 not found',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://www.br.de/nachrichten/oberbayern/inhalt/muenchner-polizeipraesident-schreiber-gestorben-100.html',
|
'url': 'http://www.br.de/nachrichten/oberbayern/inhalt/muenchner-polizeipraesident-schreiber-gestorben-100.html',
|
||||||
@@ -40,7 +41,8 @@ class BRIE(InfoExtractor):
|
|||||||
'title': 'Manfred Schreiber ist tot',
|
'title': 'Manfred Schreiber ist tot',
|
||||||
'description': 'md5:b454d867f2a9fc524ebe88c3f5092d97',
|
'description': 'md5:b454d867f2a9fc524ebe88c3f5092d97',
|
||||||
'duration': 26,
|
'duration': 26,
|
||||||
}
|
},
|
||||||
|
'skip': '404 not found',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'https://www.br-klassik.de/audio/peeping-tom-premierenkritik-dance-festival-muenchen-100.html',
|
'url': 'https://www.br-klassik.de/audio/peeping-tom-premierenkritik-dance-festival-muenchen-100.html',
|
||||||
@@ -51,7 +53,8 @@ class BRIE(InfoExtractor):
|
|||||||
'title': 'Kurzweilig und sehr bewegend',
|
'title': 'Kurzweilig und sehr bewegend',
|
||||||
'description': 'md5:0351996e3283d64adeb38ede91fac54e',
|
'description': 'md5:0351996e3283d64adeb38ede91fac54e',
|
||||||
'duration': 296,
|
'duration': 296,
|
||||||
}
|
},
|
||||||
|
'skip': '404 not found',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://www.br.de/radio/bayern1/service/team/videos/team-video-erdelt100.html',
|
'url': 'http://www.br.de/radio/bayern1/service/team/videos/team-video-erdelt100.html',
|
||||||
|
@@ -11,6 +11,7 @@ class BYUtvIE(InfoExtractor):
|
|||||||
_VALID_URL = r'^https?://(?:www\.)?byutv.org/watch/[0-9a-f-]+/(?P<video_id>[^/?#]+)'
|
_VALID_URL = r'^https?://(?:www\.)?byutv.org/watch/[0-9a-f-]+/(?P<video_id>[^/?#]+)'
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://www.byutv.org/watch/6587b9a3-89d2-42a6-a7f7-fd2f81840a7d/studio-c-season-5-episode-5',
|
'url': 'http://www.byutv.org/watch/6587b9a3-89d2-42a6-a7f7-fd2f81840a7d/studio-c-season-5-episode-5',
|
||||||
|
'md5': '05850eb8c749e2ee05ad5a1c34668493',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'studio-c-season-5-episode-5',
|
'id': 'studio-c-season-5-episode-5',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -21,7 +22,8 @@ class BYUtvIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
}
|
},
|
||||||
|
'add_ie': ['Ooyala'],
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@@ -4,11 +4,11 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..compat import compat_urllib_parse_urlparse
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
HEADRequest,
|
HEADRequest,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
url_basename,
|
|
||||||
qualities,
|
qualities,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
)
|
)
|
||||||
@@ -16,24 +16,38 @@ from ..utils import (
|
|||||||
|
|
||||||
class CanalplusIE(InfoExtractor):
|
class CanalplusIE(InfoExtractor):
|
||||||
IE_DESC = 'canalplus.fr, piwiplus.fr and d8.tv'
|
IE_DESC = 'canalplus.fr, piwiplus.fr and d8.tv'
|
||||||
_VALID_URL = r'https?://(?:www\.(?P<site>canalplus\.fr|piwiplus\.fr|d8\.tv|itele\.fr)/.*?/(?P<path>.*)|player\.canalplus\.fr/#/(?P<id>[0-9]+))'
|
_VALID_URL = r'''(?x)
|
||||||
|
https?://
|
||||||
|
(?:
|
||||||
|
(?:
|
||||||
|
(?:(?:www|m)\.)?canalplus\.fr|
|
||||||
|
(?:www\.)?piwiplus\.fr|
|
||||||
|
(?:www\.)?d8\.tv|
|
||||||
|
(?:www\.)?d17\.tv|
|
||||||
|
(?:www\.)?itele\.fr
|
||||||
|
)/(?:(?:[^/]+/)*(?P<display_id>[^/?#&]+))?(?:\?.*\bvid=(?P<vid>\d+))?|
|
||||||
|
player\.canalplus\.fr/#/(?P<id>\d+)
|
||||||
|
)
|
||||||
|
|
||||||
|
'''
|
||||||
_VIDEO_INFO_TEMPLATE = 'http://service.canal-plus.com/video/rest/getVideosLiees/%s/%s?format=json'
|
_VIDEO_INFO_TEMPLATE = 'http://service.canal-plus.com/video/rest/getVideosLiees/%s/%s?format=json'
|
||||||
_SITE_ID_MAP = {
|
_SITE_ID_MAP = {
|
||||||
'canalplus.fr': 'cplus',
|
'canalplus': 'cplus',
|
||||||
'piwiplus.fr': 'teletoon',
|
'piwiplus': 'teletoon',
|
||||||
'd8.tv': 'd8',
|
'd8': 'd8',
|
||||||
'itele.fr': 'itele',
|
'd17': 'd17',
|
||||||
|
'itele': 'itele',
|
||||||
}
|
}
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.canalplus.fr/c-emissions/pid1830-c-zapping.html?vid=1263092',
|
'url': 'http://www.canalplus.fr/c-emissions/pid1830-c-zapping.html?vid=1192814',
|
||||||
'md5': '12164a6f14ff6df8bd628e8ba9b10b78',
|
'md5': '41f438a4904f7664b91b4ed0dec969dc',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1263092',
|
'id': '1192814',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Le Zapping - 13/05/15',
|
'title': "L'Année du Zapping 2014 - L'Année du Zapping 2014",
|
||||||
'description': 'md5:09738c0d06be4b5d06a0940edb0da73f',
|
'description': "Toute l'année 2014 dans un Zapping exceptionnel !",
|
||||||
'upload_date': '20150513',
|
'upload_date': '20150105',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.piwiplus.fr/videos-piwi/pid1405-le-labyrinthe-boing-super-ranger.html?vid=1108190',
|
'url': 'http://www.piwiplus.fr/videos-piwi/pid1405-le-labyrinthe-boing-super-ranger.html?vid=1108190',
|
||||||
@@ -46,35 +60,45 @@ class CanalplusIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
'skip': 'Only works from France',
|
'skip': 'Only works from France',
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.d8.tv/d8-docs-mags/pid6589-d8-campagne-intime.html',
|
'url': 'http://www.d8.tv/d8-docs-mags/pid5198-d8-en-quete-d-actualite.html?vid=1390231',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '966289',
|
'id': '1390231',
|
||||||
'ext': 'flv',
|
|
||||||
'title': 'Campagne intime - Documentaire exceptionnel',
|
|
||||||
'description': 'md5:d2643b799fb190846ae09c61e59a859f',
|
|
||||||
'upload_date': '20131108',
|
|
||||||
},
|
|
||||||
'skip': 'videos get deleted after a while',
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.itele.fr/france/video/aubervilliers-un-lycee-en-colere-111559',
|
|
||||||
'md5': '38b8f7934def74f0d6f3ba6c036a5f82',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '1213714',
|
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Aubervilliers : un lycée en colère - Le 11/02/2015 à 06h45',
|
'title': "Vacances pas chères : prix discount ou grosses dépenses ? - En quête d'actualité",
|
||||||
'description': 'md5:8216206ec53426ea6321321f3b3c16db',
|
'description': 'md5:edb6cf1cb4a1e807b5dd089e1ac8bfc6',
|
||||||
'upload_date': '20150211',
|
'upload_date': '20160512',
|
||||||
},
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.itele.fr/chroniques/invite-bruce-toussaint/thierry-solere-nicolas-sarkozy-officialisera-sa-candidature-a-la-primaire-quand-il-le-voudra-167224',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1398334',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': "L'invité de Bruce Toussaint du 07/06/2016 - ",
|
||||||
|
'description': 'md5:40ac7c9ad0feaeb6f605bad986f61324',
|
||||||
|
'upload_date': '20160607',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'http://m.canalplus.fr/?vid=1398231',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.d17.tv/emissions/pid8303-lolywood.html?vid=1397061',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
video_id = mobj.groupdict().get('id')
|
video_id = mobj.groupdict().get('id') or mobj.groupdict().get('vid')
|
||||||
|
|
||||||
site_id = self._SITE_ID_MAP[mobj.group('site') or 'canal']
|
site_id = self._SITE_ID_MAP[compat_urllib_parse_urlparse(url).netloc.rsplit('.', 2)[-2]]
|
||||||
|
|
||||||
# Beware, some subclasses do not define an id group
|
# Beware, some subclasses do not define an id group
|
||||||
display_id = url_basename(mobj.group('path'))
|
display_id = mobj.group('display_id') or video_id
|
||||||
|
|
||||||
if video_id is None:
|
if video_id is None:
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
88
youtube_dl/extractor/carambatv.py
Normal file
88
youtube_dl/extractor/carambatv.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import compat_str
|
||||||
|
from ..utils import (
|
||||||
|
float_or_none,
|
||||||
|
int_or_none,
|
||||||
|
try_get,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CarambaTVIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'(?:carambatv:|https?://video1\.carambatv\.ru/v/)(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://video1.carambatv.ru/v/191910501',
|
||||||
|
'md5': '2f4a81b7cfd5ab866ee2d7270cb34a2a',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '191910501',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '[BadComedian] - Разборка в Маниле (Абсолютный обзор)',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg',
|
||||||
|
'duration': 2678.31,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'carambatv:191910501',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
video = self._download_json(
|
||||||
|
'http://video1.carambatv.ru/v/%s/videoinfo.js' % video_id,
|
||||||
|
video_id)
|
||||||
|
|
||||||
|
title = video['title']
|
||||||
|
|
||||||
|
base_url = video.get('video') or 'http://video1.carambatv.ru/v/%s/' % video_id
|
||||||
|
|
||||||
|
formats = [{
|
||||||
|
'url': base_url + f['fn'],
|
||||||
|
'height': int_or_none(f.get('height')),
|
||||||
|
'format_id': '%sp' % f['height'] if f.get('height') else None,
|
||||||
|
} for f in video['qualities'] if f.get('fn')]
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
thumbnail = video.get('splash')
|
||||||
|
duration = float_or_none(try_get(
|
||||||
|
video, lambda x: x['annotations'][0]['end_time'], compat_str))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'duration': duration,
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CarambaTVPageIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://carambatv\.ru/(?:[^/]+/)+(?P<id>[^/?#&]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://carambatv.ru/movie/bad-comedian/razborka-v-manile/',
|
||||||
|
'md5': '',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '191910501',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '[BadComedian] - Разборка в Маниле (Абсолютный обзор)',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
|
'duration': 2678.31,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
video_url = self._og_search_property('video:iframe', webpage, default=None)
|
||||||
|
|
||||||
|
if not video_url:
|
||||||
|
video_id = self._search_regex(
|
||||||
|
r'(?:video_id|crmb_vuid)\s*[:=]\s*["\']?(\d+)',
|
||||||
|
webpage, 'video id')
|
||||||
|
video_url = 'carambatv:%s' % video_id
|
||||||
|
|
||||||
|
return self.url_result(video_url, CarambaTVIE.ie_key())
|
@@ -1,15 +1,15 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .theplatform import ThePlatformIE
|
import re
|
||||||
|
|
||||||
|
from .theplatform import ThePlatformFeedIE
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
xpath_text,
|
|
||||||
xpath_element,
|
|
||||||
int_or_none,
|
int_or_none,
|
||||||
find_xpath_attr,
|
find_xpath_attr,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CBSBaseIE(ThePlatformIE):
|
class CBSBaseIE(ThePlatformFeedIE):
|
||||||
def _parse_smil_subtitles(self, smil, namespace=None, subtitles_lang='en'):
|
def _parse_smil_subtitles(self, smil, namespace=None, subtitles_lang='en'):
|
||||||
closed_caption_e = find_xpath_attr(smil, self._xpath_ns('.//param', namespace), 'name', 'ClosedCaptionURL')
|
closed_caption_e = find_xpath_attr(smil, self._xpath_ns('.//param', namespace), 'name', 'ClosedCaptionURL')
|
||||||
return {
|
return {
|
||||||
@@ -19,9 +19,22 @@ class CBSBaseIE(ThePlatformIE):
|
|||||||
}]
|
}]
|
||||||
} if closed_caption_e is not None and closed_caption_e.attrib.get('value') else []
|
} if closed_caption_e is not None and closed_caption_e.attrib.get('value') else []
|
||||||
|
|
||||||
|
def _extract_video_info(self, filter_query, video_id):
|
||||||
|
return self._extract_feed_info(
|
||||||
|
'dJ5BDC', 'VxxJg8Ymh8sE', filter_query, video_id, lambda entry: {
|
||||||
|
'series': entry.get('cbs$SeriesTitle'),
|
||||||
|
'season_number': int_or_none(entry.get('cbs$SeasonNumber')),
|
||||||
|
'episode': entry.get('cbs$EpisodeTitle'),
|
||||||
|
'episode_number': int_or_none(entry.get('cbs$EpisodeNumber')),
|
||||||
|
}, {
|
||||||
|
'StreamPack': {
|
||||||
|
'manifest': 'm3u',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class CBSIE(CBSBaseIE):
|
class CBSIE(CBSBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?:cbs\.com/shows/[^/]+/(?:video|artist)|colbertlateshow\.com/(?:video|podcasts))/[^/]+/(?P<id>[^/]+)'
|
_VALID_URL = r'(?:cbs:|https?://(?:www\.)?(?:cbs\.com/shows/[^/]+/video|colbertlateshow\.com/(?:video|podcasts))/)(?P<id>[\w-]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.cbs.com/shows/garth-brooks/video/_u7W953k6la293J7EPTd9oHkSPs6Xn6_/connect-chat-feat-garth-brooks/',
|
'url': 'http://www.cbs.com/shows/garth-brooks/video/_u7W953k6la293J7EPTd9oHkSPs6Xn6_/connect-chat-feat-garth-brooks/',
|
||||||
@@ -36,25 +49,7 @@ class CBSIE(CBSBaseIE):
|
|||||||
'upload_date': '20131127',
|
'upload_date': '20131127',
|
||||||
'uploader': 'CBSI-NEW',
|
'uploader': 'CBSI-NEW',
|
||||||
},
|
},
|
||||||
'params': {
|
'expected_warnings': ['Failed to download m3u8 information'],
|
||||||
# rtmp download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'_skip': 'Blocked outside the US',
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.cbs.com/shows/liveonletterman/artist/221752/st-vincent/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'WWF_5KqY3PK1',
|
|
||||||
'display_id': 'st-vincent',
|
|
||||||
'ext': 'flv',
|
|
||||||
'title': 'Live on Letterman - St. Vincent',
|
|
||||||
'description': 'Live On Letterman: St. Vincent in concert from New York\'s Ed Sullivan Theater on Tuesday, July 16, 2014.',
|
|
||||||
'duration': 3221,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# rtmp download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'_skip': 'Blocked outside the US',
|
'_skip': 'Blocked outside the US',
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://colbertlateshow.com/video/8GmB0oY0McANFvp2aEffk9jZZZ2YyXxy/the-colbeard/',
|
'url': 'http://colbertlateshow.com/video/8GmB0oY0McANFvp2aEffk9jZZZ2YyXxy/the-colbeard/',
|
||||||
@@ -66,43 +61,5 @@ class CBSIE(CBSBaseIE):
|
|||||||
TP_RELEASE_URL_TEMPLATE = 'http://link.theplatform.com/s/dJ5BDC/%s?mbr=true'
|
TP_RELEASE_URL_TEMPLATE = 'http://link.theplatform.com/s/dJ5BDC/%s?mbr=true'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
content_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, display_id)
|
return self._extract_video_info('byGuid=%s' % content_id, content_id)
|
||||||
content_id = self._search_regex(
|
|
||||||
[r"video\.settings\.content_id\s*=\s*'([^']+)';", r"cbsplayer\.contentId\s*=\s*'([^']+)';"],
|
|
||||||
webpage, 'content id')
|
|
||||||
items_data = self._download_xml(
|
|
||||||
'http://can.cbs.com/thunder/player/videoPlayerService.php',
|
|
||||||
content_id, query={'partner': 'cbs', 'contentId': content_id})
|
|
||||||
video_data = xpath_element(items_data, './/item')
|
|
||||||
title = xpath_text(video_data, 'videoTitle', 'title', True)
|
|
||||||
|
|
||||||
subtitles = {}
|
|
||||||
formats = []
|
|
||||||
for item in items_data.findall('.//item'):
|
|
||||||
pid = xpath_text(item, 'pid')
|
|
||||||
if not pid:
|
|
||||||
continue
|
|
||||||
tp_release_url = self.TP_RELEASE_URL_TEMPLATE % pid
|
|
||||||
if '.m3u8' in xpath_text(item, 'contentUrl', default=''):
|
|
||||||
tp_release_url += '&manifest=m3u'
|
|
||||||
tp_formats, tp_subtitles = self._extract_theplatform_smil(
|
|
||||||
tp_release_url, content_id, 'Downloading %s SMIL data' % pid)
|
|
||||||
formats.extend(tp_formats)
|
|
||||||
subtitles = self._merge_subtitles(subtitles, tp_subtitles)
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
info = self.get_metadata('dJ5BDC/media/guid/2198311517/%s' % content_id, content_id)
|
|
||||||
info.update({
|
|
||||||
'id': content_id,
|
|
||||||
'display_id': display_id,
|
|
||||||
'title': title,
|
|
||||||
'series': xpath_text(video_data, 'seriesTitle'),
|
|
||||||
'season_number': int_or_none(xpath_text(video_data, 'seasonNumber')),
|
|
||||||
'episode_number': int_or_none(xpath_text(video_data, 'episodeNumber')),
|
|
||||||
'duration': int_or_none(xpath_text(video_data, 'videoLength'), 1000),
|
|
||||||
'thumbnail': xpath_text(video_data, 'previewImageURL'),
|
|
||||||
'formats': formats,
|
|
||||||
'subtitles': subtitles,
|
|
||||||
})
|
|
||||||
return info
|
|
||||||
|
@@ -30,9 +30,12 @@ class CBSNewsIE(CBSBaseIE):
|
|||||||
{
|
{
|
||||||
'url': 'http://www.cbsnews.com/videos/fort-hood-shooting-army-downplays-mental-illness-as-cause-of-attack/',
|
'url': 'http://www.cbsnews.com/videos/fort-hood-shooting-army-downplays-mental-illness-as-cause-of-attack/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'fort-hood-shooting-army-downplays-mental-illness-as-cause-of-attack',
|
'id': 'SNJBOYzXiWBOvaLsdzwH8fmtP1SCd91Y',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Fort Hood shooting: Army downplays mental illness as cause of attack',
|
'title': 'Fort Hood shooting: Army downplays mental illness as cause of attack',
|
||||||
|
'description': 'md5:4a6983e480542d8b333a947bfc64ddc7',
|
||||||
|
'upload_date': '19700101',
|
||||||
|
'uploader': 'CBSI-NEW',
|
||||||
'thumbnail': 're:^https?://.*\.jpg$',
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
'duration': 205,
|
'duration': 205,
|
||||||
'subtitles': {
|
'subtitles': {
|
||||||
@@ -58,30 +61,8 @@ class CBSNewsIE(CBSBaseIE):
|
|||||||
webpage, 'video JSON info'), video_id)
|
webpage, 'video JSON info'), video_id)
|
||||||
|
|
||||||
item = video_info['item'] if 'item' in video_info else video_info
|
item = video_info['item'] if 'item' in video_info else video_info
|
||||||
title = item.get('articleTitle') or item.get('hed')
|
guid = item['mpxRefId']
|
||||||
duration = item.get('duration')
|
return self._extract_video_info('byGuid=%s' % guid, guid)
|
||||||
thumbnail = item.get('mediaImage') or item.get('thumbnail')
|
|
||||||
|
|
||||||
subtitles = {}
|
|
||||||
formats = []
|
|
||||||
for format_id in ['RtmpMobileLow', 'RtmpMobileHigh', 'Hls', 'RtmpDesktop']:
|
|
||||||
pid = item.get('media' + format_id)
|
|
||||||
if not pid:
|
|
||||||
continue
|
|
||||||
release_url = 'http://link.theplatform.com/s/dJ5BDC/%s?mbr=true' % pid
|
|
||||||
tp_formats, tp_subtitles = self._extract_theplatform_smil(release_url, video_id, 'Downloading %s SMIL data' % pid)
|
|
||||||
formats.extend(tp_formats)
|
|
||||||
subtitles = self._merge_subtitles(subtitles, tp_subtitles)
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'thumbnail': thumbnail,
|
|
||||||
'duration': duration,
|
|
||||||
'formats': formats,
|
|
||||||
'subtitles': subtitles,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CBSNewsLiveVideoIE(InfoExtractor):
|
class CBSNewsLiveVideoIE(InfoExtractor):
|
||||||
|
@@ -1,30 +1,28 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
from .cbs import CBSBaseIE
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
|
|
||||||
|
|
||||||
class CBSSportsIE(InfoExtractor):
|
class CBSSportsIE(CBSBaseIE):
|
||||||
_VALID_URL = r'https?://www\.cbssports\.com/video/player/(?P<section>[^/]+)/(?P<id>[^/]+)'
|
_VALID_URL = r'https?://www\.cbssports\.com/video/player/[^/]+/(?P<id>\d+)'
|
||||||
|
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://www.cbssports.com/video/player/tennis/318462531970/0/us-open-flashbacks-1990s',
|
'url': 'http://www.cbssports.com/video/player/videos/708337219968/0/ben-simmons-the-next-lebron?-not-so-fast',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '_d5_GbO8p1sT',
|
'id': '708337219968',
|
||||||
'ext': 'flv',
|
'ext': 'mp4',
|
||||||
'title': 'US Open flashbacks: 1990s',
|
'title': 'Ben Simmons the next LeBron? Not so fast',
|
||||||
'description': 'Bill Macatee relives the best moments in US Open history from the 1990s.',
|
'description': 'md5:854294f627921baba1f4b9a990d87197',
|
||||||
|
'timestamp': 1466293740,
|
||||||
|
'upload_date': '20160618',
|
||||||
|
'uploader': 'CBSI-NEW',
|
||||||
},
|
},
|
||||||
}
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
video_id = self._match_id(url)
|
||||||
section = mobj.group('section')
|
return self._extract_video_info('byId=%s' % video_id, video_id)
|
||||||
video_id = mobj.group('id')
|
|
||||||
all_videos = self._download_json(
|
|
||||||
'http://www.cbssports.com/data/video/player/getVideos/%s?as=json' % section,
|
|
||||||
video_id)
|
|
||||||
# The json file contains the info of all the videos in the section
|
|
||||||
video_info = next(v for v in all_videos if v['pcid'] == video_id)
|
|
||||||
return self.url_result('theplatform:%s' % video_info['pid'], 'ThePlatform')
|
|
||||||
|
@@ -58,7 +58,8 @@ class CDAIE(InfoExtractor):
|
|||||||
def extract_format(page, version):
|
def extract_format(page, version):
|
||||||
unpacked = decode_packed_codes(page)
|
unpacked = decode_packed_codes(page)
|
||||||
format_url = self._search_regex(
|
format_url = self._search_regex(
|
||||||
r"url:\\'(.+?)\\'", unpacked, '%s url' % version, fatal=False)
|
r"(?:file|url)\s*:\s*(\\?[\"'])(?P<url>http.+?)\1", unpacked,
|
||||||
|
'%s url' % version, fatal=False, group='url')
|
||||||
if not format_url:
|
if not format_url:
|
||||||
return
|
return
|
||||||
f = {
|
f = {
|
||||||
@@ -75,7 +76,8 @@ class CDAIE(InfoExtractor):
|
|||||||
info_dict['formats'].append(f)
|
info_dict['formats'].append(f)
|
||||||
if not info_dict['duration']:
|
if not info_dict['duration']:
|
||||||
info_dict['duration'] = parse_duration(self._search_regex(
|
info_dict['duration'] = parse_duration(self._search_regex(
|
||||||
r"duration:\\'(.+?)\\'", unpacked, 'duration', fatal=False))
|
r"duration\s*:\s*(\\?[\"'])(?P<duration>.+?)\1",
|
||||||
|
unpacked, 'duration', fatal=False, group='duration'))
|
||||||
|
|
||||||
extract_format(webpage, 'default')
|
extract_format(webpage, 'default')
|
||||||
|
|
||||||
|
@@ -20,54 +20,64 @@ class Channel9IE(InfoExtractor):
|
|||||||
'''
|
'''
|
||||||
IE_DESC = 'Channel 9'
|
IE_DESC = 'Channel 9'
|
||||||
IE_NAME = 'channel9'
|
IE_NAME = 'channel9'
|
||||||
_VALID_URL = r'https?://(?:www\.)?channel9\.msdn\.com/(?P<contentpath>.+)/?'
|
_VALID_URL = r'https?://(?:www\.)?channel9\.msdn\.com/(?P<contentpath>.+?)(?P<rss>/RSS)?/?(?:[?#&]|$)'
|
||||||
|
|
||||||
_TESTS = [
|
_TESTS = [{
|
||||||
{
|
'url': 'http://channel9.msdn.com/Events/TechEd/Australia/2013/KOS002',
|
||||||
'url': 'http://channel9.msdn.com/Events/TechEd/Australia/2013/KOS002',
|
'md5': 'bbd75296ba47916b754e73c3a4bbdf10',
|
||||||
'md5': 'bbd75296ba47916b754e73c3a4bbdf10',
|
'info_dict': {
|
||||||
'info_dict': {
|
'id': 'Events/TechEd/Australia/2013/KOS002',
|
||||||
'id': 'Events/TechEd/Australia/2013/KOS002',
|
'ext': 'mp4',
|
||||||
'ext': 'mp4',
|
'title': 'Developer Kick-Off Session: Stuff We Love',
|
||||||
'title': 'Developer Kick-Off Session: Stuff We Love',
|
'description': 'md5:c08d72240b7c87fcecafe2692f80e35f',
|
||||||
'description': 'md5:c08d72240b7c87fcecafe2692f80e35f',
|
'duration': 4576,
|
||||||
'duration': 4576,
|
'thumbnail': 're:http://.*\.jpg',
|
||||||
'thumbnail': 're:http://.*\.jpg',
|
'session_code': 'KOS002',
|
||||||
'session_code': 'KOS002',
|
'session_day': 'Day 1',
|
||||||
'session_day': 'Day 1',
|
'session_room': 'Arena 1A',
|
||||||
'session_room': 'Arena 1A',
|
'session_speakers': ['Ed Blankenship', 'Andrew Coates', 'Brady Gaster', 'Patrick Klug',
|
||||||
'session_speakers': ['Ed Blankenship', 'Andrew Coates', 'Brady Gaster', 'Patrick Klug', 'Mads Kristensen'],
|
'Mads Kristensen'],
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
}, {
|
||||||
'url': 'http://channel9.msdn.com/posts/Self-service-BI-with-Power-BI-nuclear-testing',
|
'url': 'http://channel9.msdn.com/posts/Self-service-BI-with-Power-BI-nuclear-testing',
|
||||||
'md5': 'b43ee4529d111bc37ba7ee4f34813e68',
|
'md5': 'b43ee4529d111bc37ba7ee4f34813e68',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'posts/Self-service-BI-with-Power-BI-nuclear-testing',
|
'id': 'posts/Self-service-BI-with-Power-BI-nuclear-testing',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Self-service BI with Power BI - nuclear testing',
|
'title': 'Self-service BI with Power BI - nuclear testing',
|
||||||
'description': 'md5:d1e6ecaafa7fb52a2cacdf9599829f5b',
|
'description': 'md5:d1e6ecaafa7fb52a2cacdf9599829f5b',
|
||||||
'duration': 1540,
|
'duration': 1540,
|
||||||
'thumbnail': 're:http://.*\.jpg',
|
'thumbnail': 're:http://.*\.jpg',
|
||||||
'authors': ['Mike Wilmot'],
|
'authors': ['Mike Wilmot'],
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
}, {
|
||||||
# low quality mp4 is best
|
# low quality mp4 is best
|
||||||
'url': 'https://channel9.msdn.com/Events/CPP/CppCon-2015/Ranges-for-the-Standard-Library',
|
'url': 'https://channel9.msdn.com/Events/CPP/CppCon-2015/Ranges-for-the-Standard-Library',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'Events/CPP/CppCon-2015/Ranges-for-the-Standard-Library',
|
'id': 'Events/CPP/CppCon-2015/Ranges-for-the-Standard-Library',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Ranges for the Standard Library',
|
'title': 'Ranges for the Standard Library',
|
||||||
'description': 'md5:2e6b4917677af3728c5f6d63784c4c5d',
|
'description': 'md5:2e6b4917677af3728c5f6d63784c4c5d',
|
||||||
'duration': 5646,
|
'duration': 5646,
|
||||||
'thumbnail': 're:http://.*\.jpg',
|
'thumbnail': 're:http://.*\.jpg',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
}
|
}, {
|
||||||
]
|
'url': 'https://channel9.msdn.com/Niners/Splendid22/Queue/76acff796e8f411184b008028e0d492b/RSS',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'Niners/Splendid22/Queue/76acff796e8f411184b008028e0d492b',
|
||||||
|
'title': 'Channel 9',
|
||||||
|
},
|
||||||
|
'playlist_count': 2,
|
||||||
|
}, {
|
||||||
|
'url': 'https://channel9.msdn.com/Events/DEVintersection/DEVintersection-2016/RSS',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://channel9.msdn.com/Events/Speakers/scott-hanselman/RSS?UrlSafeName=scott-hanselman',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
_RSS_URL = 'http://channel9.msdn.com/%s/RSS'
|
_RSS_URL = 'http://channel9.msdn.com/%s/RSS'
|
||||||
|
|
||||||
@@ -254,22 +264,30 @@ class Channel9IE(InfoExtractor):
|
|||||||
|
|
||||||
return self.playlist_result(contents)
|
return self.playlist_result(contents)
|
||||||
|
|
||||||
def _extract_list(self, content_path):
|
def _extract_list(self, video_id, rss_url=None):
|
||||||
rss = self._download_xml(self._RSS_URL % content_path, content_path, 'Downloading RSS')
|
if not rss_url:
|
||||||
|
rss_url = self._RSS_URL % video_id
|
||||||
|
rss = self._download_xml(rss_url, video_id, 'Downloading RSS')
|
||||||
entries = [self.url_result(session_url.text, 'Channel9')
|
entries = [self.url_result(session_url.text, 'Channel9')
|
||||||
for session_url in rss.findall('./channel/item/link')]
|
for session_url in rss.findall('./channel/item/link')]
|
||||||
title_text = rss.find('./channel/title').text
|
title_text = rss.find('./channel/title').text
|
||||||
return self.playlist_result(entries, content_path, title_text)
|
return self.playlist_result(entries, video_id, title_text)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
content_path = mobj.group('contentpath')
|
content_path = mobj.group('contentpath')
|
||||||
|
rss = mobj.group('rss')
|
||||||
|
|
||||||
webpage = self._download_webpage(url, content_path, 'Downloading web page')
|
if rss:
|
||||||
|
return self._extract_list(content_path, url)
|
||||||
|
|
||||||
page_type_m = re.search(r'<meta name="WT.entryid" content="(?P<pagetype>[^:]+)[^"]+"/>', webpage)
|
webpage = self._download_webpage(
|
||||||
if page_type_m is not None:
|
url, content_path, 'Downloading web page')
|
||||||
page_type = page_type_m.group('pagetype')
|
|
||||||
|
page_type = self._search_regex(
|
||||||
|
r'<meta[^>]+name=(["\'])WT\.entryid\1[^>]+content=(["\'])(?P<pagetype>[^:]+).+?\2',
|
||||||
|
webpage, 'page type', default=None, group='pagetype')
|
||||||
|
if page_type:
|
||||||
if page_type == 'Entry': # Any 'item'-like page, may contain downloadable content
|
if page_type == 'Entry': # Any 'item'-like page, may contain downloadable content
|
||||||
return self._extract_entry_item(webpage, content_path)
|
return self._extract_entry_item(webpage, content_path)
|
||||||
elif page_type == 'Session': # Event session page, may contain downloadable content
|
elif page_type == 'Session': # Event session page, may contain downloadable content
|
||||||
@@ -278,6 +296,5 @@ class Channel9IE(InfoExtractor):
|
|||||||
return self._extract_list(content_path)
|
return self._extract_list(content_path)
|
||||||
else:
|
else:
|
||||||
raise ExtractorError('Unexpected WT.entryid %s' % page_type, expected=True)
|
raise ExtractorError('Unexpected WT.entryid %s' % page_type, expected=True)
|
||||||
|
|
||||||
else: # Assuming list
|
else: # Assuming list
|
||||||
return self._extract_list(content_path)
|
return self._extract_list(content_path)
|
||||||
|
92
youtube_dl/extractor/closertotruth.py
Normal file
92
youtube_dl/extractor/closertotruth.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class CloserToTruthIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?closertotruth\.com/(?:[^/]+/)*(?P<id>[^/?#&]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://closertotruth.com/series/solutions-the-mind-body-problem#video-3688',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '0_zof1ktre',
|
||||||
|
'display_id': 'solutions-the-mind-body-problem',
|
||||||
|
'ext': 'mov',
|
||||||
|
'title': 'Solutions to the Mind-Body Problem?',
|
||||||
|
'upload_date': '20140221',
|
||||||
|
'timestamp': 1392956007,
|
||||||
|
'uploader_id': 'CTTXML'
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'http://closertotruth.com/episodes/how-do-brains-work',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '0_iuxai6g6',
|
||||||
|
'display_id': 'how-do-brains-work',
|
||||||
|
'ext': 'mov',
|
||||||
|
'title': 'How do Brains Work?',
|
||||||
|
'upload_date': '20140221',
|
||||||
|
'timestamp': 1392956024,
|
||||||
|
'uploader_id': 'CTTXML'
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'http://closertotruth.com/interviews/1725',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1725',
|
||||||
|
'title': 'AyaFr-002',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 2,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
|
||||||
|
partner_id = self._search_regex(
|
||||||
|
r'<script[^>]+src=["\'].*?\b(?:partner_id|p)/(\d+)',
|
||||||
|
webpage, 'kaltura partner_id')
|
||||||
|
|
||||||
|
title = self._search_regex(
|
||||||
|
r'<title>(.+?)\s*\|\s*.+?</title>', webpage, 'video title')
|
||||||
|
|
||||||
|
select = self._search_regex(
|
||||||
|
r'(?s)<select[^>]+id="select-version"[^>]*>(.+?)</select>',
|
||||||
|
webpage, 'select version', default=None)
|
||||||
|
if select:
|
||||||
|
entry_ids = set()
|
||||||
|
entries = []
|
||||||
|
for mobj in re.finditer(
|
||||||
|
r'<option[^>]+value=(["\'])(?P<id>[0-9a-z_]+)(?:#.+?)?\1[^>]*>(?P<title>[^<]+)',
|
||||||
|
webpage):
|
||||||
|
entry_id = mobj.group('id')
|
||||||
|
if entry_id in entry_ids:
|
||||||
|
continue
|
||||||
|
entry_ids.add(entry_id)
|
||||||
|
entries.append({
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'url': 'kaltura:%s:%s' % (partner_id, entry_id),
|
||||||
|
'ie_key': 'Kaltura',
|
||||||
|
'title': mobj.group('title'),
|
||||||
|
})
|
||||||
|
if entries:
|
||||||
|
return self.playlist_result(entries, display_id, title)
|
||||||
|
|
||||||
|
entry_id = self._search_regex(
|
||||||
|
r'<a[^>]+id=(["\'])embed-kaltura\1[^>]+data-kaltura=(["\'])(?P<id>[0-9a-z_]+)\2',
|
||||||
|
webpage, 'kaltura entry_id', group='id')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'display_id': display_id,
|
||||||
|
'url': 'kaltura:%s:%s' % (partner_id, entry_id),
|
||||||
|
'ie_key': 'Kaltura',
|
||||||
|
'title': title
|
||||||
|
}
|
@@ -44,10 +44,10 @@ class ComedyCentralShowsIE(MTVServicesInfoExtractor):
|
|||||||
# or: http://www.colbertnation.com/the-colbert-report-collections/422008/festival-of-lights/79524
|
# or: http://www.colbertnation.com/the-colbert-report-collections/422008/festival-of-lights/79524
|
||||||
_VALID_URL = r'''(?x)^(:(?P<shortname>tds|thedailyshow)
|
_VALID_URL = r'''(?x)^(:(?P<shortname>tds|thedailyshow)
|
||||||
|https?://(:www\.)?
|
|https?://(:www\.)?
|
||||||
(?P<showname>thedailyshow|thecolbertreport)\.(?:cc\.)?com/
|
(?P<showname>thedailyshow|thecolbertreport|tosh)\.(?:cc\.)?com/
|
||||||
((?:full-)?episodes/(?:[0-9a-z]{6}/)?(?P<episode>.*)|
|
((?:full-)?episodes/(?:[0-9a-z]{6}/)?(?P<episode>.*)|
|
||||||
(?P<clip>
|
(?P<clip>
|
||||||
(?:(?:guests/[^/]+|videos|video-playlists|special-editions|news-team/[^/]+)/[^/]+/(?P<videotitle>[^/?#]+))
|
(?:(?:guests/[^/]+|videos|video-(?:clips|playlists)|special-editions|news-team/[^/]+)/[^/]+/(?P<videotitle>[^/?#]+))
|
||||||
|(the-colbert-report-(videos|collections)/(?P<clipID>[0-9]+)/[^/]*/(?P<cntitle>.*?))
|
|(the-colbert-report-(videos|collections)/(?P<clipID>[0-9]+)/[^/]*/(?P<cntitle>.*?))
|
||||||
|(watch/(?P<date>[^/]*)/(?P<tdstitle>.*))
|
|(watch/(?P<date>[^/]*)/(?P<tdstitle>.*))
|
||||||
)|
|
)|
|
||||||
@@ -129,6 +129,9 @@ class ComedyCentralShowsIE(MTVServicesInfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://thedailyshow.cc.com/news-team/michael-che/7wnfel/we-need-to-talk-about-israel',
|
'url': 'http://thedailyshow.cc.com/news-team/michael-che/7wnfel/we-need-to-talk-about-israel',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://tosh.cc.com/video-clips/68g93d/twitter-users-share-summer-plans',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_available_formats = ['3500', '2200', '1700', '1200', '750', '400']
|
_available_formats = ['3500', '2200', '1700', '1200', '750', '400']
|
||||||
|
@@ -45,6 +45,7 @@ from ..utils import (
|
|||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
url_basename,
|
url_basename,
|
||||||
|
xpath_element,
|
||||||
xpath_text,
|
xpath_text,
|
||||||
xpath_with_ns,
|
xpath_with_ns,
|
||||||
determine_protocol,
|
determine_protocol,
|
||||||
@@ -52,6 +53,7 @@ from ..utils import (
|
|||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
update_Request,
|
update_Request,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
|
parse_m3u8_attributes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -987,7 +989,7 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
def _extract_f4m_formats(self, manifest_url, video_id, preference=None, f4m_id=None,
|
def _extract_f4m_formats(self, manifest_url, video_id, preference=None, f4m_id=None,
|
||||||
transform_source=lambda s: fix_xml_ampersands(s).strip(),
|
transform_source=lambda s: fix_xml_ampersands(s).strip(),
|
||||||
fatal=True):
|
fatal=True, m3u8_id=None):
|
||||||
manifest = self._download_xml(
|
manifest = self._download_xml(
|
||||||
manifest_url, video_id, 'Downloading f4m manifest',
|
manifest_url, video_id, 'Downloading f4m manifest',
|
||||||
'Unable to download f4m manifest',
|
'Unable to download f4m manifest',
|
||||||
@@ -1001,11 +1003,11 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
return self._parse_f4m_formats(
|
return self._parse_f4m_formats(
|
||||||
manifest, manifest_url, video_id, preference=preference, f4m_id=f4m_id,
|
manifest, manifest_url, video_id, preference=preference, f4m_id=f4m_id,
|
||||||
transform_source=transform_source, fatal=fatal)
|
transform_source=transform_source, fatal=fatal, m3u8_id=m3u8_id)
|
||||||
|
|
||||||
def _parse_f4m_formats(self, manifest, manifest_url, video_id, preference=None, f4m_id=None,
|
def _parse_f4m_formats(self, manifest, manifest_url, video_id, preference=None, f4m_id=None,
|
||||||
transform_source=lambda s: fix_xml_ampersands(s).strip(),
|
transform_source=lambda s: fix_xml_ampersands(s).strip(),
|
||||||
fatal=True):
|
fatal=True, m3u8_id=None):
|
||||||
# currently youtube-dl cannot decode the playerVerificationChallenge as Akamai uses Adobe Alchemy
|
# currently youtube-dl cannot decode the playerVerificationChallenge as Akamai uses Adobe Alchemy
|
||||||
akamai_pv = manifest.find('{http://ns.adobe.com/f4m/1.0}pv-2.0')
|
akamai_pv = manifest.find('{http://ns.adobe.com/f4m/1.0}pv-2.0')
|
||||||
if akamai_pv is not None and ';' in akamai_pv.text:
|
if akamai_pv is not None and ';' in akamai_pv.text:
|
||||||
@@ -1029,9 +1031,26 @@ class InfoExtractor(object):
|
|||||||
'base URL', default=None)
|
'base URL', default=None)
|
||||||
if base_url:
|
if base_url:
|
||||||
base_url = base_url.strip()
|
base_url = base_url.strip()
|
||||||
|
|
||||||
|
bootstrap_info = xpath_element(
|
||||||
|
manifest, ['{http://ns.adobe.com/f4m/1.0}bootstrapInfo', '{http://ns.adobe.com/f4m/2.0}bootstrapInfo'],
|
||||||
|
'bootstrap info', default=None)
|
||||||
|
|
||||||
for i, media_el in enumerate(media_nodes):
|
for i, media_el in enumerate(media_nodes):
|
||||||
if manifest_version == '2.0':
|
tbr = int_or_none(media_el.attrib.get('bitrate'))
|
||||||
media_url = media_el.attrib.get('href') or media_el.attrib.get('url')
|
width = int_or_none(media_el.attrib.get('width'))
|
||||||
|
height = int_or_none(media_el.attrib.get('height'))
|
||||||
|
format_id = '-'.join(filter(None, [f4m_id, compat_str(i if tbr is None else tbr)]))
|
||||||
|
# If <bootstrapInfo> is present, the specified f4m is a
|
||||||
|
# stream-level manifest, and only set-level manifests may refer to
|
||||||
|
# external resources. See section 11.4 and section 4 of F4M spec
|
||||||
|
if bootstrap_info is None:
|
||||||
|
media_url = None
|
||||||
|
# @href is introduced in 2.0, see section 11.6 of F4M spec
|
||||||
|
if manifest_version == '2.0':
|
||||||
|
media_url = media_el.attrib.get('href')
|
||||||
|
if media_url is None:
|
||||||
|
media_url = media_el.attrib.get('url')
|
||||||
if not media_url:
|
if not media_url:
|
||||||
continue
|
continue
|
||||||
manifest_url = (
|
manifest_url = (
|
||||||
@@ -1041,19 +1060,37 @@ class InfoExtractor(object):
|
|||||||
# since bitrates in parent manifest (this one) and media_url manifest
|
# since bitrates in parent manifest (this one) and media_url manifest
|
||||||
# may differ leading to inability to resolve the format by requested
|
# may differ leading to inability to resolve the format by requested
|
||||||
# bitrate in f4m downloader
|
# bitrate in f4m downloader
|
||||||
if determine_ext(manifest_url) == 'f4m':
|
ext = determine_ext(manifest_url)
|
||||||
formats.extend(self._extract_f4m_formats(
|
if ext == 'f4m':
|
||||||
|
f4m_formats = self._extract_f4m_formats(
|
||||||
manifest_url, video_id, preference=preference, f4m_id=f4m_id,
|
manifest_url, video_id, preference=preference, f4m_id=f4m_id,
|
||||||
transform_source=transform_source, fatal=fatal))
|
transform_source=transform_source, fatal=fatal)
|
||||||
|
# Sometimes stream-level manifest contains single media entry that
|
||||||
|
# does not contain any quality metadata (e.g. http://matchtv.ru/#live-player).
|
||||||
|
# At the same time parent's media entry in set-level manifest may
|
||||||
|
# contain it. We will copy it from parent in such cases.
|
||||||
|
if len(f4m_formats) == 1:
|
||||||
|
f = f4m_formats[0]
|
||||||
|
f.update({
|
||||||
|
'tbr': f.get('tbr') or tbr,
|
||||||
|
'width': f.get('width') or width,
|
||||||
|
'height': f.get('height') or height,
|
||||||
|
'format_id': f.get('format_id') if not tbr else format_id,
|
||||||
|
})
|
||||||
|
formats.extend(f4m_formats)
|
||||||
|
continue
|
||||||
|
elif ext == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
manifest_url, video_id, 'mp4', preference=preference,
|
||||||
|
m3u8_id=m3u8_id, fatal=fatal))
|
||||||
continue
|
continue
|
||||||
tbr = int_or_none(media_el.attrib.get('bitrate'))
|
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': '-'.join(filter(None, [f4m_id, compat_str(i if tbr is None else tbr)])),
|
'format_id': format_id,
|
||||||
'url': manifest_url,
|
'url': manifest_url,
|
||||||
'ext': 'flv',
|
'ext': 'flv' if bootstrap_info is not None else None,
|
||||||
'tbr': tbr,
|
'tbr': tbr,
|
||||||
'width': int_or_none(media_el.attrib.get('width')),
|
'width': width,
|
||||||
'height': int_or_none(media_el.attrib.get('height')),
|
'height': height,
|
||||||
'preference': preference,
|
'preference': preference,
|
||||||
})
|
})
|
||||||
return formats
|
return formats
|
||||||
@@ -1114,23 +1151,11 @@ class InfoExtractor(object):
|
|||||||
}]
|
}]
|
||||||
last_info = None
|
last_info = None
|
||||||
last_media = None
|
last_media = None
|
||||||
kv_rex = re.compile(
|
|
||||||
r'(?P<key>[a-zA-Z_-]+)=(?P<val>"[^"]+"|[^",]+)(?:,|$)')
|
|
||||||
for line in m3u8_doc.splitlines():
|
for line in m3u8_doc.splitlines():
|
||||||
if line.startswith('#EXT-X-STREAM-INF:'):
|
if line.startswith('#EXT-X-STREAM-INF:'):
|
||||||
last_info = {}
|
last_info = parse_m3u8_attributes(line)
|
||||||
for m in kv_rex.finditer(line):
|
|
||||||
v = m.group('val')
|
|
||||||
if v.startswith('"'):
|
|
||||||
v = v[1:-1]
|
|
||||||
last_info[m.group('key')] = v
|
|
||||||
elif line.startswith('#EXT-X-MEDIA:'):
|
elif line.startswith('#EXT-X-MEDIA:'):
|
||||||
last_media = {}
|
last_media = parse_m3u8_attributes(line)
|
||||||
for m in kv_rex.finditer(line):
|
|
||||||
v = m.group('val')
|
|
||||||
if v.startswith('"'):
|
|
||||||
v = v[1:-1]
|
|
||||||
last_media[m.group('key')] = v
|
|
||||||
elif line.startswith('#') or not line.strip():
|
elif line.startswith('#') or not line.strip():
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
|
143
youtube_dl/extractor/coub.py
Normal file
143
youtube_dl/extractor/coub.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
float_or_none,
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
qualities,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CoubIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'(?:coub:|https?://(?:coub\.com/(?:view|embed|coubs)/|c-cdn\.coub\.com/fb-player\.swf\?.*\bcoub(?:ID|id)=))(?P<id>[\da-z]+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://coub.com/view/5u5n1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5u5n1',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'The Matrix Moonwalk',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
|
'duration': 4.6,
|
||||||
|
'timestamp': 1428527772,
|
||||||
|
'upload_date': '20150408',
|
||||||
|
'uploader': 'Артём Лоскутников',
|
||||||
|
'uploader_id': 'artyom.loskutnikov',
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'repost_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'age_limit': 0,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'http://c-cdn.coub.com/fb-player.swf?bot_type=vk&coubID=7w5a4',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'coub:5u5n1',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# longer video id
|
||||||
|
'url': 'http://coub.com/view/237d5l5h',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
coub = self._download_json(
|
||||||
|
'http://coub.com/api/v2/coubs/%s.json' % video_id, video_id)
|
||||||
|
|
||||||
|
if coub.get('error'):
|
||||||
|
raise ExtractorError(
|
||||||
|
'%s said: %s' % (self.IE_NAME, coub['error']), expected=True)
|
||||||
|
|
||||||
|
title = coub['title']
|
||||||
|
|
||||||
|
file_versions = coub['file_versions']
|
||||||
|
|
||||||
|
QUALITIES = ('low', 'med', 'high')
|
||||||
|
|
||||||
|
MOBILE = 'mobile'
|
||||||
|
IPHONE = 'iphone'
|
||||||
|
HTML5 = 'html5'
|
||||||
|
|
||||||
|
SOURCE_PREFERENCE = (MOBILE, IPHONE, HTML5)
|
||||||
|
|
||||||
|
quality_key = qualities(QUALITIES)
|
||||||
|
preference_key = qualities(SOURCE_PREFERENCE)
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
|
||||||
|
for kind, items in file_versions.get(HTML5, {}).items():
|
||||||
|
if kind not in ('video', 'audio'):
|
||||||
|
continue
|
||||||
|
if not isinstance(items, dict):
|
||||||
|
continue
|
||||||
|
for quality, item in items.items():
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
item_url = item.get('url')
|
||||||
|
if not item_url:
|
||||||
|
continue
|
||||||
|
formats.append({
|
||||||
|
'url': item_url,
|
||||||
|
'format_id': '%s-%s-%s' % (HTML5, kind, quality),
|
||||||
|
'filesize': int_or_none(item.get('size')),
|
||||||
|
'vcodec': 'none' if kind == 'audio' else None,
|
||||||
|
'quality': quality_key(quality),
|
||||||
|
'preference': preference_key(HTML5),
|
||||||
|
})
|
||||||
|
|
||||||
|
iphone_url = file_versions.get(IPHONE, {}).get('url')
|
||||||
|
if iphone_url:
|
||||||
|
formats.append({
|
||||||
|
'url': iphone_url,
|
||||||
|
'format_id': IPHONE,
|
||||||
|
'preference': preference_key(IPHONE),
|
||||||
|
})
|
||||||
|
|
||||||
|
mobile_url = file_versions.get(MOBILE, {}).get('audio_url')
|
||||||
|
if mobile_url:
|
||||||
|
formats.append({
|
||||||
|
'url': mobile_url,
|
||||||
|
'format_id': '%s-audio' % MOBILE,
|
||||||
|
'preference': preference_key(MOBILE),
|
||||||
|
})
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
thumbnail = coub.get('picture')
|
||||||
|
duration = float_or_none(coub.get('duration'))
|
||||||
|
timestamp = parse_iso8601(coub.get('published_at') or coub.get('created_at'))
|
||||||
|
uploader = coub.get('channel', {}).get('title')
|
||||||
|
uploader_id = coub.get('channel', {}).get('permalink')
|
||||||
|
|
||||||
|
view_count = int_or_none(coub.get('views_count') or coub.get('views_increase_count'))
|
||||||
|
like_count = int_or_none(coub.get('likes_count'))
|
||||||
|
repost_count = int_or_none(coub.get('recoubs_count'))
|
||||||
|
comment_count = int_or_none(coub.get('comments_count'))
|
||||||
|
|
||||||
|
age_restricted = coub.get('age_restricted', coub.get('age_restricted_by_admin'))
|
||||||
|
if age_restricted is not None:
|
||||||
|
age_limit = 18 if age_restricted is True else 0
|
||||||
|
else:
|
||||||
|
age_limit = None
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'duration': duration,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'uploader': uploader,
|
||||||
|
'uploader_id': uploader_id,
|
||||||
|
'view_count': view_count,
|
||||||
|
'like_count': like_count,
|
||||||
|
'repost_count': repost_count,
|
||||||
|
'comment_count': comment_count,
|
||||||
|
'age_limit': age_limit,
|
||||||
|
'formats': formats,
|
||||||
|
}
|
@@ -2,13 +2,16 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import int_or_none
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
unified_strdate,
|
||||||
|
)
|
||||||
from ..compat import compat_urlparse
|
from ..compat import compat_urlparse
|
||||||
|
|
||||||
|
|
||||||
class DWIE(InfoExtractor):
|
class DWIE(InfoExtractor):
|
||||||
IE_NAME = 'dw'
|
IE_NAME = 'dw'
|
||||||
_VALID_URL = r'https?://(?:www\.)?dw\.com/(?:[^/]+/)+av-(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?dw\.com/(?:[^/]+/)+(?:av|e)-(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# video
|
# video
|
||||||
'url': 'http://www.dw.com/en/intelligent-light/av-19112290',
|
'url': 'http://www.dw.com/en/intelligent-light/av-19112290',
|
||||||
@@ -31,6 +34,18 @@ class DWIE(InfoExtractor):
|
|||||||
'description': 'md5:bc9ca6e4e063361e21c920c53af12405',
|
'description': 'md5:bc9ca6e4e063361e21c920c53af12405',
|
||||||
'upload_date': '20160311',
|
'upload_date': '20160311',
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
# DW documentaries, only last for one or two weeks
|
||||||
|
'url': 'http://www.dw.com/en/documentaries-welcome-to-the-90s-2016-05-21/e-19220158-9798',
|
||||||
|
'md5': '56b6214ef463bfb9a3b71aeb886f3cf1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '19274438',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Welcome to the 90s – Hip Hop',
|
||||||
|
'description': 'Welcome to the 90s - The Golden Decade of Hip Hop',
|
||||||
|
'upload_date': '20160521',
|
||||||
|
},
|
||||||
|
'skip': 'Video removed',
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -38,6 +53,7 @@ class DWIE(InfoExtractor):
|
|||||||
webpage = self._download_webpage(url, media_id)
|
webpage = self._download_webpage(url, media_id)
|
||||||
hidden_inputs = self._hidden_inputs(webpage)
|
hidden_inputs = self._hidden_inputs(webpage)
|
||||||
title = hidden_inputs['media_title']
|
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':
|
if hidden_inputs.get('player_type') == 'video' and hidden_inputs.get('stream_file') == '1':
|
||||||
formats = self._extract_smil_formats(
|
formats = self._extract_smil_formats(
|
||||||
@@ -49,13 +65,20 @@ class DWIE(InfoExtractor):
|
|||||||
else:
|
else:
|
||||||
formats = [{'url': hidden_inputs['file_name']}]
|
formats = [{'url': hidden_inputs['file_name']}]
|
||||||
|
|
||||||
|
upload_date = hidden_inputs.get('display_date')
|
||||||
|
if not upload_date:
|
||||||
|
upload_date = self._html_search_regex(
|
||||||
|
r'<span[^>]+class="date">([0-9.]+)\s*\|', webpage,
|
||||||
|
'upload date', default=None)
|
||||||
|
upload_date = unified_strdate(upload_date)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': media_id,
|
'id': media_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': self._og_search_description(webpage),
|
'description': self._og_search_description(webpage),
|
||||||
'thumbnail': hidden_inputs.get('preview_image'),
|
'thumbnail': hidden_inputs.get('preview_image'),
|
||||||
'duration': int_or_none(hidden_inputs.get('file_duration')),
|
'duration': int_or_none(hidden_inputs.get('file_duration')),
|
||||||
'upload_date': hidden_inputs.get('display_date'),
|
'upload_date': upload_date,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,8 +11,8 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class EpornerIE(InfoExtractor):
|
class EpornerIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?eporner\.com/hd-porn/(?P<id>\d+)/(?P<display_id>[\w-]+)'
|
_VALID_URL = r'https?://(?:www\.)?eporner\.com/hd-porn/(?P<id>\w+)/(?P<display_id>[\w-]+)'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://www.eporner.com/hd-porn/95008/Infamous-Tiffany-Teen-Strip-Tease-Video/',
|
'url': 'http://www.eporner.com/hd-porn/95008/Infamous-Tiffany-Teen-Strip-Tease-Video/',
|
||||||
'md5': '39d486f046212d8e1b911c52ab4691f8',
|
'md5': '39d486f046212d8e1b911c52ab4691f8',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -23,8 +23,12 @@ class EpornerIE(InfoExtractor):
|
|||||||
'duration': 1838,
|
'duration': 1838,
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'age_limit': 18,
|
'age_limit': 18,
|
||||||
}
|
},
|
||||||
}
|
}, {
|
||||||
|
# New (May 2016) URL layout
|
||||||
|
'url': 'http://www.eporner.com/hd-porn/3YRUtzMcWn0/Star-Wars-XXX-Parody/',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
@@ -8,6 +8,7 @@ class ESPNIE(InfoExtractor):
|
|||||||
_VALID_URL = r'https?://espn\.go\.com/(?:[^/]+/)*(?P<id>[^/]+)'
|
_VALID_URL = r'https?://espn\.go\.com/(?:[^/]+/)*(?P<id>[^/]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://espn.go.com/video/clip?id=10365079',
|
'url': 'http://espn.go.com/video/clip?id=10365079',
|
||||||
|
'md5': '60e5d097a523e767d06479335d1bdc58',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'FkYWtmazr6Ed8xmvILvKLWjd4QvYZpzG',
|
'id': 'FkYWtmazr6Ed8xmvILvKLWjd4QvYZpzG',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -15,21 +16,22 @@ class ESPNIE(InfoExtractor):
|
|||||||
'description': None,
|
'description': None,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
'add_ie': ['OoyalaExternal'],
|
||||||
}, {
|
}, {
|
||||||
# intl video, from http://www.espnfc.us/video/mls-highlights/150/video/2743663/must-see-moments-best-of-the-mls-season
|
# intl video, from http://www.espnfc.us/video/mls-highlights/150/video/2743663/must-see-moments-best-of-the-mls-season
|
||||||
'url': 'http://espn.go.com/video/clip?id=2743663',
|
'url': 'http://espn.go.com/video/clip?id=2743663',
|
||||||
|
'md5': 'f4ac89b59afc7e2d7dbb049523df6768',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '50NDFkeTqRHB0nXBOK-RGdSG5YQPuxHg',
|
'id': '50NDFkeTqRHB0nXBOK-RGdSG5YQPuxHg',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Must-See Moments: Best of the MLS season',
|
'title': 'Must-See Moments: Best of the MLS season',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
'add_ie': ['OoyalaExternal'],
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://espn.go.com/video/iframe/twitter/?cms=espn&id=10365079',
|
'url': 'https://espn.go.com/video/iframe/twitter/?cms=espn&id=10365079',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
@@ -21,6 +21,7 @@ from .adobetv import (
|
|||||||
)
|
)
|
||||||
from .adultswim import AdultSwimIE
|
from .adultswim import AdultSwimIE
|
||||||
from .aenetworks import AENetworksIE
|
from .aenetworks import AENetworksIE
|
||||||
|
from .afreecatv import AfreecaTVIE
|
||||||
from .aftonbladet import AftonbladetIE
|
from .aftonbladet import AftonbladetIE
|
||||||
from .airmozilla import AirMozillaIE
|
from .airmozilla import AirMozillaIE
|
||||||
from .aljazeera import AlJazeeraIE
|
from .aljazeera import AlJazeeraIE
|
||||||
@@ -43,7 +44,6 @@ from .archiveorg import ArchiveOrgIE
|
|||||||
from .ard import (
|
from .ard import (
|
||||||
ARDIE,
|
ARDIE,
|
||||||
ARDMediathekIE,
|
ARDMediathekIE,
|
||||||
SportschauIE,
|
|
||||||
)
|
)
|
||||||
from .arte import (
|
from .arte import (
|
||||||
ArteTvIE,
|
ArteTvIE,
|
||||||
@@ -56,6 +56,7 @@ from .arte import (
|
|||||||
ArteTVDDCIE,
|
ArteTVDDCIE,
|
||||||
ArteTVMagazineIE,
|
ArteTVMagazineIE,
|
||||||
ArteTVEmbedIE,
|
ArteTVEmbedIE,
|
||||||
|
ArteTVPlaylistIE,
|
||||||
)
|
)
|
||||||
from .atresplayer import AtresPlayerIE
|
from .atresplayer import AtresPlayerIE
|
||||||
from .atttechchannel import ATTTechChannelIE
|
from .atttechchannel import ATTTechChannelIE
|
||||||
@@ -69,6 +70,8 @@ from .bandcamp import BandcampIE, BandcampAlbumIE
|
|||||||
from .bbc import (
|
from .bbc import (
|
||||||
BBCCoUkIE,
|
BBCCoUkIE,
|
||||||
BBCCoUkArticleIE,
|
BBCCoUkArticleIE,
|
||||||
|
BBCCoUkIPlayerPlaylistIE,
|
||||||
|
BBCCoUkPlaylistIE,
|
||||||
BBCIE,
|
BBCIE,
|
||||||
)
|
)
|
||||||
from .beeg import BeegIE
|
from .beeg import BeegIE
|
||||||
@@ -106,6 +109,10 @@ from .camwithher import CamWithHerIE
|
|||||||
from .canalplus import CanalplusIE
|
from .canalplus import CanalplusIE
|
||||||
from .canalc2 import Canalc2IE
|
from .canalc2 import Canalc2IE
|
||||||
from .canvas import CanvasIE
|
from .canvas import CanvasIE
|
||||||
|
from .carambatv import (
|
||||||
|
CarambaTVIE,
|
||||||
|
CarambaTVPageIE,
|
||||||
|
)
|
||||||
from .cbc import (
|
from .cbc import (
|
||||||
CBCIE,
|
CBCIE,
|
||||||
CBCPlayerIE,
|
CBCPlayerIE,
|
||||||
@@ -133,6 +140,7 @@ from .cliprs import ClipRsIE
|
|||||||
from .clipfish import ClipfishIE
|
from .clipfish import ClipfishIE
|
||||||
from .cliphunter import CliphunterIE
|
from .cliphunter import CliphunterIE
|
||||||
from .clipsyndicate import ClipsyndicateIE
|
from .clipsyndicate import ClipsyndicateIE
|
||||||
|
from .closertotruth import CloserToTruthIE
|
||||||
from .cloudy import CloudyIE
|
from .cloudy import CloudyIE
|
||||||
from .clubic import ClubicIE
|
from .clubic import ClubicIE
|
||||||
from .clyp import ClypIE
|
from .clyp import ClypIE
|
||||||
@@ -143,6 +151,7 @@ from .cnn import (
|
|||||||
CNNBlogsIE,
|
CNNBlogsIE,
|
||||||
CNNArticleIE,
|
CNNArticleIE,
|
||||||
)
|
)
|
||||||
|
from .coub import CoubIE
|
||||||
from .collegerama import CollegeRamaIE
|
from .collegerama import CollegeRamaIE
|
||||||
from .comedycentral import ComedyCentralIE, ComedyCentralShowsIE
|
from .comedycentral import ComedyCentralIE, ComedyCentralShowsIE
|
||||||
from .comcarcoff import ComCarCoffIE
|
from .comcarcoff import ComCarCoffIE
|
||||||
@@ -231,6 +240,7 @@ from .everyonesmixtape import EveryonesMixtapeIE
|
|||||||
from .exfm import ExfmIE
|
from .exfm import ExfmIE
|
||||||
from .expotv import ExpoTVIE
|
from .expotv import ExpoTVIE
|
||||||
from .extremetube import ExtremeTubeIE
|
from .extremetube import ExtremeTubeIE
|
||||||
|
from .eyedotv import EyedoTVIE
|
||||||
from .facebook import FacebookIE
|
from .facebook import FacebookIE
|
||||||
from .faz import FazIE
|
from .faz import FazIE
|
||||||
from .fc2 import FC2IE
|
from .fc2 import FC2IE
|
||||||
@@ -288,6 +298,7 @@ from .globo import (
|
|||||||
GloboArticleIE,
|
GloboArticleIE,
|
||||||
)
|
)
|
||||||
from .godtube import GodTubeIE
|
from .godtube import GodTubeIE
|
||||||
|
from .godtv import GodTVIE
|
||||||
from .goldenmoustache import GoldenMoustacheIE
|
from .goldenmoustache import GoldenMoustacheIE
|
||||||
from .golem import GolemIE
|
from .golem import GolemIE
|
||||||
from .googledrive import GoogleDriveIE
|
from .googledrive import GoogleDriveIE
|
||||||
@@ -379,6 +390,7 @@ from .leeco import (
|
|||||||
LePlaylistIE,
|
LePlaylistIE,
|
||||||
LetvCloudIE,
|
LetvCloudIE,
|
||||||
)
|
)
|
||||||
|
from .libraryofcongress import LibraryOfCongressIE
|
||||||
from .libsyn import LibsynIE
|
from .libsyn import LibsynIE
|
||||||
from .lifenews import (
|
from .lifenews import (
|
||||||
LifeNewsIE,
|
LifeNewsIE,
|
||||||
@@ -506,7 +518,10 @@ from .nhl import (
|
|||||||
NHLVideocenterCategoryIE,
|
NHLVideocenterCategoryIE,
|
||||||
NHLIE,
|
NHLIE,
|
||||||
)
|
)
|
||||||
from .nick import NickIE
|
from .nick import (
|
||||||
|
NickIE,
|
||||||
|
NickDeIE,
|
||||||
|
)
|
||||||
from .niconico import NiconicoIE, NiconicoPlaylistIE
|
from .niconico import NiconicoIE, NiconicoPlaylistIE
|
||||||
from .ninegag import NineGagIE
|
from .ninegag import NineGagIE
|
||||||
from .noco import NocoIE
|
from .noco import NocoIE
|
||||||
@@ -616,7 +631,14 @@ from .qqmusic import (
|
|||||||
QQMusicToplistIE,
|
QQMusicToplistIE,
|
||||||
QQMusicPlaylistIE,
|
QQMusicPlaylistIE,
|
||||||
)
|
)
|
||||||
from .r7 import R7IE
|
from .r7 import (
|
||||||
|
R7IE,
|
||||||
|
R7ArticleIE,
|
||||||
|
)
|
||||||
|
from .radiocanada import (
|
||||||
|
RadioCanadaIE,
|
||||||
|
RadioCanadaAudioVideoIE,
|
||||||
|
)
|
||||||
from .radiode import RadioDeIE
|
from .radiode import RadioDeIE
|
||||||
from .radiojavan import RadioJavanIE
|
from .radiojavan import RadioJavanIE
|
||||||
from .radiobremen import RadioBremenIE
|
from .radiobremen import RadioBremenIE
|
||||||
@@ -630,11 +652,16 @@ from .rds import RDSIE
|
|||||||
from .redtube import RedTubeIE
|
from .redtube import RedTubeIE
|
||||||
from .regiotv import RegioTVIE
|
from .regiotv import RegioTVIE
|
||||||
from .restudy import RestudyIE
|
from .restudy import RestudyIE
|
||||||
|
from .reuters import ReutersIE
|
||||||
from .reverbnation import ReverbNationIE
|
from .reverbnation import ReverbNationIE
|
||||||
from .revision3 import Revision3IE
|
from .revision3 import (
|
||||||
|
Revision3EmbedIE,
|
||||||
|
Revision3IE,
|
||||||
|
)
|
||||||
from .rice import RICEIE
|
from .rice import RICEIE
|
||||||
from .ringtv import RingTVIE
|
from .ringtv import RingTVIE
|
||||||
from .ro220 import Ro220IE
|
from .ro220 import Ro220IE
|
||||||
|
from .rockstargames import RockstarGamesIE
|
||||||
from .rottentomatoes import RottenTomatoesIE
|
from .rottentomatoes import RottenTomatoesIE
|
||||||
from .roxwel import RoxwelIE
|
from .roxwel import RoxwelIE
|
||||||
from .rtbf import RTBFIE
|
from .rtbf import RTBFIE
|
||||||
@@ -670,6 +697,7 @@ from .screencast import ScreencastIE
|
|||||||
from .screencastomatic import ScreencastOMaticIE
|
from .screencastomatic import ScreencastOMaticIE
|
||||||
from .screenjunkies import ScreenJunkiesIE
|
from .screenjunkies import ScreenJunkiesIE
|
||||||
from .screenwavemedia import ScreenwaveMediaIE, TeamFourIE
|
from .screenwavemedia import ScreenwaveMediaIE, TeamFourIE
|
||||||
|
from .seeker import SeekerIE
|
||||||
from .senateisvp import SenateISVPIE
|
from .senateisvp import SenateISVPIE
|
||||||
from .sendtonews import SendtoNewsIE
|
from .sendtonews import SendtoNewsIE
|
||||||
from .servingsys import ServingSysIE
|
from .servingsys import ServingSysIE
|
||||||
@@ -722,6 +750,7 @@ from .sportbox import (
|
|||||||
SportBoxEmbedIE,
|
SportBoxEmbedIE,
|
||||||
)
|
)
|
||||||
from .sportdeutschland import SportDeutschlandIE
|
from .sportdeutschland import SportDeutschlandIE
|
||||||
|
from .sportschau import SportschauIE
|
||||||
from .srgssr import (
|
from .srgssr import (
|
||||||
SRGSSRIE,
|
SRGSSRIE,
|
||||||
SRGSSRPlayIE,
|
SRGSSRPlayIE,
|
||||||
@@ -762,6 +791,7 @@ from .telecinco import TelecincoIE
|
|||||||
from .telegraaf import TelegraafIE
|
from .telegraaf import TelegraafIE
|
||||||
from .telemb import TeleMBIE
|
from .telemb import TeleMBIE
|
||||||
from .teletask import TeleTaskIE
|
from .teletask import TeleTaskIE
|
||||||
|
from .telewebion import TelewebionIE
|
||||||
from .testurl import TestURLIE
|
from .testurl import TestURLIE
|
||||||
from .tf1 import TF1IE
|
from .tf1 import TF1IE
|
||||||
from .theintercept import TheInterceptIE
|
from .theintercept import TheInterceptIE
|
||||||
@@ -827,7 +857,10 @@ from .tvc import (
|
|||||||
)
|
)
|
||||||
from .tvigle import TvigleIE
|
from .tvigle import TvigleIE
|
||||||
from .tvland import TVLandIE
|
from .tvland import TVLandIE
|
||||||
from .tvp import TvpIE, TvpSeriesIE
|
from .tvp import (
|
||||||
|
TVPIE,
|
||||||
|
TVPSeriesIE,
|
||||||
|
)
|
||||||
from .tvplay import TVPlayIE
|
from .tvplay import TVPlayIE
|
||||||
from .tweakers import TweakersIE
|
from .tweakers import TweakersIE
|
||||||
from .twentyfourvideo import TwentyFourVideoIE
|
from .twentyfourvideo import TwentyFourVideoIE
|
||||||
@@ -843,6 +876,7 @@ from .twitch import (
|
|||||||
TwitchProfileIE,
|
TwitchProfileIE,
|
||||||
TwitchPastBroadcastsIE,
|
TwitchPastBroadcastsIE,
|
||||||
TwitchStreamIE,
|
TwitchStreamIE,
|
||||||
|
TwitchClipsIE,
|
||||||
)
|
)
|
||||||
from .twitter import (
|
from .twitter import (
|
||||||
TwitterCardIE,
|
TwitterCardIE,
|
||||||
@@ -894,6 +928,7 @@ from .videomore import (
|
|||||||
)
|
)
|
||||||
from .videopremium import VideoPremiumIE
|
from .videopremium import VideoPremiumIE
|
||||||
from .videott import VideoTtIE
|
from .videott import VideoTtIE
|
||||||
|
from .vidio import VidioIE
|
||||||
from .vidme import (
|
from .vidme import (
|
||||||
VidmeIE,
|
VidmeIE,
|
||||||
VidmeUserIE,
|
VidmeUserIE,
|
||||||
@@ -939,26 +974,29 @@ from .vporn import VpornIE
|
|||||||
from .vrt import VRTIE
|
from .vrt import VRTIE
|
||||||
from .vube import VubeIE
|
from .vube import VubeIE
|
||||||
from .vuclip import VuClipIE
|
from .vuclip import VuClipIE
|
||||||
from .vulture import VultureIE
|
|
||||||
from .walla import WallaIE
|
from .walla import WallaIE
|
||||||
from .washingtonpost import WashingtonPostIE
|
from .washingtonpost import (
|
||||||
|
WashingtonPostIE,
|
||||||
|
WashingtonPostArticleIE,
|
||||||
|
)
|
||||||
from .wat import WatIE
|
from .wat import WatIE
|
||||||
from .watchindianporn import WatchIndianPornIE
|
from .watchindianporn import WatchIndianPornIE
|
||||||
from .wdr import (
|
from .wdr import (
|
||||||
WDRIE,
|
WDRIE,
|
||||||
WDRMobileIE,
|
WDRMobileIE,
|
||||||
WDRMausIE,
|
|
||||||
)
|
)
|
||||||
from .webofstories import (
|
from .webofstories import (
|
||||||
WebOfStoriesIE,
|
WebOfStoriesIE,
|
||||||
WebOfStoriesPlaylistIE,
|
WebOfStoriesPlaylistIE,
|
||||||
)
|
)
|
||||||
from .weibo import WeiboIE
|
|
||||||
from .weiqitv import WeiqiTVIE
|
from .weiqitv import WeiqiTVIE
|
||||||
from .wimp import WimpIE
|
from .wimp import WimpIE
|
||||||
from .wistia import WistiaIE
|
from .wistia import WistiaIE
|
||||||
from .worldstarhiphop import WorldStarHipHopIE
|
from .worldstarhiphop import WorldStarHipHopIE
|
||||||
from .wrzuta import WrzutaIE
|
from .wrzuta import (
|
||||||
|
WrzutaIE,
|
||||||
|
WrzutaPlaylistIE,
|
||||||
|
)
|
||||||
from .wsj import WSJIE
|
from .wsj import WSJIE
|
||||||
from .xbef import XBefIE
|
from .xbef import XBefIE
|
||||||
from .xboxclips import XboxClipsIE
|
from .xboxclips import XboxClipsIE
|
||||||
@@ -994,7 +1032,10 @@ from .yesjapan import YesJapanIE
|
|||||||
from .yinyuetai import YinYueTaiIE
|
from .yinyuetai import YinYueTaiIE
|
||||||
from .ynet import YnetIE
|
from .ynet import YnetIE
|
||||||
from .youjizz import YouJizzIE
|
from .youjizz import YouJizzIE
|
||||||
from .youku import YoukuIE
|
from .youku import (
|
||||||
|
YoukuIE,
|
||||||
|
YoukuShowIE,
|
||||||
|
)
|
||||||
from .youporn import YouPornIE
|
from .youporn import YouPornIE
|
||||||
from .yourupload import YourUploadIE
|
from .yourupload import YourUploadIE
|
||||||
from .youtube import (
|
from .youtube import (
|
||||||
|
64
youtube_dl/extractor/eyedotv.py
Normal file
64
youtube_dl/extractor/eyedotv.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
xpath_text,
|
||||||
|
parse_duration,
|
||||||
|
ExtractorError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EyedoTVIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?eyedo\.tv/[^/]+/(?:#!/)?Live/Detail/(?P<id>[0-9]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://www.eyedo.tv/en-US/#!/Live/Detail/16301',
|
||||||
|
'md5': 'ba14f17995cdfc20c36ba40e21bf73f7',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '16301',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Journée du conseil scientifique de l\'Afnic 2015',
|
||||||
|
'description': 'md5:4abe07293b2f73efc6e1c37028d58c98',
|
||||||
|
'uploader': 'Afnic Live',
|
||||||
|
'uploader_id': '8023',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ROOT_URL = 'http://live.eyedo.net:1935/'
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
video_data = self._download_xml('http://eyedo.tv/api/live/GetLive/%s' % video_id, video_id)
|
||||||
|
|
||||||
|
def _add_ns(path):
|
||||||
|
return self._xpath_ns(path, 'http://schemas.datacontract.org/2004/07/EyeDo.Core.Implementation.Web.ViewModels.Api')
|
||||||
|
|
||||||
|
title = xpath_text(video_data, _add_ns('Titre'), 'title', True)
|
||||||
|
state_live_code = xpath_text(video_data, _add_ns('StateLiveCode'), 'title', True)
|
||||||
|
if state_live_code == 'avenir':
|
||||||
|
raise ExtractorError(
|
||||||
|
'%s said: We\'re sorry, but this video is not yet available.' % self.IE_NAME,
|
||||||
|
expected=True)
|
||||||
|
|
||||||
|
is_live = state_live_code == 'live'
|
||||||
|
m3u8_url = None
|
||||||
|
# http://eyedo.tv/Content/Html5/Scripts/html5view.js
|
||||||
|
if is_live:
|
||||||
|
if xpath_text(video_data, 'Cdn') == 'true':
|
||||||
|
m3u8_url = 'http://rrr.sz.xlcdn.com/?account=eyedo&file=A%s&type=live&service=wowza&protocol=http&output=playlist.m3u8' % video_id
|
||||||
|
else:
|
||||||
|
m3u8_url = self._ROOT_URL + 'w/%s/eyedo_720p/playlist.m3u8' % video_id
|
||||||
|
else:
|
||||||
|
m3u8_url = self._ROOT_URL + 'replay-w/%s/mp4:%s.mp4/playlist.m3u8' % (video_id, video_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'formats': self._extract_m3u8_formats(
|
||||||
|
m3u8_url, video_id, 'mp4', 'm3u8' if is_live else 'm3u8_native'),
|
||||||
|
'description': xpath_text(video_data, _add_ns('Description')),
|
||||||
|
'duration': parse_duration(xpath_text(video_data, _add_ns('Duration'))),
|
||||||
|
'uploader': xpath_text(video_data, _add_ns('Createur')),
|
||||||
|
'uploader_id': xpath_text(video_data, _add_ns('CreateurId')),
|
||||||
|
'chapter': xpath_text(video_data, _add_ns('ChapitreTitre')),
|
||||||
|
'chapter_id': xpath_text(video_data, _add_ns('ChapitreId')),
|
||||||
|
}
|
@@ -13,7 +13,8 @@ class Formula1IE(InfoExtractor):
|
|||||||
'id': 'JvYXJpMzE6pArfHWm5ARp5AiUmD-gibV',
|
'id': 'JvYXJpMzE6pArfHWm5ARp5AiUmD-gibV',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': 'Race highlights - Spain 2016',
|
'title': 'Race highlights - Spain 2016',
|
||||||
}
|
},
|
||||||
|
'add_ie': ['Ooyala'],
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import smuggle_url
|
from ..utils import (
|
||||||
|
smuggle_url,
|
||||||
|
update_url_query,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FoxSportsIE(InfoExtractor):
|
class FoxSportsIE(InfoExtractor):
|
||||||
@@ -9,11 +12,15 @@ class FoxSportsIE(InfoExtractor):
|
|||||||
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://www.foxsports.com/video?vid=432609859715',
|
'url': 'http://www.foxsports.com/video?vid=432609859715',
|
||||||
|
'md5': 'b49050e955bebe32c301972e4012ac17',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'gA0bHB3Ladz3',
|
'id': 'i0qKWsk3qJaM',
|
||||||
'ext': 'flv',
|
'ext': 'mp4',
|
||||||
'title': 'Courtney Lee on going up 2-0 in series vs. Blazers',
|
'title': 'Courtney Lee on going up 2-0 in series vs. Blazers',
|
||||||
'description': 'Courtney Lee talks about Memphis being focused.',
|
'description': 'Courtney Lee talks about Memphis being focused.',
|
||||||
|
'upload_date': '20150423',
|
||||||
|
'timestamp': 1429761109,
|
||||||
|
'uploader': 'NEWA-FNG-FOXSPORTS',
|
||||||
},
|
},
|
||||||
'add_ie': ['ThePlatform'],
|
'add_ie': ['ThePlatform'],
|
||||||
}
|
}
|
||||||
@@ -28,5 +35,8 @@ class FoxSportsIE(InfoExtractor):
|
|||||||
r"data-player-config='([^']+)'", webpage, 'data player config'),
|
r"data-player-config='([^']+)'", webpage, 'data player config'),
|
||||||
video_id)
|
video_id)
|
||||||
|
|
||||||
return self.url_result(smuggle_url(
|
return self.url_result(smuggle_url(update_url_query(
|
||||||
config['releaseURL'] + '&manifest=f4m', {'force_smil_url': True}))
|
config['releaseURL'], {
|
||||||
|
'mbr': 'true',
|
||||||
|
'switch': 'http',
|
||||||
|
}), {'force_smil_url': True}))
|
||||||
|
@@ -62,6 +62,8 @@ from .digiteka import DigitekaIE
|
|||||||
from .instagram import InstagramIE
|
from .instagram import InstagramIE
|
||||||
from .liveleak import LiveLeakIE
|
from .liveleak import LiveLeakIE
|
||||||
from .threeqsdn import ThreeQSDNIE
|
from .threeqsdn import ThreeQSDNIE
|
||||||
|
from .theplatform import ThePlatformIE
|
||||||
|
from .vessel import VesselIE
|
||||||
|
|
||||||
|
|
||||||
class GenericIE(InfoExtractor):
|
class GenericIE(InfoExtractor):
|
||||||
@@ -625,13 +627,13 @@ class GenericIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
# MTVSercices embed
|
# MTVSercices embed
|
||||||
{
|
{
|
||||||
'url': 'http://www.gametrailers.com/news-post/76093/north-america-europe-is-getting-that-mario-kart-8-mercedes-dlc-too',
|
'url': 'http://www.vulture.com/2016/06/new-key-peele-sketches-released.html',
|
||||||
'md5': '35727f82f58c76d996fc188f9755b0d5',
|
'md5': 'ca1aef97695ef2c1d6973256a57e5252',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '0306a69b-8adf-4fb5-aace-75f8e8cbfca9',
|
'id': '769f7ec0-0692-4d62-9b45-0d88074bffc1',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Review',
|
'title': 'Key and Peele|October 10, 2012|2|203|Liam Neesons - Uncensored',
|
||||||
'description': 'Mario\'s life in the fast lane has never looked so good.',
|
'description': 'Two valets share their love for movie star Liam Neesons.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
# YouTube embed via <data-embed-url="">
|
# YouTube embed via <data-embed-url="">
|
||||||
@@ -783,6 +785,19 @@ class GenericIE(InfoExtractor):
|
|||||||
'title': 'Rosetta #CometLanding webcast HL 10',
|
'title': 'Rosetta #CometLanding webcast HL 10',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
# Another Livestream embed, without 'new.' in URL
|
||||||
|
{
|
||||||
|
'url': 'https://www.freespeech.org/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '123537347',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 're:^FSTV [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# Live stream
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
},
|
||||||
# LazyYT
|
# LazyYT
|
||||||
{
|
{
|
||||||
'url': 'http://discourse.ubuntu.com/t/unity-8-desktop-mode-windows-on-mir/1986',
|
'url': 'http://discourse.ubuntu.com/t/unity-8-desktop-mode-windows-on-mir/1986',
|
||||||
@@ -867,18 +882,6 @@ class GenericIE(InfoExtractor):
|
|||||||
'title': 'EP3S5 - Bon Appétit - Baqueira Mi Corazon !',
|
'title': 'EP3S5 - Bon Appétit - Baqueira Mi Corazon !',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
# Kaltura embed
|
|
||||||
{
|
|
||||||
'url': 'http://www.monumentalnetwork.com/videos/john-carlson-postgame-2-25-15',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '1_eergr3h1',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'upload_date': '20150226',
|
|
||||||
'uploader_id': 'MonumentalSports-Kaltura@perfectsensedigital.com',
|
|
||||||
'timestamp': int,
|
|
||||||
'title': 'John Carlson Postgame 2/25/15',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
# Kaltura embed (different embed code)
|
# Kaltura embed (different embed code)
|
||||||
{
|
{
|
||||||
'url': 'http://www.premierchristianradio.com/Shows/Saturday/Unbelievable/Conference-Videos/Os-Guinness-Is-It-Fools-Talk-Unbelievable-Conference-2014',
|
'url': 'http://www.premierchristianradio.com/Shows/Saturday/Unbelievable/Conference-Videos/Os-Guinness-Is-It-Fools-Talk-Unbelievable-Conference-2014',
|
||||||
@@ -904,6 +907,19 @@ class GenericIE(InfoExtractor):
|
|||||||
'uploader_id': 'echojecka',
|
'uploader_id': 'echojecka',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
# Kaltura embed with single quotes
|
||||||
|
{
|
||||||
|
'url': 'http://fod.infobase.com/p_ViewPlaylist.aspx?AssignmentID=NUN8ZY',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '0_izeg5utt',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '35871',
|
||||||
|
'timestamp': 1355743100,
|
||||||
|
'upload_date': '20121217',
|
||||||
|
'uploader_id': 'batchUser',
|
||||||
|
},
|
||||||
|
'add_ie': ['Kaltura'],
|
||||||
|
},
|
||||||
# Eagle.Platform embed (generic URL)
|
# Eagle.Platform embed (generic URL)
|
||||||
{
|
{
|
||||||
'url': 'http://lenta.ru/news/2015/03/06/navalny/',
|
'url': 'http://lenta.ru/news/2015/03/06/navalny/',
|
||||||
@@ -1016,16 +1032,31 @@ class GenericIE(InfoExtractor):
|
|||||||
'timestamp': 1389118457,
|
'timestamp': 1389118457,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
# NBC News embed
|
||||||
|
{
|
||||||
|
'url': 'http://www.vulture.com/2016/06/letterman-couldnt-care-less-about-late-night.html',
|
||||||
|
'md5': '1aa589c675898ae6d37a17913cf68d66',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '701714499682',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'PREVIEW: On Assignment: David Letterman',
|
||||||
|
'description': 'A preview of Tom Brokaw\'s interview with David Letterman as part of the On Assignment series powered by Dateline. Airs Sunday June 12 at 7/6c.',
|
||||||
|
},
|
||||||
|
},
|
||||||
# UDN embed
|
# UDN embed
|
||||||
{
|
{
|
||||||
'url': 'http://www.udn.com/news/story/7314/822787',
|
'url': 'https://video.udn.com/news/300346',
|
||||||
'md5': 'fd2060e988c326991037b9aff9df21a6',
|
'md5': 'fd2060e988c326991037b9aff9df21a6',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '300346',
|
'id': '300346',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '中一中男師變性 全校師生力挺',
|
'title': '中一中男師變性 全校師生力挺',
|
||||||
'thumbnail': 're:^https?://.*\.jpg$',
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
}
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
# Ooyala embed
|
# Ooyala embed
|
||||||
{
|
{
|
||||||
@@ -1042,20 +1073,6 @@ class GenericIE(InfoExtractor):
|
|||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
# Contains a SMIL manifest
|
|
||||||
{
|
|
||||||
'url': 'http://www.telewebion.com/fa/1263668/%D9%82%D8%B1%D8%B9%D9%87%E2%80%8C%DA%A9%D8%B4%DB%8C-%D9%84%DB%8C%DA%AF-%D9%82%D9%87%D8%B1%D9%85%D8%A7%D9%86%D8%A7%D9%86-%D8%A7%D8%B1%D9%88%D9%BE%D8%A7/%2B-%D9%81%D9%88%D8%AA%D8%A8%D8%A7%D9%84.html',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'file',
|
|
||||||
'ext': 'flv',
|
|
||||||
'title': '+ Football: Lottery Champions League Europe',
|
|
||||||
'uploader': 'www.telewebion.com',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# rtmpe downloads
|
|
||||||
'skip_download': True,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# Brightcove URL in single quotes
|
# Brightcove URL in single quotes
|
||||||
{
|
{
|
||||||
'url': 'http://www.sportsnet.ca/baseball/mlb/sn-presents-russell-martin-world-citizen/',
|
'url': 'http://www.sportsnet.ca/baseball/mlb/sn-presents-russell-martin-world-citizen/',
|
||||||
@@ -1193,6 +1210,16 @@ class GenericIE(InfoExtractor):
|
|||||||
'uploader': 'Lake8737',
|
'uploader': 'Lake8737',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
# Duplicated embedded video URLs
|
||||||
|
{
|
||||||
|
'url': 'http://www.hudl.com/athlete/2538180/highlights/149298443',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '149298443_480_16c25b74_2',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'vs. Blue Orange Spring Game',
|
||||||
|
'uploader': 'www.hudl.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
def report_following_redirect(self, new_url):
|
def report_following_redirect(self, new_url):
|
||||||
@@ -1499,6 +1526,16 @@ class GenericIE(InfoExtractor):
|
|||||||
if bc_urls:
|
if bc_urls:
|
||||||
return _playlist_from_matches(bc_urls, ie='BrightcoveNew')
|
return _playlist_from_matches(bc_urls, ie='BrightcoveNew')
|
||||||
|
|
||||||
|
# Look for ThePlatform embeds
|
||||||
|
tp_urls = ThePlatformIE._extract_urls(webpage)
|
||||||
|
if tp_urls:
|
||||||
|
return _playlist_from_matches(tp_urls, ie='ThePlatform')
|
||||||
|
|
||||||
|
# Look for Vessel embeds
|
||||||
|
vessel_urls = VesselIE._extract_urls(webpage)
|
||||||
|
if vessel_urls:
|
||||||
|
return _playlist_from_matches(vessel_urls, ie=VesselIE.ie_key())
|
||||||
|
|
||||||
# Look for embedded rtl.nl player
|
# Look for embedded rtl.nl player
|
||||||
matches = re.findall(
|
matches = re.findall(
|
||||||
r'<iframe[^>]+?src="((?:https?:)?//(?:www\.)?rtl\.nl/system/videoplayer/[^"]+(?:video_)?embed[^"]+)"',
|
r'<iframe[^>]+?src="((?:https?:)?//(?:www\.)?rtl\.nl/system/videoplayer/[^"]+(?:video_)?embed[^"]+)"',
|
||||||
@@ -1806,14 +1843,6 @@ class GenericIE(InfoExtractor):
|
|||||||
url = unescapeHTML(mobj.group('url'))
|
url = unescapeHTML(mobj.group('url'))
|
||||||
return self.url_result(url)
|
return self.url_result(url)
|
||||||
|
|
||||||
# Look for embedded vulture.com player
|
|
||||||
mobj = re.search(
|
|
||||||
r'<iframe src="(?P<url>https?://video\.vulture\.com/[^"]+)"',
|
|
||||||
webpage)
|
|
||||||
if mobj is not None:
|
|
||||||
url = unescapeHTML(mobj.group('url'))
|
|
||||||
return self.url_result(url, ie='Vulture')
|
|
||||||
|
|
||||||
# Look for embedded mtvservices player
|
# Look for embedded mtvservices player
|
||||||
mtvservices_url = MTVServicesEmbeddedIE._extract_url(webpage)
|
mtvservices_url = MTVServicesEmbeddedIE._extract_url(webpage)
|
||||||
if mtvservices_url:
|
if mtvservices_url:
|
||||||
@@ -1862,7 +1891,7 @@ class GenericIE(InfoExtractor):
|
|||||||
return self.url_result(self._proto_relative_url(mobj.group('url'), scheme='http:'), 'CondeNast')
|
return self.url_result(self._proto_relative_url(mobj.group('url'), scheme='http:'), 'CondeNast')
|
||||||
|
|
||||||
mobj = re.search(
|
mobj = re.search(
|
||||||
r'<iframe[^>]+src="(?P<url>https?://new\.livestream\.com/[^"]+/player[^"]+)"',
|
r'<iframe[^>]+src="(?P<url>https?://(?:new\.)?livestream\.com/[^"]+/player[^"]+)"',
|
||||||
webpage)
|
webpage)
|
||||||
if mobj is not None:
|
if mobj is not None:
|
||||||
return self.url_result(mobj.group('url'), 'Livestream')
|
return self.url_result(mobj.group('url'), 'Livestream')
|
||||||
@@ -1874,7 +1903,7 @@ class GenericIE(InfoExtractor):
|
|||||||
return self.url_result(mobj.group('url'), 'Zapiks')
|
return self.url_result(mobj.group('url'), 'Zapiks')
|
||||||
|
|
||||||
# Look for Kaltura embeds
|
# Look for Kaltura embeds
|
||||||
mobj = (re.search(r"(?s)kWidget\.(?:thumb)?[Ee]mbed\(\{.*?'wid'\s*:\s*'_?(?P<partner_id>[^']+)',.*?'entry_?[Ii]d'\s*:\s*'(?P<id>[^']+)',", webpage) or
|
mobj = (re.search(r"(?s)kWidget\.(?:thumb)?[Ee]mbed\(\{.*?(?P<q1>['\"])wid(?P=q1)\s*:\s*(?P<q2>['\"])_?(?P<partner_id>[^'\"]+)(?P=q2),.*?(?P<q3>['\"])entry_?[Ii]d(?P=q3)\s*:\s*(?P<q4>['\"])(?P<id>[^'\"]+)(?P=q4),", webpage) or
|
||||||
re.search(r'(?s)(?P<q1>["\'])(?:https?:)?//cdnapi(?:sec)?\.kaltura\.com/.*?(?:p|partner_id)/(?P<partner_id>\d+).*?(?P=q1).*?entry_?[Ii]d\s*:\s*(?P<q2>["\'])(?P<id>.+?)(?P=q2)', webpage))
|
re.search(r'(?s)(?P<q1>["\'])(?:https?:)?//cdnapi(?:sec)?\.kaltura\.com/.*?(?:p|partner_id)/(?P<partner_id>\d+).*?(?P=q1).*?entry_?[Ii]d\s*:\s*(?P<q2>["\'])(?P<id>.+?)(?P=q2)', webpage))
|
||||||
if mobj is not None:
|
if mobj is not None:
|
||||||
return self.url_result(smuggle_url(
|
return self.url_result(smuggle_url(
|
||||||
@@ -1926,6 +1955,12 @@ class GenericIE(InfoExtractor):
|
|||||||
if nbc_sports_url:
|
if nbc_sports_url:
|
||||||
return self.url_result(nbc_sports_url, 'NBCSportsVPlayer')
|
return self.url_result(nbc_sports_url, 'NBCSportsVPlayer')
|
||||||
|
|
||||||
|
# Look for NBC News embeds
|
||||||
|
nbc_news_embed_url = re.search(
|
||||||
|
r'<iframe[^>]+src=(["\'])(?P<url>(?:https?:)?//www\.nbcnews\.com/widget/video-embed/[^"\']+)\1', webpage)
|
||||||
|
if nbc_news_embed_url:
|
||||||
|
return self.url_result(nbc_news_embed_url.group('url'), 'NBCNews')
|
||||||
|
|
||||||
# Look for Google Drive embeds
|
# Look for Google Drive embeds
|
||||||
google_drive_url = GoogleDriveIE._extract_url(webpage)
|
google_drive_url = GoogleDriveIE._extract_url(webpage)
|
||||||
if google_drive_url:
|
if google_drive_url:
|
||||||
@@ -2105,7 +2140,7 @@ class GenericIE(InfoExtractor):
|
|||||||
raise UnsupportedError(url)
|
raise UnsupportedError(url)
|
||||||
|
|
||||||
entries = []
|
entries = []
|
||||||
for video_url in found:
|
for video_url in orderedSet(found):
|
||||||
video_url = unescapeHTML(video_url)
|
video_url = unescapeHTML(video_url)
|
||||||
video_url = video_url.replace('\\/', '/')
|
video_url = video_url.replace('\\/', '/')
|
||||||
video_url = compat_urlparse.urljoin(url, video_url)
|
video_url = compat_urlparse.urljoin(url, video_url)
|
||||||
|
66
youtube_dl/extractor/godtv.py
Normal file
66
youtube_dl/extractor/godtv.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from .ooyala import OoyalaIE
|
||||||
|
from ..utils import js_to_json
|
||||||
|
|
||||||
|
|
||||||
|
class GodTVIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?god\.tv(?:/[^/]+)*/(?P<id>[^/?#&]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://god.tv/jesus-image/video/jesus-conference-2016/randy-needham',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'lpd3g2MzE6D1g8zFAKz8AGpxWcpu6o_3',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Randy Needham',
|
||||||
|
'duration': 3615.08,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'http://god.tv/playlist/bible-study',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'bible-study',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 37,
|
||||||
|
}, {
|
||||||
|
'url': 'http://god.tv/node/15097',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://god.tv/live/africa',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://god.tv/liveevents',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
|
||||||
|
settings = self._parse_json(
|
||||||
|
self._search_regex(
|
||||||
|
r'jQuery\.extend\(Drupal\.settings\s*,\s*({.+?})\);',
|
||||||
|
webpage, 'settings', default='{}'),
|
||||||
|
display_id, transform_source=js_to_json, fatal=False)
|
||||||
|
|
||||||
|
ooyala_id = None
|
||||||
|
|
||||||
|
if settings:
|
||||||
|
playlist = settings.get('playlist')
|
||||||
|
if playlist and isinstance(playlist, list):
|
||||||
|
entries = [
|
||||||
|
OoyalaIE._build_url_result(video['content_id'])
|
||||||
|
for video in playlist if video.get('content_id')]
|
||||||
|
if entries:
|
||||||
|
return self.playlist_result(entries, display_id)
|
||||||
|
ooyala_id = settings.get('ooyala', {}).get('content_id')
|
||||||
|
|
||||||
|
if not ooyala_id:
|
||||||
|
ooyala_id = self._search_regex(
|
||||||
|
r'["\']content_id["\']\s*:\s*(["\'])(?P<id>[\w-]+)\1',
|
||||||
|
webpage, 'ooyala id', group='id')
|
||||||
|
|
||||||
|
return OoyalaIE._build_url_result(ooyala_id)
|
@@ -14,6 +14,7 @@ class GrouponIE(InfoExtractor):
|
|||||||
'description': 'Studio kept at 105 degrees and 40% humidity with anti-microbial and anti-slip Flotex flooring; certified instructors',
|
'description': 'Studio kept at 105 degrees and 40% humidity with anti-microbial and anti-slip Flotex flooring; certified instructors',
|
||||||
},
|
},
|
||||||
'playlist': [{
|
'playlist': [{
|
||||||
|
'md5': '42428ce8a00585f9bc36e49226eae7a1',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'fk6OhWpXgIQ',
|
'id': 'fk6OhWpXgIQ',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -24,10 +25,11 @@ class GrouponIE(InfoExtractor):
|
|||||||
'uploader_id': 'groupon',
|
'uploader_id': 'groupon',
|
||||||
'uploader': 'Groupon',
|
'uploader': 'Groupon',
|
||||||
},
|
},
|
||||||
|
'add_ie': ['Youtube'],
|
||||||
}],
|
}],
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_PROVIDERS = {
|
_PROVIDERS = {
|
||||||
|
@@ -8,7 +8,7 @@ class HowcastIE(InfoExtractor):
|
|||||||
_VALID_URL = r'https?://(?:www\.)?howcast\.com/videos/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?howcast\.com/videos/(?P<id>\d+)'
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://www.howcast.com/videos/390161-How-to-Tie-a-Square-Knot-Properly',
|
'url': 'http://www.howcast.com/videos/390161-How-to-Tie-a-Square-Knot-Properly',
|
||||||
'md5': '8b743df908c42f60cf6496586c7f12c3',
|
'md5': '7d45932269a288149483144f01b99789',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '390161',
|
'id': '390161',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -19,9 +19,9 @@ class HowcastIE(InfoExtractor):
|
|||||||
'duration': 56.823,
|
'duration': 56.823,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
'add_ie': ['Ooyala'],
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@@ -12,7 +12,7 @@ from ..utils import (
|
|||||||
class ImdbIE(InfoExtractor):
|
class ImdbIE(InfoExtractor):
|
||||||
IE_NAME = 'imdb'
|
IE_NAME = 'imdb'
|
||||||
IE_DESC = 'Internet Movie Database trailers'
|
IE_DESC = 'Internet Movie Database trailers'
|
||||||
_VALID_URL = r'https?://(?:www|m)\.imdb\.com/video/[^/]+/vi(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www|m)\.imdb\.com/(?:video/[^/]+/|title/tt\d+.*?#lb-)vi(?P<id>\d+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.imdb.com/video/imdb/vi2524815897',
|
'url': 'http://www.imdb.com/video/imdb/vi2524815897',
|
||||||
@@ -25,6 +25,12 @@ class ImdbIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://www.imdb.com/video/_/vi2524815897',
|
'url': 'http://www.imdb.com/video/_/vi2524815897',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.imdb.com/title/tt1667889/?ref_=ext_shr_eml_vi#lb-vi2524815897',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.imdb.com/title/tt1667889/#lb-vi2524815897',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@@ -60,7 +60,8 @@ class IndavideoEmbedIE(InfoExtractor):
|
|||||||
|
|
||||||
formats = [{
|
formats = [{
|
||||||
'url': video_url,
|
'url': video_url,
|
||||||
'height': self._search_regex(r'\.(\d{3,4})\.mp4$', video_url, 'height', default=None),
|
'height': int_or_none(self._search_regex(
|
||||||
|
r'\.(\d{3,4})\.mp4(?:\?|$)', video_url, 'height', default=None)),
|
||||||
} for video_url in video_urls]
|
} for video_url in video_urls]
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
limit_length,
|
limit_length,
|
||||||
lowercase_escape,
|
lowercase_escape,
|
||||||
|
try_get,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -19,10 +20,16 @@ class InstagramIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'aye83DjauH',
|
'id': 'aye83DjauH',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'uploader_id': 'naomipq',
|
|
||||||
'title': 'Video by naomipq',
|
'title': 'Video by naomipq',
|
||||||
'description': 'md5:1f17f0ab29bd6fe2bfad705f58de3cb8',
|
'description': 'md5:1f17f0ab29bd6fe2bfad705f58de3cb8',
|
||||||
}
|
'thumbnail': 're:^https?://.*\.jpg',
|
||||||
|
'timestamp': 1371748545,
|
||||||
|
'upload_date': '20130620',
|
||||||
|
'uploader_id': 'naomipq',
|
||||||
|
'uploader': 'Naomi Leonor Phan-Quang',
|
||||||
|
'like_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
# missing description
|
# missing description
|
||||||
'url': 'https://www.instagram.com/p/BA-pQFBG8HZ/?taken-by=britneyspears',
|
'url': 'https://www.instagram.com/p/BA-pQFBG8HZ/?taken-by=britneyspears',
|
||||||
@@ -31,6 +38,13 @@ class InstagramIE(InfoExtractor):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'uploader_id': 'britneyspears',
|
'uploader_id': 'britneyspears',
|
||||||
'title': 'Video by britneyspears',
|
'title': 'Video by britneyspears',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg',
|
||||||
|
'timestamp': 1453760977,
|
||||||
|
'upload_date': '20160125',
|
||||||
|
'uploader_id': 'britneyspears',
|
||||||
|
'uploader': 'Britney Spears',
|
||||||
|
'like_count': int,
|
||||||
|
'comment_count': int,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
@@ -67,21 +81,57 @@ class InstagramIE(InfoExtractor):
|
|||||||
url = mobj.group('url')
|
url = mobj.group('url')
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
uploader_id = self._search_regex(r'"owner":{"username":"(.+?)"',
|
|
||||||
webpage, 'uploader id', fatal=False)
|
(video_url, description, thumbnail, timestamp, uploader,
|
||||||
desc = self._search_regex(
|
uploader_id, like_count, comment_count) = [None] * 8
|
||||||
r'"caption":"(.+?)"', webpage, 'description', default=None)
|
|
||||||
if desc is not None:
|
shared_data = self._parse_json(
|
||||||
desc = lowercase_escape(desc)
|
self._search_regex(
|
||||||
|
r'window\._sharedData\s*=\s*({.+?});',
|
||||||
|
webpage, 'shared data', default='{}'),
|
||||||
|
video_id, fatal=False)
|
||||||
|
if shared_data:
|
||||||
|
media = try_get(
|
||||||
|
shared_data, lambda x: x['entry_data']['PostPage'][0]['media'], dict)
|
||||||
|
if media:
|
||||||
|
video_url = media.get('video_url')
|
||||||
|
description = media.get('caption')
|
||||||
|
thumbnail = media.get('display_src')
|
||||||
|
timestamp = int_or_none(media.get('date'))
|
||||||
|
uploader = media.get('owner', {}).get('full_name')
|
||||||
|
uploader_id = media.get('owner', {}).get('username')
|
||||||
|
like_count = int_or_none(media.get('likes', {}).get('count'))
|
||||||
|
comment_count = int_or_none(media.get('comments', {}).get('count'))
|
||||||
|
|
||||||
|
if not video_url:
|
||||||
|
video_url = self._og_search_video_url(webpage, secure=False)
|
||||||
|
|
||||||
|
if not uploader_id:
|
||||||
|
uploader_id = self._search_regex(
|
||||||
|
r'"owner"\s*:\s*{\s*"username"\s*:\s*"(.+?)"',
|
||||||
|
webpage, 'uploader id', fatal=False)
|
||||||
|
|
||||||
|
if not description:
|
||||||
|
description = self._search_regex(
|
||||||
|
r'"caption"\s*:\s*"(.+?)"', webpage, 'description', default=None)
|
||||||
|
if description is not None:
|
||||||
|
description = lowercase_escape(description)
|
||||||
|
|
||||||
|
if not thumbnail:
|
||||||
|
thumbnail = self._og_search_thumbnail(webpage)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'url': self._og_search_video_url(webpage, secure=False),
|
'url': video_url,
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Video by %s' % uploader_id,
|
'title': 'Video by %s' % uploader_id,
|
||||||
'thumbnail': self._og_search_thumbnail(webpage),
|
'description': description,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'timestamp': timestamp,
|
||||||
'uploader_id': uploader_id,
|
'uploader_id': uploader_id,
|
||||||
'description': desc,
|
'uploader': uploader,
|
||||||
|
'like_count': like_count,
|
||||||
|
'comment_count': comment_count,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -12,9 +12,35 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class JWPlatformBaseIE(InfoExtractor):
|
class JWPlatformBaseIE(InfoExtractor):
|
||||||
|
@staticmethod
|
||||||
|
def _find_jwplayer_data(webpage):
|
||||||
|
# TODO: Merge this with JWPlayer-related codes in generic.py
|
||||||
|
|
||||||
|
mobj = re.search(
|
||||||
|
'jwplayer\((?P<quote>[\'"])[^\'" ]+(?P=quote)\)\.setup\((?P<options>[^)]+)\)',
|
||||||
|
webpage)
|
||||||
|
if mobj:
|
||||||
|
return mobj.group('options')
|
||||||
|
|
||||||
|
def _extract_jwplayer_data(self, webpage, video_id, *args, **kwargs):
|
||||||
|
jwplayer_data = self._parse_json(
|
||||||
|
self._find_jwplayer_data(webpage), video_id)
|
||||||
|
return self._parse_jwplayer_data(
|
||||||
|
jwplayer_data, video_id, *args, **kwargs)
|
||||||
|
|
||||||
def _parse_jwplayer_data(self, jwplayer_data, video_id, require_title=True, m3u8_id=None, rtmp_params=None):
|
def _parse_jwplayer_data(self, jwplayer_data, video_id, require_title=True, m3u8_id=None, rtmp_params=None):
|
||||||
|
# JWPlayer backward compatibility: flattened playlists
|
||||||
|
# https://github.com/jwplayer/jwplayer/blob/v7.4.3/src/js/api/config.js#L81-L96
|
||||||
|
if 'playlist' not in jwplayer_data:
|
||||||
|
jwplayer_data = {'playlist': [jwplayer_data]}
|
||||||
|
|
||||||
video_data = jwplayer_data['playlist'][0]
|
video_data = jwplayer_data['playlist'][0]
|
||||||
|
|
||||||
|
# JWPlayer backward compatibility: flattened sources
|
||||||
|
# https://github.com/jwplayer/jwplayer/blob/v7.4.3/src/js/playlist/item.js#L29-L35
|
||||||
|
if 'sources' not in video_data:
|
||||||
|
video_data['sources'] = [video_data]
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for source in video_data['sources']:
|
for source in video_data['sources']:
|
||||||
source_url = self._proto_relative_url(source['file'])
|
source_url = self._proto_relative_url(source['file'])
|
||||||
|
@@ -148,8 +148,8 @@ class KuwoAlbumIE(InfoExtractor):
|
|||||||
'url': 'http://www.kuwo.cn/album/502294/',
|
'url': 'http://www.kuwo.cn/album/502294/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '502294',
|
'id': '502294',
|
||||||
'title': 'M',
|
'title': 'Made\xa0Series\xa0《M》',
|
||||||
'description': 'md5:6a7235a84cc6400ec3b38a7bdaf1d60c',
|
'description': 'md5:d463f0d8a0ff3c3ea3d6ed7452a9483f',
|
||||||
},
|
},
|
||||||
'playlist_count': 2,
|
'playlist_count': 2,
|
||||||
}
|
}
|
||||||
@@ -209,7 +209,7 @@ class KuwoSingerIE(InfoExtractor):
|
|||||||
'url': 'http://www.kuwo.cn/mingxing/bruno+mars/',
|
'url': 'http://www.kuwo.cn/mingxing/bruno+mars/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'bruno+mars',
|
'id': 'bruno+mars',
|
||||||
'title': 'Bruno Mars',
|
'title': 'Bruno\xa0Mars',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 329,
|
'playlist_mincount': 329,
|
||||||
}, {
|
}, {
|
||||||
@@ -306,7 +306,7 @@ class KuwoMvIE(KuwoBaseIE):
|
|||||||
'id': '6480076',
|
'id': '6480076',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'My HouseMV',
|
'title': 'My HouseMV',
|
||||||
'creator': '2PM',
|
'creator': 'PM02:00',
|
||||||
},
|
},
|
||||||
# In this video, music URLs (anti.s) are blocked outside China and
|
# In this video, music URLs (anti.s) are blocked outside China and
|
||||||
# USA, while the MV URL (mvurl) is available globally, so force the MV
|
# USA, while the MV URL (mvurl) is available globally, so force the MV
|
||||||
|
@@ -28,7 +28,7 @@ from ..utils import (
|
|||||||
|
|
||||||
class LeIE(InfoExtractor):
|
class LeIE(InfoExtractor):
|
||||||
IE_DESC = '乐视网'
|
IE_DESC = '乐视网'
|
||||||
_VALID_URL = r'https?://www\.le\.com/ptv/vplay/(?P<id>\d+)\.html'
|
_VALID_URL = r'https?://(?:www\.le\.com/ptv/vplay|sports\.le\.com/video)/(?P<id>\d+)\.html'
|
||||||
|
|
||||||
_URL_TEMPLATE = 'http://www.le.com/ptv/vplay/%s.html'
|
_URL_TEMPLATE = 'http://www.le.com/ptv/vplay/%s.html'
|
||||||
|
|
||||||
@@ -69,6 +69,9 @@ class LeIE(InfoExtractor):
|
|||||||
'hls_prefer_native': True,
|
'hls_prefer_native': True,
|
||||||
},
|
},
|
||||||
'skip': 'Only available in China',
|
'skip': 'Only available in China',
|
||||||
|
}, {
|
||||||
|
'url': 'http://sports.le.com/video/25737697.html',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -196,7 +199,7 @@ class LeIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class LePlaylistIE(InfoExtractor):
|
class LePlaylistIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://[a-z]+\.le\.com/[a-z]+/(?P<id>[a-z0-9_]+)'
|
_VALID_URL = r'https?://[a-z]+\.le\.com/(?!video)[a-z]+/(?P<id>[a-z0-9_]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.le.com/tv/46177.html',
|
'url': 'http://www.le.com/tv/46177.html',
|
||||||
|
143
youtube_dl/extractor/libraryofcongress.py
Normal file
143
youtube_dl/extractor/libraryofcongress.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
from ..utils import (
|
||||||
|
determine_ext,
|
||||||
|
float_or_none,
|
||||||
|
int_or_none,
|
||||||
|
parse_filesize,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryOfCongressIE(InfoExtractor):
|
||||||
|
IE_NAME = 'loc'
|
||||||
|
IE_DESC = 'Library of Congress'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?loc\.gov/(?:item/|today/cyberlc/feature_wdesc\.php\?.*\brec=)(?P<id>[0-9]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
# embedded via <div class="media-player"
|
||||||
|
'url': 'http://loc.gov/item/90716351/',
|
||||||
|
'md5': '353917ff7f0255aa6d4b80a034833de8',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '90716351',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': "Pa's trip to Mars",
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
|
'duration': 0,
|
||||||
|
'view_count': int,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# webcast embedded via mediaObjectId
|
||||||
|
'url': 'https://www.loc.gov/today/cyberlc/feature_wdesc.php?rec=5578',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5578',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Help! Preservation Training Needs Here, There & Everywhere',
|
||||||
|
'duration': 3765,
|
||||||
|
'view_count': int,
|
||||||
|
'subtitles': 'mincount:1',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# with direct download links
|
||||||
|
'url': 'https://www.loc.gov/item/78710669/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '78710669',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'La vie et la passion de Jesus-Christ',
|
||||||
|
'duration': 0,
|
||||||
|
'view_count': int,
|
||||||
|
'formats': 'mincount:4',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
media_id = self._search_regex(
|
||||||
|
(r'id=(["\'])media-player-(?P<id>.+?)\1',
|
||||||
|
r'<video[^>]+id=(["\'])uuid-(?P<id>.+?)\1',
|
||||||
|
r'<video[^>]+data-uuid=(["\'])(?P<id>.+?)\1',
|
||||||
|
r'mediaObjectId\s*:\s*(["\'])(?P<id>.+?)\1'),
|
||||||
|
webpage, 'media id', group='id')
|
||||||
|
|
||||||
|
data = self._download_json(
|
||||||
|
'https://media.loc.gov/services/v1/media?id=%s&context=json' % media_id,
|
||||||
|
video_id)['mediaObject']
|
||||||
|
|
||||||
|
derivative = data['derivatives'][0]
|
||||||
|
media_url = derivative['derivativeUrl']
|
||||||
|
|
||||||
|
title = derivative.get('shortName') or data.get('shortName') or self._og_search_title(
|
||||||
|
webpage)
|
||||||
|
|
||||||
|
# Following algorithm was extracted from setAVSource js function
|
||||||
|
# found in webpage
|
||||||
|
media_url = media_url.replace('rtmp', 'https')
|
||||||
|
|
||||||
|
is_video = data.get('mediaType', 'v').lower() == 'v'
|
||||||
|
ext = determine_ext(media_url)
|
||||||
|
if ext not in ('mp4', 'mp3'):
|
||||||
|
media_url += '.mp4' if is_video else '.mp3'
|
||||||
|
|
||||||
|
if 'vod/mp4:' in media_url:
|
||||||
|
formats = [{
|
||||||
|
'url': media_url.replace('vod/mp4:', 'hls-vod/media/') + '.m3u8',
|
||||||
|
'format_id': 'hls',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'protocol': 'm3u8_native',
|
||||||
|
'quality': 1,
|
||||||
|
}]
|
||||||
|
elif 'vod/mp3:' in media_url:
|
||||||
|
formats = [{
|
||||||
|
'url': media_url.replace('vod/mp3:', ''),
|
||||||
|
'vcodec': 'none',
|
||||||
|
}]
|
||||||
|
|
||||||
|
download_urls = set()
|
||||||
|
for m in re.finditer(
|
||||||
|
r'<option[^>]+value=(["\'])(?P<url>.+?)\1[^>]+data-file-download=[^>]+>\s*(?P<id>.+?)(?:(?: |\s+)\((?P<size>.+?)\))?\s*<', webpage):
|
||||||
|
format_id = m.group('id').lower()
|
||||||
|
if format_id == 'gif':
|
||||||
|
continue
|
||||||
|
download_url = m.group('url')
|
||||||
|
if download_url in download_urls:
|
||||||
|
continue
|
||||||
|
download_urls.add(download_url)
|
||||||
|
formats.append({
|
||||||
|
'url': download_url,
|
||||||
|
'format_id': format_id,
|
||||||
|
'filesize_approx': parse_filesize(m.group('size')),
|
||||||
|
})
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
duration = float_or_none(data.get('duration'))
|
||||||
|
view_count = int_or_none(data.get('viewCount'))
|
||||||
|
|
||||||
|
subtitles = {}
|
||||||
|
cc_url = data.get('ccUrl')
|
||||||
|
if cc_url:
|
||||||
|
subtitles.setdefault('en', []).append({
|
||||||
|
'url': cc_url,
|
||||||
|
'ext': 'ttml',
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'thumbnail': self._og_search_thumbnail(webpage, default=None),
|
||||||
|
'duration': duration,
|
||||||
|
'view_count': view_count,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
@@ -7,48 +7,53 @@ from .common import InfoExtractor
|
|||||||
from ..compat import compat_urlparse
|
from ..compat import compat_urlparse
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
determine_ext,
|
determine_ext,
|
||||||
int_or_none,
|
|
||||||
remove_end,
|
|
||||||
unified_strdate,
|
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
remove_end,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LifeNewsIE(InfoExtractor):
|
class LifeNewsIE(InfoExtractor):
|
||||||
IE_NAME = 'lifenews'
|
IE_NAME = 'life'
|
||||||
IE_DESC = 'LIFE | NEWS'
|
IE_DESC = 'Life.ru'
|
||||||
_VALID_URL = r'https?://lifenews\.ru/(?:mobile/)?(?P<section>news|video)/(?P<id>\d+)'
|
_VALID_URL = r'https?://life\.ru/t/[^/]+/(?P<id>\d+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# single video embedded via video/source
|
# single video embedded via video/source
|
||||||
'url': 'http://lifenews.ru/news/98736',
|
'url': 'https://life.ru/t/новости/98736',
|
||||||
'md5': '77c95eaefaca216e32a76a343ad89d23',
|
'md5': '77c95eaefaca216e32a76a343ad89d23',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '98736',
|
'id': '98736',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Мужчина нашел дома архив оборонного завода',
|
'title': 'Мужчина нашел дома архив оборонного завода',
|
||||||
'description': 'md5:3b06b1b39b5e2bea548e403d99b8bf26',
|
'description': 'md5:3b06b1b39b5e2bea548e403d99b8bf26',
|
||||||
|
'timestamp': 1344154740,
|
||||||
'upload_date': '20120805',
|
'upload_date': '20120805',
|
||||||
|
'view_count': int,
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
# single video embedded via iframe
|
# single video embedded via iframe
|
||||||
'url': 'http://lifenews.ru/news/152125',
|
'url': 'https://life.ru/t/новости/152125',
|
||||||
'md5': '77d19a6f0886cd76bdbf44b4d971a273',
|
'md5': '77d19a6f0886cd76bdbf44b4d971a273',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '152125',
|
'id': '152125',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'В Сети появилось видео захвата «Правым сектором» колхозных полей ',
|
'title': 'В Сети появилось видео захвата «Правым сектором» колхозных полей ',
|
||||||
'description': 'Жители двух поселков Днепропетровской области не простили радикалам угрозу лишения плодородных земель и пошли в лобовую. ',
|
'description': 'Жители двух поселков Днепропетровской области не простили радикалам угрозу лишения плодородных земель и пошли в лобовую. ',
|
||||||
|
'timestamp': 1427961840,
|
||||||
'upload_date': '20150402',
|
'upload_date': '20150402',
|
||||||
|
'view_count': int,
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
# two videos embedded via iframe
|
# two videos embedded via iframe
|
||||||
'url': 'http://lifenews.ru/news/153461',
|
'url': 'https://life.ru/t/новости/153461',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '153461',
|
'id': '153461',
|
||||||
'title': 'В Москве спасли потерявшегося медвежонка, который спрятался на дереве',
|
'title': 'В Москве спасли потерявшегося медвежонка, который спрятался на дереве',
|
||||||
'description': 'Маленький хищник не смог найти дорогу домой и обрел временное убежище на тополе недалеко от жилого массива, пока его не нашла соседская собака.',
|
'description': 'Маленький хищник не смог найти дорогу домой и обрел временное убежище на тополе недалеко от жилого массива, пока его не нашла соседская собака.',
|
||||||
'upload_date': '20150505',
|
'timestamp': 1430825520,
|
||||||
|
'view_count': int,
|
||||||
},
|
},
|
||||||
'playlist': [{
|
'playlist': [{
|
||||||
'md5': '9b6ef8bc0ffa25aebc8bdb40d89ab795',
|
'md5': '9b6ef8bc0ffa25aebc8bdb40d89ab795',
|
||||||
@@ -57,6 +62,7 @@ class LifeNewsIE(InfoExtractor):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'В Москве спасли потерявшегося медвежонка, который спрятался на дереве (Видео 1)',
|
'title': 'В Москве спасли потерявшегося медвежонка, который спрятался на дереве (Видео 1)',
|
||||||
'description': 'Маленький хищник не смог найти дорогу домой и обрел временное убежище на тополе недалеко от жилого массива, пока его не нашла соседская собака.',
|
'description': 'Маленький хищник не смог найти дорогу домой и обрел временное убежище на тополе недалеко от жилого массива, пока его не нашла соседская собака.',
|
||||||
|
'timestamp': 1430825520,
|
||||||
'upload_date': '20150505',
|
'upload_date': '20150505',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
@@ -66,22 +72,25 @@ class LifeNewsIE(InfoExtractor):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'В Москве спасли потерявшегося медвежонка, который спрятался на дереве (Видео 2)',
|
'title': 'В Москве спасли потерявшегося медвежонка, который спрятался на дереве (Видео 2)',
|
||||||
'description': 'Маленький хищник не смог найти дорогу домой и обрел временное убежище на тополе недалеко от жилого массива, пока его не нашла соседская собака.',
|
'description': 'Маленький хищник не смог найти дорогу домой и обрел временное убежище на тополе недалеко от жилого массива, пока его не нашла соседская собака.',
|
||||||
|
'timestamp': 1430825520,
|
||||||
'upload_date': '20150505',
|
'upload_date': '20150505',
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://lifenews.ru/video/13035',
|
'url': 'https://life.ru/t/новости/213035',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://life.ru/t/%D0%BD%D0%BE%D0%B2%D0%BE%D1%81%D1%82%D0%B8/153461',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://life.ru/t/новости/411489/manuel_vals_nazval_frantsiiu_tsieliu_nomier_odin_dlia_ighil',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
video_id = self._match_id(url)
|
||||||
video_id = mobj.group('id')
|
|
||||||
section = mobj.group('section')
|
|
||||||
|
|
||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage(url, video_id)
|
||||||
'http://lifenews.ru/%s/%s' % (section, video_id),
|
|
||||||
video_id, 'Downloading page')
|
|
||||||
|
|
||||||
video_urls = re.findall(
|
video_urls = re.findall(
|
||||||
r'<video[^>]+><source[^>]+src=["\'](.+?)["\']', webpage)
|
r'<video[^>]+><source[^>]+src=["\'](.+?)["\']', webpage)
|
||||||
@@ -95,26 +104,22 @@ class LifeNewsIE(InfoExtractor):
|
|||||||
|
|
||||||
title = remove_end(
|
title = remove_end(
|
||||||
self._og_search_title(webpage),
|
self._og_search_title(webpage),
|
||||||
' - Первый по срочным новостям — LIFE | NEWS')
|
' - Life.ru')
|
||||||
|
|
||||||
description = self._og_search_description(webpage)
|
description = self._og_search_description(webpage)
|
||||||
|
|
||||||
view_count = self._html_search_regex(
|
view_count = self._html_search_regex(
|
||||||
r'<div class=\'views\'>\s*(\d+)\s*</div>', webpage, 'view count', fatal=False)
|
r'<div[^>]+class=(["\']).*?\bhits-count\b.*?\1[^>]*>\s*(?P<value>\d+)\s*</div>',
|
||||||
comment_count = self._html_search_regex(
|
webpage, 'view count', fatal=False, group='value')
|
||||||
r'=\'commentCount\'[^>]*>\s*(\d+)\s*<',
|
|
||||||
webpage, 'comment count', fatal=False)
|
|
||||||
|
|
||||||
upload_date = self._html_search_regex(
|
timestamp = parse_iso8601(self._search_regex(
|
||||||
r'<time[^>]*datetime=\'([^\']+)\'', webpage, 'upload date', fatal=False)
|
r'<time[^>]+datetime=(["\'])(?P<value>.+?)\1',
|
||||||
if upload_date is not None:
|
webpage, 'upload date', fatal=False, group='value'))
|
||||||
upload_date = unified_strdate(upload_date)
|
|
||||||
|
|
||||||
common_info = {
|
common_info = {
|
||||||
'description': description,
|
'description': description,
|
||||||
'view_count': int_or_none(view_count),
|
'view_count': int_or_none(view_count),
|
||||||
'comment_count': int_or_none(comment_count),
|
'timestamp': timestamp,
|
||||||
'upload_date': upload_date,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def make_entry(video_id, video_url, index=None):
|
def make_entry(video_id, video_url, index=None):
|
||||||
@@ -183,7 +188,8 @@ class LifeEmbedIE(InfoExtractor):
|
|||||||
ext = determine_ext(video_url)
|
ext = determine_ext(video_url)
|
||||||
if ext == 'm3u8':
|
if ext == 'm3u8':
|
||||||
formats.extend(self._extract_m3u8_formats(
|
formats.extend(self._extract_m3u8_formats(
|
||||||
video_url, video_id, 'mp4', m3u8_id='m3u8'))
|
video_url, video_id, 'mp4',
|
||||||
|
entry_protocol='m3u8_native', m3u8_id='m3u8'))
|
||||||
else:
|
else:
|
||||||
formats.append({
|
formats.append({
|
||||||
'url': video_url,
|
'url': video_url,
|
||||||
|
@@ -98,13 +98,19 @@ class LimelightBaseIE(InfoExtractor):
|
|||||||
} for thumbnail in properties.get('thumbnails', []) if thumbnail.get('url')]
|
} for thumbnail in properties.get('thumbnails', []) if thumbnail.get('url')]
|
||||||
|
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
for caption in properties.get('captions', {}):
|
for caption in properties.get('captions', []):
|
||||||
lang = caption.get('language_code')
|
lang = caption.get('language_code')
|
||||||
subtitles_url = caption.get('url')
|
subtitles_url = caption.get('url')
|
||||||
if lang and subtitles_url:
|
if lang and subtitles_url:
|
||||||
subtitles[lang] = [{
|
subtitles.setdefault(lang, []).append({
|
||||||
'url': subtitles_url,
|
'url': subtitles_url,
|
||||||
}]
|
})
|
||||||
|
closed_captions_url = properties.get('closed_captions_url')
|
||||||
|
if closed_captions_url:
|
||||||
|
subtitles.setdefault('en', []).append({
|
||||||
|
'url': closed_captions_url,
|
||||||
|
'ext': 'ttml',
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
@@ -123,7 +129,18 @@ class LimelightBaseIE(InfoExtractor):
|
|||||||
|
|
||||||
class LimelightMediaIE(LimelightBaseIE):
|
class LimelightMediaIE(LimelightBaseIE):
|
||||||
IE_NAME = 'limelight'
|
IE_NAME = 'limelight'
|
||||||
_VALID_URL = r'(?:limelight:media:|https?://link\.videoplatform\.limelight\.com/media/\??\bmediaId=)(?P<id>[a-z0-9]{32})'
|
_VALID_URL = r'''(?x)
|
||||||
|
(?:
|
||||||
|
limelight:media:|
|
||||||
|
https?://
|
||||||
|
(?:
|
||||||
|
link\.videoplatform\.limelight\.com/media/|
|
||||||
|
assets\.delvenetworks\.com/player/loader\.swf
|
||||||
|
)
|
||||||
|
\?.*?\bmediaId=
|
||||||
|
)
|
||||||
|
(?P<id>[a-z0-9]{32})
|
||||||
|
'''
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://link.videoplatform.limelight.com/media/?mediaId=3ffd040b522b4485b6d84effc750cd86',
|
'url': 'http://link.videoplatform.limelight.com/media/?mediaId=3ffd040b522b4485b6d84effc750cd86',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -158,6 +175,9 @@ class LimelightMediaIE(LimelightBaseIE):
|
|||||||
# rtmp download
|
# rtmp download
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://assets.delvenetworks.com/player/loader.swf?mediaId=8018a574f08d416e95ceaccae4ba0452',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
_PLAYLIST_SERVICE_PATH = 'media'
|
_PLAYLIST_SERVICE_PATH = 'media'
|
||||||
_API_PATH = 'media'
|
_API_PATH = 'media'
|
||||||
@@ -176,15 +196,29 @@ class LimelightMediaIE(LimelightBaseIE):
|
|||||||
|
|
||||||
class LimelightChannelIE(LimelightBaseIE):
|
class LimelightChannelIE(LimelightBaseIE):
|
||||||
IE_NAME = 'limelight:channel'
|
IE_NAME = 'limelight:channel'
|
||||||
_VALID_URL = r'(?:limelight:channel:|https?://link\.videoplatform\.limelight\.com/media/\??\bchannelId=)(?P<id>[a-z0-9]{32})'
|
_VALID_URL = r'''(?x)
|
||||||
_TEST = {
|
(?:
|
||||||
|
limelight:channel:|
|
||||||
|
https?://
|
||||||
|
(?:
|
||||||
|
link\.videoplatform\.limelight\.com/media/|
|
||||||
|
assets\.delvenetworks\.com/player/loader\.swf
|
||||||
|
)
|
||||||
|
\?.*?\bchannelId=
|
||||||
|
)
|
||||||
|
(?P<id>[a-z0-9]{32})
|
||||||
|
'''
|
||||||
|
_TESTS = [{
|
||||||
'url': 'http://link.videoplatform.limelight.com/media/?channelId=ab6a524c379342f9b23642917020c082',
|
'url': 'http://link.videoplatform.limelight.com/media/?channelId=ab6a524c379342f9b23642917020c082',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'ab6a524c379342f9b23642917020c082',
|
'id': 'ab6a524c379342f9b23642917020c082',
|
||||||
'title': 'Javascript Sample Code',
|
'title': 'Javascript Sample Code',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 3,
|
'playlist_mincount': 3,
|
||||||
}
|
}, {
|
||||||
|
'url': 'http://assets.delvenetworks.com/player/loader.swf?channelId=ab6a524c379342f9b23642917020c082',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
_PLAYLIST_SERVICE_PATH = 'channel'
|
_PLAYLIST_SERVICE_PATH = 'channel'
|
||||||
_API_PATH = 'channels'
|
_API_PATH = 'channels'
|
||||||
|
|
||||||
@@ -207,15 +241,29 @@ class LimelightChannelIE(LimelightBaseIE):
|
|||||||
|
|
||||||
class LimelightChannelListIE(LimelightBaseIE):
|
class LimelightChannelListIE(LimelightBaseIE):
|
||||||
IE_NAME = 'limelight:channel_list'
|
IE_NAME = 'limelight:channel_list'
|
||||||
_VALID_URL = r'(?:limelight:channel_list:|https?://link\.videoplatform\.limelight\.com/media/\?.*?\bchannelListId=)(?P<id>[a-z0-9]{32})'
|
_VALID_URL = r'''(?x)
|
||||||
_TEST = {
|
(?:
|
||||||
|
limelight:channel_list:|
|
||||||
|
https?://
|
||||||
|
(?:
|
||||||
|
link\.videoplatform\.limelight\.com/media/|
|
||||||
|
assets\.delvenetworks\.com/player/loader\.swf
|
||||||
|
)
|
||||||
|
\?.*?\bchannelListId=
|
||||||
|
)
|
||||||
|
(?P<id>[a-z0-9]{32})
|
||||||
|
'''
|
||||||
|
_TESTS = [{
|
||||||
'url': 'http://link.videoplatform.limelight.com/media/?channelListId=301b117890c4465c8179ede21fd92e2b',
|
'url': 'http://link.videoplatform.limelight.com/media/?channelListId=301b117890c4465c8179ede21fd92e2b',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '301b117890c4465c8179ede21fd92e2b',
|
'id': '301b117890c4465c8179ede21fd92e2b',
|
||||||
'title': 'Website - Hero Player',
|
'title': 'Website - Hero Player',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 2,
|
'playlist_mincount': 2,
|
||||||
}
|
}, {
|
||||||
|
'url': 'https://assets.delvenetworks.com/player/loader.swf?channelListId=301b117890c4465c8179ede21fd92e2b',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
_PLAYLIST_SERVICE_PATH = 'channel_list'
|
_PLAYLIST_SERVICE_PATH = 'channel_list'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@@ -150,7 +150,7 @@ class LivestreamIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _extract_stream_info(self, stream_info):
|
def _extract_stream_info(self, stream_info):
|
||||||
broadcast_id = stream_info['broadcast_id']
|
broadcast_id = compat_str(stream_info['broadcast_id'])
|
||||||
is_live = stream_info.get('is_live')
|
is_live = stream_info.get('is_live')
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
@@ -203,9 +203,10 @@ class LivestreamIE(InfoExtractor):
|
|||||||
if not videos_info:
|
if not videos_info:
|
||||||
break
|
break
|
||||||
for v in videos_info:
|
for v in videos_info:
|
||||||
|
v_id = compat_str(v['id'])
|
||||||
entries.append(self.url_result(
|
entries.append(self.url_result(
|
||||||
'http://livestream.com/accounts/%s/events/%s/videos/%s' % (account_id, event_id, v['id']),
|
'http://livestream.com/accounts/%s/events/%s/videos/%s' % (account_id, event_id, v_id),
|
||||||
'Livestream', v['id'], v['caption']))
|
'Livestream', v_id, v.get('caption')))
|
||||||
last_video = videos_info[-1]['id']
|
last_video = videos_info[-1]['id']
|
||||||
return self.playlist_result(entries, event_id, event_data['full_name'])
|
return self.playlist_result(entries, event_id, event_data['full_name'])
|
||||||
|
|
||||||
|
@@ -1,100 +1,100 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_str
|
from ..compat import (
|
||||||
|
compat_HTTPError,
|
||||||
|
compat_str,
|
||||||
|
compat_urlparse,
|
||||||
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
clean_html,
|
|
||||||
int_or_none,
|
int_or_none,
|
||||||
sanitized_Request,
|
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LyndaBaseIE(InfoExtractor):
|
class LyndaBaseIE(InfoExtractor):
|
||||||
_LOGIN_URL = 'https://www.lynda.com/login/login.aspx'
|
_SIGNIN_URL = 'https://www.lynda.com/signin'
|
||||||
|
_PASSWORD_URL = 'https://www.lynda.com/signin/password'
|
||||||
|
_USER_URL = 'https://www.lynda.com/signin/user'
|
||||||
_ACCOUNT_CREDENTIALS_HINT = 'Use --username and --password options to provide lynda.com account credentials.'
|
_ACCOUNT_CREDENTIALS_HINT = 'Use --username and --password options to provide lynda.com account credentials.'
|
||||||
_NETRC_MACHINE = 'lynda'
|
_NETRC_MACHINE = 'lynda'
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
self._login()
|
self._login()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _check_error(json_string, key_or_keys):
|
||||||
|
keys = [key_or_keys] if isinstance(key_or_keys, compat_str) else key_or_keys
|
||||||
|
for key in keys:
|
||||||
|
error = json_string.get(key)
|
||||||
|
if error:
|
||||||
|
raise ExtractorError('Unable to login: %s' % error, expected=True)
|
||||||
|
|
||||||
|
def _login_step(self, form_html, fallback_action_url, extra_form_data, note, referrer_url):
|
||||||
|
action_url = self._search_regex(
|
||||||
|
r'<form[^>]+action=(["\'])(?P<url>.+?)\1', form_html,
|
||||||
|
'post url', default=fallback_action_url, group='url')
|
||||||
|
|
||||||
|
if not action_url.startswith('http'):
|
||||||
|
action_url = compat_urlparse.urljoin(self._SIGNIN_URL, action_url)
|
||||||
|
|
||||||
|
form_data = self._hidden_inputs(form_html)
|
||||||
|
form_data.update(extra_form_data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self._download_json(
|
||||||
|
action_url, None, note,
|
||||||
|
data=urlencode_postdata(form_data),
|
||||||
|
headers={
|
||||||
|
'Referer': referrer_url,
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
})
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 500:
|
||||||
|
response = self._parse_json(e.cause.read().decode('utf-8'), None)
|
||||||
|
self._check_error(response, ('email', 'password'))
|
||||||
|
raise
|
||||||
|
|
||||||
|
self._check_error(response, 'ErrorMessage')
|
||||||
|
|
||||||
|
return response, action_url
|
||||||
|
|
||||||
def _login(self):
|
def _login(self):
|
||||||
username, password = self._get_login_info()
|
username, password = self._get_login_info()
|
||||||
if username is None:
|
if username is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
login_form = {
|
# Step 1: download signin page
|
||||||
'username': username,
|
signin_page = self._download_webpage(
|
||||||
'password': password,
|
self._SIGNIN_URL, None, 'Downloading signin page')
|
||||||
'remember': 'false',
|
|
||||||
'stayPut': 'false'
|
|
||||||
}
|
|
||||||
request = sanitized_Request(
|
|
||||||
self._LOGIN_URL, urlencode_postdata(login_form))
|
|
||||||
login_page = self._download_webpage(
|
|
||||||
request, None, 'Logging in as %s' % username)
|
|
||||||
|
|
||||||
# Not (yet) logged in
|
# Already logged in
|
||||||
m = re.search(r'loginResultJson\s*=\s*\'(?P<json>[^\']+)\';', login_page)
|
if any(re.search(p, signin_page) for p in (
|
||||||
if m is not None:
|
'isLoggedIn\s*:\s*true', r'logout\.aspx', r'>Log out<')):
|
||||||
response = m.group('json')
|
|
||||||
response_json = json.loads(response)
|
|
||||||
state = response_json['state']
|
|
||||||
|
|
||||||
if state == 'notlogged':
|
|
||||||
raise ExtractorError(
|
|
||||||
'Unable to login, incorrect username and/or password',
|
|
||||||
expected=True)
|
|
||||||
|
|
||||||
# This is when we get popup:
|
|
||||||
# > You're already logged in to lynda.com on two devices.
|
|
||||||
# > If you log in here, we'll log you out of another device.
|
|
||||||
# So, we need to confirm this.
|
|
||||||
if state == 'conflicted':
|
|
||||||
confirm_form = {
|
|
||||||
'username': '',
|
|
||||||
'password': '',
|
|
||||||
'resolve': 'true',
|
|
||||||
'remember': 'false',
|
|
||||||
'stayPut': 'false',
|
|
||||||
}
|
|
||||||
request = sanitized_Request(
|
|
||||||
self._LOGIN_URL, urlencode_postdata(confirm_form))
|
|
||||||
login_page = self._download_webpage(
|
|
||||||
request, None,
|
|
||||||
'Confirming log in and log out from another device')
|
|
||||||
|
|
||||||
if all(not re.search(p, login_page) for p in ('isLoggedIn\s*:\s*true', r'logout\.aspx', r'>Log out<')):
|
|
||||||
if 'login error' in login_page:
|
|
||||||
mobj = re.search(
|
|
||||||
r'(?s)<h1[^>]+class="topmost">(?P<title>[^<]+)</h1>\s*<div>(?P<description>.+?)</div>',
|
|
||||||
login_page)
|
|
||||||
if mobj:
|
|
||||||
raise ExtractorError(
|
|
||||||
'lynda returned error: %s - %s'
|
|
||||||
% (mobj.group('title'), clean_html(mobj.group('description'))),
|
|
||||||
expected=True)
|
|
||||||
raise ExtractorError('Unable to log in')
|
|
||||||
|
|
||||||
def _logout(self):
|
|
||||||
username, _ = self._get_login_info()
|
|
||||||
if username is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self._download_webpage(
|
# Step 2: submit email
|
||||||
'http://www.lynda.com/ajax/logout.aspx', None,
|
signin_form = self._search_regex(
|
||||||
'Logging out', 'Unable to log out', fatal=False)
|
r'(?s)(<form[^>]+data-form-name=["\']signin["\'][^>]*>.+?</form>)',
|
||||||
|
signin_page, 'signin form')
|
||||||
|
signin_page, signin_url = self._login_step(
|
||||||
|
signin_form, self._PASSWORD_URL, {'email': username},
|
||||||
|
'Submitting email', self._SIGNIN_URL)
|
||||||
|
|
||||||
|
# Step 3: submit password
|
||||||
|
password_form = signin_page['body']
|
||||||
|
self._login_step(
|
||||||
|
password_form, self._USER_URL, {'email': username, 'password': password},
|
||||||
|
'Submitting password', signin_url)
|
||||||
|
|
||||||
|
|
||||||
class LyndaIE(LyndaBaseIE):
|
class LyndaIE(LyndaBaseIE):
|
||||||
IE_NAME = 'lynda'
|
IE_NAME = 'lynda'
|
||||||
IE_DESC = 'lynda.com videos'
|
IE_DESC = 'lynda.com videos'
|
||||||
_VALID_URL = r'https?://www\.lynda\.com/(?:[^/]+/[^/]+/\d+|player/embed)/(?P<id>\d+)'
|
_VALID_URL = r'https?://www\.lynda\.com/(?:[^/]+/[^/]+/\d+|player/embed)/(?P<id>\d+)'
|
||||||
_NETRC_MACHINE = 'lynda'
|
|
||||||
|
|
||||||
_TIMECODE_REGEX = r'\[(?P<timecode>\d+:\d+:\d+[\.,]\d+)\]'
|
_TIMECODE_REGEX = r'\[(?P<timecode>\d+:\d+:\d+[\.,]\d+)\]'
|
||||||
|
|
||||||
@@ -212,8 +212,6 @@ class LyndaCourseIE(LyndaBaseIE):
|
|||||||
'http://www.lynda.com/ajax/player?courseId=%s&type=course' % course_id,
|
'http://www.lynda.com/ajax/player?courseId=%s&type=course' % course_id,
|
||||||
course_id, 'Downloading course JSON')
|
course_id, 'Downloading course JSON')
|
||||||
|
|
||||||
self._logout()
|
|
||||||
|
|
||||||
if course.get('Status') == 'NotFound':
|
if course.get('Status') == 'NotFound':
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
'Course %s does not exist' % course_id, expected=True)
|
'Course %s does not exist' % course_id, expected=True)
|
||||||
@@ -246,5 +244,6 @@ class LyndaCourseIE(LyndaBaseIE):
|
|||||||
% unaccessible_videos + self._ACCOUNT_CREDENTIALS_HINT)
|
% unaccessible_videos + self._ACCOUNT_CREDENTIALS_HINT)
|
||||||
|
|
||||||
course_title = course.get('Title')
|
course_title = course.get('Title')
|
||||||
|
course_description = course.get('Description')
|
||||||
|
|
||||||
return self.playlist_result(entries, course_id, course_title)
|
return self.playlist_result(entries, course_id, course_title, course_description)
|
||||||
|
@@ -4,16 +4,12 @@ from __future__ import unicode_literals
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_urllib_parse_urlencode
|
from ..utils import xpath_text
|
||||||
from ..utils import (
|
|
||||||
sanitized_Request,
|
|
||||||
xpath_text,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MatchTVIE(InfoExtractor):
|
class MatchTVIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://matchtv\.ru/?#live-player'
|
_VALID_URL = r'https?://matchtv\.ru(?:/on-air|/?#live-player)'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://matchtv.ru/#live-player',
|
'url': 'http://matchtv.ru/#live-player',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'matchtv-live',
|
'id': 'matchtv-live',
|
||||||
@@ -24,12 +20,16 @@ class MatchTVIE(InfoExtractor):
|
|||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
}
|
}, {
|
||||||
|
'url': 'http://matchtv.ru/on-air/',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = 'matchtv-live'
|
video_id = 'matchtv-live'
|
||||||
request = sanitized_Request(
|
video_url = self._download_json(
|
||||||
'http://player.matchtv.ntvplus.tv/player/smil?%s' % compat_urllib_parse_urlencode({
|
'http://player.matchtv.ntvplus.tv/player/smil', video_id,
|
||||||
|
query={
|
||||||
'ts': '',
|
'ts': '',
|
||||||
'quality': 'SD',
|
'quality': 'SD',
|
||||||
'contentId': '561d2c0df7159b37178b4567',
|
'contentId': '561d2c0df7159b37178b4567',
|
||||||
@@ -40,11 +40,10 @@ class MatchTVIE(InfoExtractor):
|
|||||||
'contentType': 'channel',
|
'contentType': 'channel',
|
||||||
'timeShift': '0',
|
'timeShift': '0',
|
||||||
'platform': 'portal',
|
'platform': 'portal',
|
||||||
}),
|
},
|
||||||
headers={
|
headers={
|
||||||
'Referer': 'http://player.matchtv.ntvplus.tv/embed-player/NTVEmbedPlayer.swf',
|
'Referer': 'http://player.matchtv.ntvplus.tv/embed-player/NTVEmbedPlayer.swf',
|
||||||
})
|
})['data']['videoUrl']
|
||||||
video_url = self._download_json(request, video_id)['data']['videoUrl']
|
|
||||||
f4m_url = xpath_text(self._download_xml(video_url, video_id), './to')
|
f4m_url = xpath_text(self._download_xml(video_url, video_id), './to')
|
||||||
formats = self._extract_f4m_formats(f4m_url, video_id)
|
formats = self._extract_f4m_formats(f4m_url, video_id)
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
@@ -1,5 +1,8 @@
|
|||||||
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_urllib_parse_urlencode,
|
compat_urllib_parse_urlencode,
|
||||||
@@ -8,6 +11,7 @@ from ..compat import (
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
get_element_by_attribute,
|
get_element_by_attribute,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
remove_start,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -15,7 +19,7 @@ class MiTeleIE(InfoExtractor):
|
|||||||
IE_DESC = 'mitele.es'
|
IE_DESC = 'mitele.es'
|
||||||
_VALID_URL = r'https?://www\.mitele\.es/[^/]+/[^/]+/[^/]+/(?P<id>[^/]+)/'
|
_VALID_URL = r'https?://www\.mitele\.es/[^/]+/[^/]+/[^/]+/(?P<id>[^/]+)/'
|
||||||
|
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://www.mitele.es/programas-tv/diario-de/la-redaccion/programa-144/',
|
'url': 'http://www.mitele.es/programas-tv/diario-de/la-redaccion/programa-144/',
|
||||||
# MD5 is unstable
|
# MD5 is unstable
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -24,10 +28,31 @@ class MiTeleIE(InfoExtractor):
|
|||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': 'Tor, la web invisible',
|
'title': 'Tor, la web invisible',
|
||||||
'description': 'md5:3b6fce7eaa41b2d97358726378d9369f',
|
'description': 'md5:3b6fce7eaa41b2d97358726378d9369f',
|
||||||
|
'series': 'Diario de',
|
||||||
|
'season': 'La redacción',
|
||||||
|
'episode': 'Programa 144',
|
||||||
'thumbnail': 're:(?i)^https?://.*\.jpg$',
|
'thumbnail': 're:(?i)^https?://.*\.jpg$',
|
||||||
'duration': 2913,
|
'duration': 2913,
|
||||||
},
|
},
|
||||||
}
|
}, {
|
||||||
|
# no explicit title
|
||||||
|
'url': 'http://www.mitele.es/programas-tv/cuarto-milenio/temporada-6/programa-226/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'eLZSwoEd1S3pVyUm8lc6F',
|
||||||
|
'display_id': 'programa-226',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'Cuarto Milenio - Temporada 6 - Programa 226',
|
||||||
|
'description': 'md5:50daf9fadefa4e62d9fc866d0c015701',
|
||||||
|
'series': 'Cuarto Milenio',
|
||||||
|
'season': 'Temporada 6',
|
||||||
|
'episode': 'Programa 226',
|
||||||
|
'thumbnail': 're:(?i)^https?://.*\.jpg$',
|
||||||
|
'duration': 7312,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
@@ -70,7 +95,22 @@ class MiTeleIE(InfoExtractor):
|
|||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
title = self._search_regex(
|
title = self._search_regex(
|
||||||
r'class="Destacado-text"[^>]*>\s*<strong>([^<]+)</strong>', webpage, 'title')
|
r'class="Destacado-text"[^>]*>\s*<strong>([^<]+)</strong>',
|
||||||
|
webpage, 'title', default=None)
|
||||||
|
|
||||||
|
mobj = re.search(r'''(?sx)
|
||||||
|
class="Destacado-text"[^>]*>.*?<h1>\s*
|
||||||
|
<span>(?P<series>[^<]+)</span>\s*
|
||||||
|
<span>(?P<season>[^<]+)</span>\s*
|
||||||
|
<span>(?P<episode>[^<]+)</span>''', webpage)
|
||||||
|
series, season, episode = mobj.groups() if mobj else [None] * 3
|
||||||
|
|
||||||
|
if not title:
|
||||||
|
if mobj:
|
||||||
|
title = '%s - %s - %s' % (series, season, episode)
|
||||||
|
else:
|
||||||
|
title = remove_start(self._search_regex(
|
||||||
|
r'<title>([^<]+)</title>', webpage, 'title'), 'Ver online ')
|
||||||
|
|
||||||
video_id = self._search_regex(
|
video_id = self._search_regex(
|
||||||
r'data-media-id\s*=\s*"([^"]+)"', webpage,
|
r'data-media-id\s*=\s*"([^"]+)"', webpage,
|
||||||
@@ -83,6 +123,9 @@ class MiTeleIE(InfoExtractor):
|
|||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': get_element_by_attribute('class', 'text', webpage),
|
'description': get_element_by_attribute('class', 'text', webpage),
|
||||||
|
'series': series,
|
||||||
|
'season': season,
|
||||||
|
'episode': episode,
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
@@ -6,6 +6,7 @@ from .common import InfoExtractor
|
|||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_urllib_parse_urlencode,
|
compat_urllib_parse_urlencode,
|
||||||
compat_str,
|
compat_str,
|
||||||
|
compat_xpath,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
@@ -84,9 +85,10 @@ class MTVServicesInfoExtractor(InfoExtractor):
|
|||||||
rtmp_video_url = rendition.find('./src').text
|
rtmp_video_url = rendition.find('./src').text
|
||||||
if rtmp_video_url.endswith('siteunavail.png'):
|
if rtmp_video_url.endswith('siteunavail.png'):
|
||||||
continue
|
continue
|
||||||
|
new_url = self._transform_rtmp_url(rtmp_video_url)
|
||||||
formats.append({
|
formats.append({
|
||||||
'ext': ext,
|
'ext': 'flv' if new_url.startswith('rtmp') else ext,
|
||||||
'url': self._transform_rtmp_url(rtmp_video_url),
|
'url': new_url,
|
||||||
'format_id': rendition.get('bitrate'),
|
'format_id': rendition.get('bitrate'),
|
||||||
'width': int(rendition.get('width')),
|
'width': int(rendition.get('width')),
|
||||||
'height': int(rendition.get('height')),
|
'height': int(rendition.get('height')),
|
||||||
@@ -139,9 +141,9 @@ class MTVServicesInfoExtractor(InfoExtractor):
|
|||||||
itemdoc, './/{http://search.yahoo.com/mrss/}category',
|
itemdoc, './/{http://search.yahoo.com/mrss/}category',
|
||||||
'scheme', 'urn:mtvn:video_title')
|
'scheme', 'urn:mtvn:video_title')
|
||||||
if title_el is None:
|
if title_el is None:
|
||||||
title_el = itemdoc.find('.//{http://search.yahoo.com/mrss/}title')
|
title_el = itemdoc.find(compat_xpath('.//{http://search.yahoo.com/mrss/}title'))
|
||||||
if title_el is None:
|
if title_el is None:
|
||||||
title_el = itemdoc.find('.//title') or itemdoc.find('./title')
|
title_el = itemdoc.find(compat_xpath('.//title'))
|
||||||
if title_el.text is None:
|
if title_el.text is None:
|
||||||
title_el = None
|
title_el = None
|
||||||
|
|
||||||
|
@@ -67,6 +67,23 @@ class NBCIE(InfoExtractor):
|
|||||||
# This video has expired but with an escaped embedURL
|
# This video has expired but with an escaped embedURL
|
||||||
'url': 'http://www.nbc.com/parenthood/episode-guide/season-5/just-like-at-home/515',
|
'url': 'http://www.nbc.com/parenthood/episode-guide/season-5/just-like-at-home/515',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# HLS streams requires the 'hdnea3' cookie
|
||||||
|
'url': 'http://www.nbc.com/Kings/video/goliath/n1806',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'n1806',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Goliath',
|
||||||
|
'description': 'When an unknown soldier saves the life of the King\'s son in battle, he\'s thrust into the limelight and politics of the kingdom.',
|
||||||
|
'timestamp': 1237100400,
|
||||||
|
'upload_date': '20090315',
|
||||||
|
'uploader': 'NBCU-COM',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
'skip': 'Only works from US',
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -249,6 +266,11 @@ class NBCNewsIE(ThePlatformIE):
|
|||||||
'url': 'http://www.nbcnews.com/watch/dateline/full-episode--deadly-betrayal-386250819952',
|
'url': 'http://www.nbcnews.com/watch/dateline/full-episode--deadly-betrayal-386250819952',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
# From http://www.vulture.com/2016/06/letterman-couldnt-care-less-about-late-night.html
|
||||||
|
'url': 'http://www.nbcnews.com/widget/video-embed/701714499682',
|
||||||
|
'only_matching': True,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -272,18 +294,17 @@ class NBCNewsIE(ThePlatformIE):
|
|||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
info = None
|
info = None
|
||||||
bootstrap_json = self._search_regex(
|
bootstrap_json = self._search_regex(
|
||||||
r'(?m)var\s+(?:bootstrapJson|playlistData)\s*=\s*({.+});?\s*$',
|
[r'(?m)(?:var\s+(?:bootstrapJson|playlistData)|NEWS\.videoObj)\s*=\s*({.+});?\s*$',
|
||||||
|
r'videoObj\s*:\s*({.+})', r'data-video="([^"]+)"'],
|
||||||
webpage, 'bootstrap json', default=None)
|
webpage, 'bootstrap json', default=None)
|
||||||
if bootstrap_json:
|
bootstrap = self._parse_json(
|
||||||
bootstrap = self._parse_json(bootstrap_json, display_id)
|
bootstrap_json, display_id, transform_source=unescapeHTML)
|
||||||
|
if 'results' in bootstrap:
|
||||||
info = bootstrap['results'][0]['video']
|
info = bootstrap['results'][0]['video']
|
||||||
|
elif 'video' in bootstrap:
|
||||||
|
info = bootstrap['video']
|
||||||
else:
|
else:
|
||||||
player_instance_json = self._search_regex(
|
info = bootstrap
|
||||||
r'videoObj\s*:\s*({.+})', webpage, 'player instance', default=None)
|
|
||||||
if not player_instance_json:
|
|
||||||
player_instance_json = self._html_search_regex(
|
|
||||||
r'data-video="([^"]+)"', webpage, 'video json')
|
|
||||||
info = self._parse_json(player_instance_json, display_id)
|
|
||||||
video_id = info['mpxId']
|
video_id = info['mpxId']
|
||||||
title = info['title']
|
title = info['title']
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from .mtv import MTVServicesInfoExtractor
|
from .mtv import MTVServicesInfoExtractor
|
||||||
from ..compat import compat_urllib_parse_urlencode
|
from ..compat import compat_urllib_parse_urlencode
|
||||||
|
from ..utils import update_url_query
|
||||||
|
|
||||||
|
|
||||||
class NickIE(MTVServicesInfoExtractor):
|
class NickIE(MTVServicesInfoExtractor):
|
||||||
@@ -61,3 +62,26 @@ class NickIE(MTVServicesInfoExtractor):
|
|||||||
|
|
||||||
def _extract_mgid(self, webpage):
|
def _extract_mgid(self, webpage):
|
||||||
return self._search_regex(r'data-contenturi="([^"]+)', webpage, 'mgid')
|
return self._search_regex(r'data-contenturi="([^"]+)', webpage, 'mgid')
|
||||||
|
|
||||||
|
|
||||||
|
class NickDeIE(MTVServicesInfoExtractor):
|
||||||
|
IE_NAME = 'nick.de'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?nick\.de/(?:playlist|shows)/(?:[^/]+/)*(?P<id>[^/?#&]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://www.nick.de/playlist/3773-top-videos/videos/episode/17306-zu-wasser-und-zu-land-rauchende-erdnusse',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.nick.de/shows/342-icarly',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
mrss_url = update_url_query(self._search_regex(
|
||||||
|
r'data-mrss=(["\'])(?P<url>http.+?)\1', webpage, 'mrss url', group='url'),
|
||||||
|
{'siteKey': 'nick.de'})
|
||||||
|
|
||||||
|
return self._get_videos_info_from_url(mrss_url, video_id)
|
||||||
|
@@ -163,7 +163,7 @@ class NRKTVIE(NRKBaseIE):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '20 spørsmål 23.05.2014',
|
'title': '20 spørsmål 23.05.2014',
|
||||||
'description': 'md5:bdea103bc35494c143c6a9acdd84887a',
|
'description': 'md5:bdea103bc35494c143c6a9acdd84887a',
|
||||||
'duration': 1741.52,
|
'duration': 1741,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://tv.nrk.no/program/mdfp15000514',
|
'url': 'https://tv.nrk.no/program/mdfp15000514',
|
||||||
@@ -173,7 +173,7 @@ class NRKTVIE(NRKBaseIE):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Grunnlovsjubiléet - Stor ståhei for ingenting 24.05.2014',
|
'title': 'Grunnlovsjubiléet - Stor ståhei for ingenting 24.05.2014',
|
||||||
'description': 'md5:89290c5ccde1b3a24bb8050ab67fe1db',
|
'description': 'md5:89290c5ccde1b3a24bb8050ab67fe1db',
|
||||||
'duration': 4605.08,
|
'duration': 4605,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# single playlist video
|
# single playlist video
|
||||||
@@ -260,30 +260,34 @@ class NRKPlaylistIE(InfoExtractor):
|
|||||||
|
|
||||||
class NRKSkoleIE(InfoExtractor):
|
class NRKSkoleIE(InfoExtractor):
|
||||||
IE_DESC = 'NRK Skole'
|
IE_DESC = 'NRK Skole'
|
||||||
_VALID_URL = r'https?://(?:www\.)?nrk\.no/skole/klippdetalj?.*\btopic=(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https?://(?:www\.)?nrk\.no/skole/?\?.*\bmediaId=(?P<id>\d+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://nrk.no/skole/klippdetalj?topic=nrk:klipp/616532',
|
'url': 'https://www.nrk.no/skole/?page=search&q=&mediaId=14099',
|
||||||
'md5': '04cd85877cc1913bce73c5d28a47e00f',
|
'md5': '6bc936b01f9dd8ed45bc58b252b2d9b6',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '6021',
|
'id': '6021',
|
||||||
'ext': 'flv',
|
'ext': 'mp4',
|
||||||
'title': 'Genetikk og eneggede tvillinger',
|
'title': 'Genetikk og eneggede tvillinger',
|
||||||
'description': 'md5:3aca25dcf38ec30f0363428d2b265f8d',
|
'description': 'md5:3aca25dcf38ec30f0363428d2b265f8d',
|
||||||
'duration': 399,
|
'duration': 399,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.nrk.no/skole/klippdetalj?topic=nrk%3Aklipp%2F616532#embed',
|
'url': 'https://www.nrk.no/skole/?page=objectives&subject=naturfag&objective=K15114&mediaId=19355',
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.nrk.no/skole/klippdetalj?topic=urn:x-mediadb:21379',
|
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = compat_urllib_parse_unquote(self._match_id(url))
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(
|
||||||
|
'https://mimir.nrk.no/plugin/1.0/static?mediaId=%s' % video_id,
|
||||||
|
video_id)
|
||||||
|
|
||||||
|
nrk_id = self._parse_json(
|
||||||
|
self._search_regex(
|
||||||
|
r'<script[^>]+type=["\']application/json["\'][^>]*>({.+?})</script>',
|
||||||
|
webpage, 'application json'),
|
||||||
|
video_id)['activeMedia']['psId']
|
||||||
|
|
||||||
nrk_id = self._search_regex(r'data-nrk-id=["\'](\d+)', webpage, 'nrk id')
|
|
||||||
return self.url_result('nrk:%s' % nrk_id)
|
return self.url_result('nrk:%s' % nrk_id)
|
||||||
|
@@ -8,6 +8,7 @@ from ..utils import (
|
|||||||
float_or_none,
|
float_or_none,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
unsmuggle_url,
|
unsmuggle_url,
|
||||||
|
determine_ext,
|
||||||
)
|
)
|
||||||
from ..compat import compat_urllib_parse_urlencode
|
from ..compat import compat_urllib_parse_urlencode
|
||||||
|
|
||||||
@@ -15,71 +16,80 @@ from ..compat import compat_urllib_parse_urlencode
|
|||||||
class OoyalaBaseIE(InfoExtractor):
|
class OoyalaBaseIE(InfoExtractor):
|
||||||
_PLAYER_BASE = 'http://player.ooyala.com/'
|
_PLAYER_BASE = 'http://player.ooyala.com/'
|
||||||
_CONTENT_TREE_BASE = _PLAYER_BASE + 'player_api/v1/content_tree/'
|
_CONTENT_TREE_BASE = _PLAYER_BASE + 'player_api/v1/content_tree/'
|
||||||
_AUTHORIZATION_URL_TEMPLATE = _PLAYER_BASE + 'sas/player_api/v1/authorization/embed_code/%s/%s?'
|
_AUTHORIZATION_URL_TEMPLATE = _PLAYER_BASE + 'sas/player_api/v2/authorization/embed_code/%s/%s?'
|
||||||
|
|
||||||
def _extract(self, content_tree_url, video_id, domain='example.org'):
|
def _extract(self, content_tree_url, video_id, domain='example.org'):
|
||||||
content_tree = self._download_json(content_tree_url, video_id)['content_tree']
|
content_tree = self._download_json(content_tree_url, video_id)['content_tree']
|
||||||
metadata = content_tree[list(content_tree)[0]]
|
metadata = content_tree[list(content_tree)[0]]
|
||||||
embed_code = metadata['embed_code']
|
embed_code = metadata['embed_code']
|
||||||
pcode = metadata.get('asset_pcode') or embed_code
|
pcode = metadata.get('asset_pcode') or embed_code
|
||||||
video_info = {
|
title = metadata['title']
|
||||||
'id': embed_code,
|
|
||||||
'title': metadata['title'],
|
auth_data = self._download_json(
|
||||||
'description': metadata.get('description'),
|
self._AUTHORIZATION_URL_TEMPLATE % (pcode, embed_code) +
|
||||||
'thumbnail': metadata.get('thumbnail_image') or metadata.get('promo_image'),
|
compat_urllib_parse_urlencode({
|
||||||
'duration': float_or_none(metadata.get('duration'), 1000),
|
'domain': domain,
|
||||||
}
|
'supportedFormats': 'mp4,rtmp,m3u8,hds',
|
||||||
|
}), video_id)
|
||||||
|
|
||||||
|
cur_auth_data = auth_data['authorization_data'][embed_code]
|
||||||
|
|
||||||
urls = []
|
urls = []
|
||||||
formats = []
|
formats = []
|
||||||
for supported_format in ('mp4', 'm3u8', 'hds', 'rtmp'):
|
if cur_auth_data['authorized']:
|
||||||
auth_data = self._download_json(
|
for stream in cur_auth_data['streams']:
|
||||||
self._AUTHORIZATION_URL_TEMPLATE % (pcode, embed_code) +
|
s_url = base64.b64decode(
|
||||||
compat_urllib_parse_urlencode({
|
stream['url']['data'].encode('ascii')).decode('utf-8')
|
||||||
'domain': domain,
|
if s_url in urls:
|
||||||
'supportedFormats': supported_format
|
continue
|
||||||
}),
|
urls.append(s_url)
|
||||||
video_id, 'Downloading %s JSON' % supported_format)
|
ext = determine_ext(s_url, None)
|
||||||
|
delivery_type = stream['delivery_type']
|
||||||
cur_auth_data = auth_data['authorization_data'][embed_code]
|
if delivery_type == 'hls' or ext == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
if cur_auth_data['authorized']:
|
s_url, embed_code, 'mp4', 'm3u8_native',
|
||||||
for stream in cur_auth_data['streams']:
|
m3u8_id='hls', fatal=False))
|
||||||
url = base64.b64decode(
|
elif delivery_type == 'hds' or ext == 'f4m':
|
||||||
stream['url']['data'].encode('ascii')).decode('utf-8')
|
formats.extend(self._extract_f4m_formats(
|
||||||
if url in urls:
|
s_url + '?hdcore=3.7.0', embed_code, f4m_id='hds', fatal=False))
|
||||||
continue
|
elif ext == 'smil':
|
||||||
urls.append(url)
|
formats.extend(self._extract_smil_formats(
|
||||||
delivery_type = stream['delivery_type']
|
s_url, embed_code, fatal=False))
|
||||||
if delivery_type == 'hls' or '.m3u8' in url:
|
else:
|
||||||
formats.extend(self._extract_m3u8_formats(
|
formats.append({
|
||||||
url, embed_code, 'mp4', 'm3u8_native',
|
'url': s_url,
|
||||||
m3u8_id='hls', fatal=False))
|
'ext': ext or stream.get('delivery_type'),
|
||||||
elif delivery_type == 'hds' or '.f4m' in url:
|
'vcodec': stream.get('video_codec'),
|
||||||
formats.extend(self._extract_f4m_formats(
|
'format_id': delivery_type,
|
||||||
url + '?hdcore=3.7.0', embed_code, f4m_id='hds', fatal=False))
|
'width': int_or_none(stream.get('width')),
|
||||||
elif '.smil' in url:
|
'height': int_or_none(stream.get('height')),
|
||||||
formats.extend(self._extract_smil_formats(
|
'abr': int_or_none(stream.get('audio_bitrate')),
|
||||||
url, embed_code, fatal=False))
|
'vbr': int_or_none(stream.get('video_bitrate')),
|
||||||
else:
|
'fps': float_or_none(stream.get('framerate')),
|
||||||
formats.append({
|
})
|
||||||
'url': url,
|
else:
|
||||||
'ext': stream.get('delivery_type'),
|
raise ExtractorError('%s said: %s' % (
|
||||||
'vcodec': stream.get('video_codec'),
|
self.IE_NAME, cur_auth_data['message']), expected=True)
|
||||||
'format_id': delivery_type,
|
|
||||||
'width': int_or_none(stream.get('width')),
|
|
||||||
'height': int_or_none(stream.get('height')),
|
|
||||||
'abr': int_or_none(stream.get('audio_bitrate')),
|
|
||||||
'vbr': int_or_none(stream.get('video_bitrate')),
|
|
||||||
'fps': float_or_none(stream.get('framerate')),
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
raise ExtractorError('%s said: %s' % (
|
|
||||||
self.IE_NAME, cur_auth_data['message']), expected=True)
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
video_info['formats'] = formats
|
subtitles = {}
|
||||||
return video_info
|
for lang, sub in metadata.get('closed_captions_vtt', {}).get('captions', {}).items():
|
||||||
|
sub_url = sub.get('url')
|
||||||
|
if not sub_url:
|
||||||
|
continue
|
||||||
|
subtitles[lang] = [{
|
||||||
|
'url': sub_url,
|
||||||
|
}]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': embed_code,
|
||||||
|
'title': title,
|
||||||
|
'description': metadata.get('description'),
|
||||||
|
'thumbnail': metadata.get('thumbnail_image') or metadata.get('promo_image'),
|
||||||
|
'duration': float_or_none(metadata.get('duration'), 1000),
|
||||||
|
'subtitles': subtitles,
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class OoyalaIE(OoyalaBaseIE):
|
class OoyalaIE(OoyalaBaseIE):
|
||||||
|
@@ -14,7 +14,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class OpenloadIE(InfoExtractor):
|
class OpenloadIE(InfoExtractor):
|
||||||
_VALID_URL = r'https://openload.(?:co|io)/(?:f|embed)/(?P<id>[a-zA-Z0-9-]+)'
|
_VALID_URL = r'https://openload.(?:co|io)/(?:f|embed)/(?P<id>[a-zA-Z0-9-_]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://openload.co/f/kUEfGclsU9o',
|
'url': 'https://openload.co/f/kUEfGclsU9o',
|
||||||
@@ -31,6 +31,9 @@ class OpenloadIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://openload.io/f/ZAn6oz-VZGE/',
|
'url': 'https://openload.io/f/ZAn6oz-VZGE/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://openload.co/f/_-ztPaZtMhM/',
|
||||||
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
# unavailable via https://openload.co/f/Sxz5sADo82g/, different layout
|
# unavailable via https://openload.co/f/Sxz5sADo82g/, different layout
|
||||||
# for title and ext
|
# for title and ext
|
||||||
@@ -100,7 +103,7 @@ class OpenloadIE(InfoExtractor):
|
|||||||
raise ExtractorError('File not found', expected=True)
|
raise ExtractorError('File not found', expected=True)
|
||||||
|
|
||||||
code = self._search_regex(
|
code = self._search_regex(
|
||||||
r'</video>\s*</div>\s*<script[^>]+>([^<]+)</script>',
|
r'</video>\s*</div>\s*<script[^>]+>[^>]+</script>\s*<script[^>]+>([^<]+)</script>',
|
||||||
webpage, 'JS code')
|
webpage, 'JS code')
|
||||||
|
|
||||||
decoded = self.openload_decode(code)
|
decoded = self.openload_decode(code)
|
||||||
|
@@ -2,7 +2,10 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import parse_iso8601
|
from ..utils import (
|
||||||
|
parse_iso8601,
|
||||||
|
unescapeHTML,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PeriscopeIE(InfoExtractor):
|
class PeriscopeIE(InfoExtractor):
|
||||||
@@ -42,8 +45,11 @@ class PeriscopeIE(InfoExtractor):
|
|||||||
broadcast = broadcast_data['broadcast']
|
broadcast = broadcast_data['broadcast']
|
||||||
status = broadcast['status']
|
status = broadcast['status']
|
||||||
|
|
||||||
uploader = broadcast.get('user_display_name') or broadcast_data.get('user', {}).get('display_name')
|
user = broadcast_data.get('user', {})
|
||||||
uploader_id = broadcast.get('user_id') or broadcast_data.get('user', {}).get('id')
|
|
||||||
|
uploader = broadcast.get('user_display_name') or user.get('display_name')
|
||||||
|
uploader_id = (broadcast.get('username') or user.get('username') or
|
||||||
|
broadcast.get('user_id') or user.get('id'))
|
||||||
|
|
||||||
title = '%s - %s' % (uploader, status) if uploader else status
|
title = '%s - %s' % (uploader, status) if uploader else status
|
||||||
state = broadcast.get('state').lower()
|
state = broadcast.get('state').lower()
|
||||||
@@ -92,6 +98,7 @@ class PeriscopeUserIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'LularoeHusbandMike',
|
'id': 'LularoeHusbandMike',
|
||||||
'title': 'LULAROE HUSBAND MIKE',
|
'title': 'LULAROE HUSBAND MIKE',
|
||||||
|
'description': 'md5:6cf4ec8047768098da58e446e82c82f0',
|
||||||
},
|
},
|
||||||
# Periscope only shows videos in the last 24 hours, so it's possible to
|
# Periscope only shows videos in the last 24 hours, so it's possible to
|
||||||
# get 0 videos
|
# get 0 videos
|
||||||
@@ -103,16 +110,19 @@ class PeriscopeUserIE(InfoExtractor):
|
|||||||
|
|
||||||
webpage = self._download_webpage(url, user_id)
|
webpage = self._download_webpage(url, user_id)
|
||||||
|
|
||||||
broadcast_data = self._parse_json(self._html_search_meta(
|
data_store = self._parse_json(
|
||||||
'broadcast-data', webpage, default='{}'), user_id)
|
unescapeHTML(self._search_regex(
|
||||||
username = broadcast_data.get('user', {}).get('display_name')
|
r'data-store=(["\'])(?P<data>.+?)\1',
|
||||||
user_broadcasts = self._parse_json(
|
webpage, 'data store', default='{}', group='data')),
|
||||||
self._html_search_meta('user-broadcasts', webpage, default='{}'),
|
|
||||||
user_id)
|
user_id)
|
||||||
|
|
||||||
|
user = data_store.get('User', {}).get('user', {})
|
||||||
|
title = user.get('display_name') or user.get('username')
|
||||||
|
description = user.get('description')
|
||||||
|
|
||||||
entries = [
|
entries = [
|
||||||
self.url_result(
|
self.url_result(
|
||||||
'https://www.periscope.tv/%s/%s' % (user_id, broadcast['id']))
|
'https://www.periscope.tv/%s/%s' % (user_id, broadcast['id']))
|
||||||
for broadcast in user_broadcasts.get('broadcasts', [])]
|
for broadcast in data_store.get('UserBroadcastHistory', {}).get('broadcasts', [])]
|
||||||
|
|
||||||
return self.playlist_result(entries, user_id, username)
|
return self.playlist_result(entries, user_id, title, description)
|
||||||
|
@@ -4,9 +4,8 @@ import re
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
xpath_text,
|
dict_get,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -23,6 +22,19 @@ class PlaywireIE(InfoExtractor):
|
|||||||
'duration': 145.94,
|
'duration': 145.94,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
|
# m3u8 in f4m
|
||||||
|
'url': 'http://config.playwire.com/21772/videos/v2/4840492/zeus.json',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4840492',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'ITV EL SHOW FULL',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# Multiple resolutions while bitrates missing
|
||||||
'url': 'http://cdn.playwire.com/11625/embed/85228.html',
|
'url': 'http://cdn.playwire.com/11625/embed/85228.html',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
@@ -48,25 +60,10 @@ class PlaywireIE(InfoExtractor):
|
|||||||
thumbnail = content.get('poster')
|
thumbnail = content.get('poster')
|
||||||
src = content['media']['f4m']
|
src = content['media']['f4m']
|
||||||
|
|
||||||
f4m = self._download_xml(src, video_id)
|
formats = self._extract_f4m_formats(src, video_id, m3u8_id='hls')
|
||||||
base_url = xpath_text(f4m, './{http://ns.adobe.com/f4m/1.0}baseURL', 'base url', fatal=True)
|
for a_format in formats:
|
||||||
formats = []
|
if not dict_get(a_format, ['tbr', 'width', 'height']):
|
||||||
for media in f4m.findall('./{http://ns.adobe.com/f4m/1.0}media'):
|
a_format['quality'] = 1 if '-hd.' in a_format['url'] else 0
|
||||||
media_url = media.get('url')
|
|
||||||
if not media_url:
|
|
||||||
continue
|
|
||||||
tbr = int_or_none(media.get('bitrate'))
|
|
||||||
width = int_or_none(media.get('width'))
|
|
||||||
height = int_or_none(media.get('height'))
|
|
||||||
f = {
|
|
||||||
'url': '%s/%s' % (base_url, media.attrib['url']),
|
|
||||||
'tbr': tbr,
|
|
||||||
'width': width,
|
|
||||||
'height': height,
|
|
||||||
}
|
|
||||||
if not (tbr or width or height):
|
|
||||||
f['quality'] = 1 if '-hd.' in media_url else 0
|
|
||||||
formats.append(f)
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -1,19 +1,32 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
qualities,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PornHdIE(InfoExtractor):
|
class PornHdIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?pornhd\.com/(?:[a-z]{2,4}/)?videos/(?P<id>\d+)(?:/(?P<display_id>.+))?'
|
_VALID_URL = r'https?://(?:www\.)?pornhd\.com/(?:[a-z]{2,4}/)?videos/(?P<id>\d+)(?:/(?P<display_id>.+))?'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
|
'url': 'http://www.pornhd.com/videos/9864/selfie-restroom-masturbation-fun-with-chubby-cutie-hd-porn-video',
|
||||||
|
'md5': 'c8b964b1f0a4b5f7f28ae3a5c9f86ad5',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '9864',
|
||||||
|
'display_id': 'selfie-restroom-masturbation-fun-with-chubby-cutie-hd-porn-video',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Restroom selfie masturbation',
|
||||||
|
'description': 'md5:3748420395e03e31ac96857a8f125b2b',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg',
|
||||||
|
'view_count': int,
|
||||||
|
'age_limit': 18,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# removed video
|
||||||
'url': 'http://www.pornhd.com/videos/1962/sierra-day-gets-his-cum-all-over-herself-hd-porn-video',
|
'url': 'http://www.pornhd.com/videos/1962/sierra-day-gets-his-cum-all-over-herself-hd-porn-video',
|
||||||
'md5': '956b8ca569f7f4d8ec563e2c41598441',
|
'md5': '956b8ca569f7f4d8ec563e2c41598441',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -25,8 +38,9 @@ class PornHdIE(InfoExtractor):
|
|||||||
'thumbnail': 're:^https?://.*\.jpg',
|
'thumbnail': 're:^https?://.*\.jpg',
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'age_limit': 18,
|
'age_limit': 18,
|
||||||
}
|
},
|
||||||
}
|
'skip': 'Not available anymore',
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
@@ -38,28 +52,38 @@ class PornHdIE(InfoExtractor):
|
|||||||
title = self._html_search_regex(
|
title = self._html_search_regex(
|
||||||
[r'<span[^>]+class=["\']video-name["\'][^>]*>([^<]+)',
|
[r'<span[^>]+class=["\']video-name["\'][^>]*>([^<]+)',
|
||||||
r'<title>(.+?) - .*?[Pp]ornHD.*?</title>'], webpage, 'title')
|
r'<title>(.+?) - .*?[Pp]ornHD.*?</title>'], webpage, 'title')
|
||||||
description = self._html_search_regex(
|
|
||||||
r'<div class="description">([^<]+)</div>', webpage, 'description', fatal=False)
|
|
||||||
view_count = int_or_none(self._html_search_regex(
|
|
||||||
r'(\d+) views\s*</span>', webpage, 'view count', fatal=False))
|
|
||||||
thumbnail = self._search_regex(
|
|
||||||
r"'poster'\s*:\s*'([^']+)'", webpage, 'thumbnail', fatal=False)
|
|
||||||
|
|
||||||
quality = qualities(['sd', 'hd'])
|
sources = self._parse_json(js_to_json(self._search_regex(
|
||||||
sources = json.loads(js_to_json(self._search_regex(
|
|
||||||
r"(?s)'sources'\s*:\s*(\{.+?\})\s*\}[;,)]",
|
r"(?s)'sources'\s*:\s*(\{.+?\})\s*\}[;,)]",
|
||||||
webpage, 'sources')))
|
webpage, 'sources', default='{}')), video_id)
|
||||||
|
|
||||||
|
if not sources:
|
||||||
|
message = self._html_search_regex(
|
||||||
|
r'(?s)<(div|p)[^>]+class="no-video"[^>]*>(?P<value>.+?)</\1',
|
||||||
|
webpage, 'error message', group='value')
|
||||||
|
raise ExtractorError('%s said: %s' % (self.IE_NAME, message), expected=True)
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for qname, video_url in sources.items():
|
for format_id, video_url in sources.items():
|
||||||
if not video_url:
|
if not video_url:
|
||||||
continue
|
continue
|
||||||
|
height = int_or_none(self._search_regex(
|
||||||
|
r'^(\d+)[pP]', format_id, 'height', default=None))
|
||||||
formats.append({
|
formats.append({
|
||||||
'url': video_url,
|
'url': video_url,
|
||||||
'format_id': qname,
|
'format_id': format_id,
|
||||||
'quality': quality(qname),
|
'height': height,
|
||||||
})
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
description = self._html_search_regex(
|
||||||
|
r'<(div|p)[^>]+class="description"[^>]*>(?P<value>[^<]+)</\1',
|
||||||
|
webpage, 'description', fatal=False, group='value')
|
||||||
|
view_count = int_or_none(self._html_search_regex(
|
||||||
|
r'(\d+) views\s*<', webpage, 'view count', fatal=False))
|
||||||
|
thumbnail = self._search_regex(
|
||||||
|
r"'poster'\s*:\s*'([^']+)'", webpage, 'thumbnail', fatal=False)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
@@ -39,7 +40,25 @@ class PornHubIE(InfoExtractor):
|
|||||||
'dislike_count': int,
|
'dislike_count': int,
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'age_limit': 18,
|
'age_limit': 18,
|
||||||
}
|
},
|
||||||
|
}, {
|
||||||
|
# non-ASCII title
|
||||||
|
'url': 'http://www.pornhub.com/view_video.php?viewkey=1331683002',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1331683002',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '重庆婷婷女王足交',
|
||||||
|
'uploader': 'cj397186295',
|
||||||
|
'duration': 1753,
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'dislike_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'age_limit': 18,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.pornhub.com/view_video.php?viewkey=ph557bbb6676d2d',
|
'url': 'http://www.pornhub.com/view_video.php?viewkey=ph557bbb6676d2d',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@@ -76,19 +95,25 @@ class PornHubIE(InfoExtractor):
|
|||||||
'PornHub said: %s' % error_msg,
|
'PornHub said: %s' % error_msg,
|
||||||
expected=True, video_id=video_id)
|
expected=True, video_id=video_id)
|
||||||
|
|
||||||
|
# video_title from flashvars contains whitespace instead of non-ASCII (see
|
||||||
|
# http://www.pornhub.com/view_video.php?viewkey=1331683002), not relying
|
||||||
|
# on that anymore.
|
||||||
|
title = self._html_search_meta(
|
||||||
|
'twitter:title', webpage, default=None) or self._search_regex(
|
||||||
|
(r'<h1[^>]+class=["\']title["\'][^>]*>(?P<title>[^<]+)',
|
||||||
|
r'<div[^>]+data-video-title=(["\'])(?P<title>.+?)\1',
|
||||||
|
r'shareTitle\s*=\s*(["\'])(?P<title>.+?)\1'),
|
||||||
|
webpage, 'title', group='title')
|
||||||
|
|
||||||
flashvars = self._parse_json(
|
flashvars = self._parse_json(
|
||||||
self._search_regex(
|
self._search_regex(
|
||||||
r'var\s+flashvars_\d+\s*=\s*({.+?});', webpage, 'flashvars', default='{}'),
|
r'var\s+flashvars_\d+\s*=\s*({.+?});', webpage, 'flashvars', default='{}'),
|
||||||
video_id)
|
video_id)
|
||||||
if flashvars:
|
if flashvars:
|
||||||
video_title = flashvars.get('video_title')
|
|
||||||
thumbnail = flashvars.get('image_url')
|
thumbnail = flashvars.get('image_url')
|
||||||
duration = int_or_none(flashvars.get('video_duration'))
|
duration = int_or_none(flashvars.get('video_duration'))
|
||||||
else:
|
else:
|
||||||
video_title, thumbnail, duration = [None] * 3
|
title, thumbnail, duration = [None] * 3
|
||||||
|
|
||||||
if not video_title:
|
|
||||||
video_title = self._html_search_regex(r'<h1 [^>]+>([^<]+)', webpage, 'title')
|
|
||||||
|
|
||||||
video_uploader = self._html_search_regex(
|
video_uploader = self._html_search_regex(
|
||||||
r'(?s)From: .+?<(?:a href="/users/|a href="/channels/|span class="username)[^>]+>(.+?)<',
|
r'(?s)From: .+?<(?:a href="/users/|a href="/channels/|span class="username)[^>]+>(.+?)<',
|
||||||
@@ -137,7 +162,7 @@ class PornHubIE(InfoExtractor):
|
|||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'uploader': video_uploader,
|
'uploader': video_uploader,
|
||||||
'title': video_title,
|
'title': title,
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
'view_count': view_count,
|
'view_count': view_count,
|
||||||
|
@@ -2,22 +2,19 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import int_or_none
|
||||||
js_to_json,
|
|
||||||
unescapeHTML,
|
|
||||||
int_or_none,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class R7IE(InfoExtractor):
|
class R7IE(InfoExtractor):
|
||||||
_VALID_URL = r'''(?x)https?://
|
_VALID_URL = r'''(?x)
|
||||||
|
https?://
|
||||||
(?:
|
(?:
|
||||||
(?:[a-zA-Z]+)\.r7\.com(?:/[^/]+)+/idmedia/|
|
(?:[a-zA-Z]+)\.r7\.com(?:/[^/]+)+/idmedia/|
|
||||||
noticias\.r7\.com(?:/[^/]+)+/[^/]+-|
|
noticias\.r7\.com(?:/[^/]+)+/[^/]+-|
|
||||||
player\.r7\.com/video/i/
|
player\.r7\.com/video/i/
|
||||||
)
|
)
|
||||||
(?P<id>[\da-f]{24})
|
(?P<id>[\da-f]{24})
|
||||||
'''
|
'''
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://videos.r7.com/policiais-humilham-suspeito-a-beira-da-morte-morre-com-dignidade-/idmedia/54e7050b0cf2ff57e0279389.html',
|
'url': 'http://videos.r7.com/policiais-humilham-suspeito-a-beira-da-morte-morre-com-dignidade-/idmedia/54e7050b0cf2ff57e0279389.html',
|
||||||
'md5': '403c4e393617e8e8ddc748978ee8efde',
|
'md5': '403c4e393617e8e8ddc748978ee8efde',
|
||||||
@@ -25,6 +22,7 @@ class R7IE(InfoExtractor):
|
|||||||
'id': '54e7050b0cf2ff57e0279389',
|
'id': '54e7050b0cf2ff57e0279389',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Policiais humilham suspeito à beira da morte: "Morre com dignidade"',
|
'title': 'Policiais humilham suspeito à beira da morte: "Morre com dignidade"',
|
||||||
|
'description': 'md5:01812008664be76a6479aa58ec865b72',
|
||||||
'thumbnail': 're:^https?://.*\.jpg$',
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
'duration': 98,
|
'duration': 98,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
@@ -44,45 +42,72 @@ class R7IE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
webpage = self._download_webpage(
|
video = self._download_json(
|
||||||
'http://player.r7.com/video/i/%s' % video_id, video_id)
|
'http://player-api.r7.com/video/i/%s' % video_id, video_id)
|
||||||
|
|
||||||
item = self._parse_json(js_to_json(self._search_regex(
|
title = video['title']
|
||||||
r'(?s)var\s+item\s*=\s*({.+?});', webpage, 'player')), video_id)
|
|
||||||
|
|
||||||
title = unescapeHTML(item['title'])
|
|
||||||
thumbnail = item.get('init', {}).get('thumbUri')
|
|
||||||
duration = None
|
|
||||||
|
|
||||||
statistics = item.get('statistics', {})
|
|
||||||
like_count = int_or_none(statistics.get('likes'))
|
|
||||||
view_count = int_or_none(statistics.get('views'))
|
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for format_key, format_dict in item['playlist'][0].items():
|
media_url_hls = video.get('media_url_hls')
|
||||||
src = format_dict.get('src')
|
if media_url_hls:
|
||||||
if not src:
|
formats.extend(self._extract_m3u8_formats(
|
||||||
continue
|
media_url_hls, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
format_id = format_dict.get('format') or format_key
|
m3u8_id='hls', fatal=False))
|
||||||
if duration is None:
|
media_url = video.get('media_url')
|
||||||
duration = format_dict.get('duration')
|
if media_url:
|
||||||
if '.f4m' in src:
|
f = {
|
||||||
formats.extend(self._extract_f4m_formats(src, video_id, preference=-1))
|
'url': media_url,
|
||||||
elif src.endswith('.m3u8'):
|
'format_id': 'http',
|
||||||
formats.extend(self._extract_m3u8_formats(src, video_id, 'mp4', preference=-2))
|
}
|
||||||
else:
|
# m3u8 format always matches the http format, let's copy metadata from
|
||||||
formats.append({
|
# one to another
|
||||||
'url': src,
|
m3u8_formats = list(filter(
|
||||||
'format_id': format_id,
|
lambda f: f.get('vcodec') != 'none' and f.get('resolution') != 'multiple',
|
||||||
})
|
formats))
|
||||||
|
if len(m3u8_formats) == 1:
|
||||||
|
f_copy = m3u8_formats[0].copy()
|
||||||
|
f_copy.update(f)
|
||||||
|
f_copy['protocol'] = 'http'
|
||||||
|
f = f_copy
|
||||||
|
formats.append(f)
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
description = video.get('description')
|
||||||
|
thumbnail = video.get('thumb')
|
||||||
|
duration = int_or_none(video.get('media_duration'))
|
||||||
|
like_count = int_or_none(video.get('likes'))
|
||||||
|
view_count = int_or_none(video.get('views'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
|
'description': description,
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
'like_count': like_count,
|
'like_count': like_count,
|
||||||
'view_count': view_count,
|
'view_count': view_count,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class R7ArticleIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:[a-zA-Z]+)\.r7\.com/(?:[^/]+/)+[^/?#&]+-(?P<id>\d+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://tv.r7.com/record-play/balanco-geral/videos/policiais-humilham-suspeito-a-beira-da-morte-morre-com-dignidade-16102015',
|
||||||
|
'only_matching': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def suitable(cls, url):
|
||||||
|
return False if R7IE.suitable(url) else super(R7ArticleIE, cls).suitable(url)
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
|
||||||
|
video_id = self._search_regex(
|
||||||
|
r'<div[^>]+(?:id=["\']player-|class=["\']embed["\'][^>]+id=["\'])([\da-f]{24})',
|
||||||
|
webpage, 'video id')
|
||||||
|
|
||||||
|
return self.url_result('http://player.r7.com/video/i/%s' % video_id, R7IE.ie_key())
|
||||||
|
130
youtube_dl/extractor/radiocanada.py
Normal file
130
youtube_dl/extractor/radiocanada.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
xpath_text,
|
||||||
|
find_xpath_attr,
|
||||||
|
determine_ext,
|
||||||
|
int_or_none,
|
||||||
|
unified_strdate,
|
||||||
|
xpath_element,
|
||||||
|
ExtractorError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RadioCanadaIE(InfoExtractor):
|
||||||
|
IE_NAME = 'radiocanada'
|
||||||
|
_VALID_URL = r'(?:radiocanada:|https?://ici\.radio-canada\.ca/widgets/mediaconsole/)(?P<app_code>[^:/]+)[:/](?P<id>[0-9]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://ici.radio-canada.ca/widgets/mediaconsole/medianet/7184272',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '7184272',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'Le parcours du tireur capté sur vidéo',
|
||||||
|
'description': 'Images des caméras de surveillance fournies par la GRC montrant le parcours du tireur d\'Ottawa',
|
||||||
|
'upload_date': '20141023',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
app_code, video_id = re.match(self._VALID_URL, url).groups()
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
# TODO: extract m3u8 and f4m formats
|
||||||
|
# m3u8 formats can be extracted using ipad device_type return 403 error code when ffmpeg try to download segements
|
||||||
|
# f4m formats can be extracted using flashhd device_type but they produce unplayable file
|
||||||
|
for device_type in ('flash',):
|
||||||
|
v_data = self._download_xml(
|
||||||
|
'http://api.radio-canada.ca/validationMedia/v1/Validation.ashx',
|
||||||
|
video_id, note='Downloading %s XML' % device_type, query={
|
||||||
|
'appCode': app_code,
|
||||||
|
'idMedia': video_id,
|
||||||
|
'connectionType': 'broadband',
|
||||||
|
'multibitrate': 'true',
|
||||||
|
'deviceType': device_type,
|
||||||
|
# paysJ391wsHjbOJwvCs26toz and bypasslock are used to bypass geo-restriction
|
||||||
|
'paysJ391wsHjbOJwvCs26toz': 'CA',
|
||||||
|
'bypasslock': 'NZt5K62gRqfc',
|
||||||
|
})
|
||||||
|
v_url = xpath_text(v_data, 'url')
|
||||||
|
if not v_url:
|
||||||
|
continue
|
||||||
|
if v_url == 'null':
|
||||||
|
raise ExtractorError('%s said: %s' % (
|
||||||
|
self.IE_NAME, xpath_text(v_data, 'message')), expected=True)
|
||||||
|
ext = determine_ext(v_url)
|
||||||
|
if ext == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
v_url, video_id, 'mp4', m3u8_id='hls', fatal=False))
|
||||||
|
elif ext == 'f4m':
|
||||||
|
formats.extend(self._extract_f4m_formats(v_url, video_id, f4m_id='hds', fatal=False))
|
||||||
|
else:
|
||||||
|
ext = determine_ext(v_url)
|
||||||
|
bitrates = xpath_element(v_data, 'bitrates')
|
||||||
|
for url_e in bitrates.findall('url'):
|
||||||
|
tbr = int_or_none(url_e.get('bitrate'))
|
||||||
|
if not tbr:
|
||||||
|
continue
|
||||||
|
formats.append({
|
||||||
|
'format_id': 'rtmp-%d' % tbr,
|
||||||
|
'url': re.sub(r'\d+\.%s' % ext, '%d.%s' % (tbr, ext), v_url),
|
||||||
|
'ext': 'flv',
|
||||||
|
'protocol': 'rtmp',
|
||||||
|
'width': int_or_none(url_e.get('width')),
|
||||||
|
'height': int_or_none(url_e.get('height')),
|
||||||
|
'tbr': tbr,
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
metadata = self._download_xml(
|
||||||
|
'http://api.radio-canada.ca/metaMedia/v1/index.ashx',
|
||||||
|
video_id, note='Downloading metadata XML', query={
|
||||||
|
'appCode': app_code,
|
||||||
|
'idMedia': video_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_meta(name):
|
||||||
|
el = find_xpath_attr(metadata, './/Meta', 'name', name)
|
||||||
|
return el.text if el is not None else None
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': get_meta('Title'),
|
||||||
|
'description': get_meta('Description') or get_meta('ShortDescription'),
|
||||||
|
'thumbnail': get_meta('imageHR') or get_meta('imageMR') or get_meta('imageBR'),
|
||||||
|
'duration': int_or_none(get_meta('length')),
|
||||||
|
'series': get_meta('Emission'),
|
||||||
|
'season_number': int_or_none('SrcSaison'),
|
||||||
|
'episode_number': int_or_none('SrcEpisode'),
|
||||||
|
'upload_date': unified_strdate(get_meta('Date')),
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RadioCanadaAudioVideoIE(InfoExtractor):
|
||||||
|
'radiocanada:audiovideo'
|
||||||
|
_VALID_URL = r'https?://ici\.radio-canada\.ca/audio-video/media-(?P<id>[0-9]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://ici.radio-canada.ca/audio-video/media-7527184/barack-obama-au-vietnam',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '7527184',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'Barack Obama au Vietnam',
|
||||||
|
'description': 'Les États-Unis lèvent l\'embargo sur la vente d\'armes qui datait de la guerre du Vietnam',
|
||||||
|
'upload_date': '20160523',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# rtmp download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
return self.url_result('radiocanada:medianet:%s' % self._match_id(url))
|
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import(
|
from ..utils import (
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
str_to_int,
|
str_to_int,
|
||||||
)
|
)
|
||||||
|
69
youtube_dl/extractor/reuters.py
Normal file
69
youtube_dl/extractor/reuters.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
js_to_json,
|
||||||
|
int_or_none,
|
||||||
|
unescapeHTML,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReutersIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?reuters\.com/.*?\?.*?videoId=(?P<id>[0-9]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://www.reuters.com/video/2016/05/20/san-francisco-police-chief-resigns?videoId=368575562',
|
||||||
|
'md5': '8015113643a0b12838f160b0b81cc2ee',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '368575562',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'San Francisco police chief resigns',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(
|
||||||
|
'http://www.reuters.com/assets/iframe/yovideo?videoId=%s' % video_id, video_id)
|
||||||
|
video_data = js_to_json(self._search_regex(
|
||||||
|
r'(?s)Reuters\.yovideo\.drawPlayer\(({.*?})\);',
|
||||||
|
webpage, 'video data'))
|
||||||
|
|
||||||
|
def get_json_value(key, fatal=False):
|
||||||
|
return self._search_regex('"%s"\s*:\s*"([^"]+)"' % key, video_data, key, fatal=fatal)
|
||||||
|
|
||||||
|
title = unescapeHTML(get_json_value('title', fatal=True))
|
||||||
|
mmid, fid = re.search(r',/(\d+)\?f=(\d+)', get_json_value('flv', fatal=True)).groups()
|
||||||
|
|
||||||
|
mas_data = self._download_json(
|
||||||
|
'http://mas-e.cds1.yospace.com/mas/%s/%s?trans=json' % (mmid, fid),
|
||||||
|
video_id, transform_source=js_to_json)
|
||||||
|
formats = []
|
||||||
|
for f in mas_data:
|
||||||
|
f_url = f.get('url')
|
||||||
|
if not f_url:
|
||||||
|
continue
|
||||||
|
method = f.get('method')
|
||||||
|
if method == 'hls':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
f_url, video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False))
|
||||||
|
else:
|
||||||
|
container = f.get('container')
|
||||||
|
ext = '3gp' if method == 'mobile' else container
|
||||||
|
formats.append({
|
||||||
|
'format_id': ext,
|
||||||
|
'url': f_url,
|
||||||
|
'ext': ext,
|
||||||
|
'container': container if method != 'mobile' else None,
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'thumbnail': get_json_value('thumb'),
|
||||||
|
'duration': int_or_none(get_json_value('seconds')),
|
||||||
|
'formats': formats,
|
||||||
|
}
|
@@ -13,8 +13,64 @@ from ..utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Revision3EmbedIE(InfoExtractor):
|
||||||
|
IE_NAME = 'revision3:embed'
|
||||||
|
_VALID_URL = r'(?:revision3:(?:(?P<playlist_type>[^:]+):)?|https?://(?:(?:(?:www|embed)\.)?(?:revision3|animalist)|(?:(?:api|embed)\.)?seekernetwork)\.com/player/embed\?videoId=)(?P<playlist_id>\d+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://api.seekernetwork.com/player/embed?videoId=67558',
|
||||||
|
'md5': '83bcd157cab89ad7318dd7b8c9cf1306',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '67558',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'The Pros & Cons Of Zoos',
|
||||||
|
'description': 'Zoos are often depicted as a terrible place for animals to live, but is there any truth to this?',
|
||||||
|
'uploader_id': 'dnews',
|
||||||
|
'uploader': 'DNews',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_API_KEY = 'ba9c741bce1b9d8e3defcc22193f3651b8867e62'
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
playlist_id = mobj.group('playlist_id')
|
||||||
|
playlist_type = mobj.group('playlist_type') or 'video_id'
|
||||||
|
video_data = self._download_json(
|
||||||
|
'http://revision3.com/api/getPlaylist.json', playlist_id, query={
|
||||||
|
'api_key': self._API_KEY,
|
||||||
|
'codecs': 'h264,vp8,theora',
|
||||||
|
playlist_type: playlist_id,
|
||||||
|
})['items'][0]
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for vcodec, media in video_data['media'].items():
|
||||||
|
for quality_id, quality in media.items():
|
||||||
|
if quality_id == 'hls':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
quality['url'], playlist_id, 'mp4',
|
||||||
|
'm3u8_native', m3u8_id='hls', fatal=False))
|
||||||
|
else:
|
||||||
|
formats.append({
|
||||||
|
'url': quality['url'],
|
||||||
|
'format_id': '%s-%s' % (vcodec, quality_id),
|
||||||
|
'tbr': int_or_none(quality.get('bitrate')),
|
||||||
|
'vcodec': vcodec,
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': playlist_id,
|
||||||
|
'title': unescapeHTML(video_data['title']),
|
||||||
|
'description': unescapeHTML(video_data.get('summary')),
|
||||||
|
'uploader': video_data.get('show', {}).get('name'),
|
||||||
|
'uploader_id': video_data.get('show', {}).get('slug'),
|
||||||
|
'duration': int_or_none(video_data.get('duration')),
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Revision3IE(InfoExtractor):
|
class Revision3IE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?P<domain>(?:revision3|testtube|animalist)\.com)/(?P<id>[^/]+(?:/[^/?#]+)?)'
|
IE_NAME = 'revision'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?(?P<domain>(?:revision3|animalist)\.com)/(?P<id>[^/]+(?:/[^/?#]+)?)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.revision3.com/technobuffalo/5-google-predictions-for-2016',
|
'url': 'http://www.revision3.com/technobuffalo/5-google-predictions-for-2016',
|
||||||
'md5': 'd94a72d85d0a829766de4deb8daaf7df',
|
'md5': 'd94a72d85d0a829766de4deb8daaf7df',
|
||||||
@@ -32,52 +88,14 @@ class Revision3IE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
# Show
|
# Show
|
||||||
'url': 'http://testtube.com/brainstuff',
|
'url': 'http://revision3.com/variant',
|
||||||
'info_dict': {
|
'only_matching': True,
|
||||||
'id': '251',
|
|
||||||
'title': 'BrainStuff',
|
|
||||||
'description': 'Whether the topic is popcorn or particle physics, you can count on the HowStuffWorks team to explore-and explain-the everyday science in the world around us on BrainStuff.',
|
|
||||||
},
|
|
||||||
'playlist_mincount': 93,
|
|
||||||
}, {
|
|
||||||
'url': 'https://testtube.com/dnews/5-weird-ways-plants-can-eat-animals?utm_source=FB&utm_medium=DNews&utm_campaign=DNewsSocial',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '58227',
|
|
||||||
'display_id': 'dnews/5-weird-ways-plants-can-eat-animals',
|
|
||||||
'duration': 275,
|
|
||||||
'ext': 'webm',
|
|
||||||
'title': '5 Weird Ways Plants Can Eat Animals',
|
|
||||||
'description': 'Why have some plants evolved to eat meat?',
|
|
||||||
'upload_date': '20150120',
|
|
||||||
'timestamp': 1421763300,
|
|
||||||
'uploader': 'DNews',
|
|
||||||
'uploader_id': 'dnews',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'http://testtube.com/tt-editors-picks/the-israel-palestine-conflict-explained-in-ten-min',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '71618',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'display_id': 'tt-editors-picks/the-israel-palestine-conflict-explained-in-ten-min',
|
|
||||||
'title': 'The Israel-Palestine Conflict Explained in Ten Minutes',
|
|
||||||
'description': 'If you\'d like to learn about the struggle between Israelis and Palestinians, this video is a great place to start',
|
|
||||||
'uploader': 'Editors\' Picks',
|
|
||||||
'uploader_id': 'tt-editors-picks',
|
|
||||||
'timestamp': 1453309200,
|
|
||||||
'upload_date': '20160120',
|
|
||||||
},
|
|
||||||
'add_ie': ['Youtube'],
|
|
||||||
}, {
|
}, {
|
||||||
# Tag
|
# Tag
|
||||||
'url': 'http://testtube.com/tech-news',
|
'url': 'http://revision3.com/vr',
|
||||||
'info_dict': {
|
'only_matching': True,
|
||||||
'id': '21018',
|
|
||||||
'title': 'tech news',
|
|
||||||
},
|
|
||||||
'playlist_mincount': 9,
|
|
||||||
}]
|
}]
|
||||||
_PAGE_DATA_TEMPLATE = 'http://www.%s/apiProxy/ddn/%s?domain=%s'
|
_PAGE_DATA_TEMPLATE = 'http://www.%s/apiProxy/ddn/%s?domain=%s'
|
||||||
_API_KEY = 'ba9c741bce1b9d8e3defcc22193f3651b8867e62'
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
domain, display_id = re.match(self._VALID_URL, url).groups()
|
domain, display_id = re.match(self._VALID_URL, url).groups()
|
||||||
@@ -119,33 +137,9 @@ class Revision3IE(InfoExtractor):
|
|||||||
})
|
})
|
||||||
return info
|
return info
|
||||||
|
|
||||||
video_data = self._download_json(
|
|
||||||
'http://revision3.com/api/getPlaylist.json?api_key=%s&codecs=h264,vp8,theora&video_id=%s' % (self._API_KEY, video_id),
|
|
||||||
video_id)['items'][0]
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
for vcodec, media in video_data['media'].items():
|
|
||||||
for quality_id, quality in media.items():
|
|
||||||
if quality_id == 'hls':
|
|
||||||
formats.extend(self._extract_m3u8_formats(
|
|
||||||
quality['url'], video_id, 'mp4',
|
|
||||||
'm3u8_native', m3u8_id='hls', fatal=False))
|
|
||||||
else:
|
|
||||||
formats.append({
|
|
||||||
'url': quality['url'],
|
|
||||||
'format_id': '%s-%s' % (vcodec, quality_id),
|
|
||||||
'tbr': int_or_none(quality.get('bitrate')),
|
|
||||||
'vcodec': vcodec,
|
|
||||||
})
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
info.update({
|
info.update({
|
||||||
'title': unescapeHTML(video_data['title']),
|
'_type': 'url_transparent',
|
||||||
'description': unescapeHTML(video_data.get('summary')),
|
'url': 'revision3:%s' % video_id,
|
||||||
'uploader': video_data.get('show', {}).get('name'),
|
|
||||||
'uploader_id': video_data.get('show', {}).get('slug'),
|
|
||||||
'duration': int_or_none(video_data.get('duration')),
|
|
||||||
'formats': formats,
|
|
||||||
})
|
})
|
||||||
return info
|
return info
|
||||||
else:
|
else:
|
||||||
|
69
youtube_dl/extractor/rockstargames.py
Normal file
69
youtube_dl/extractor/rockstargames.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
parse_iso8601,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RockstarGamesIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?rockstargames\.com/videos(?:/video/|#?/?\?.*\bvideo=)(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.rockstargames.com/videos/video/11544/',
|
||||||
|
'md5': '03b5caa6e357a4bd50e3143fc03e5733',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '11544',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Further Adventures in Finance and Felony Trailer',
|
||||||
|
'description': 'md5:6d31f55f30cb101b5476c4a379e324a3',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
|
'timestamp': 1464876000,
|
||||||
|
'upload_date': '20160602',
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.rockstargames.com/videos#/?video=48',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
video = self._download_json(
|
||||||
|
'https://www.rockstargames.com/videoplayer/videos/get-video.json',
|
||||||
|
video_id, query={
|
||||||
|
'id': video_id,
|
||||||
|
'locale': 'en_us',
|
||||||
|
})['video']
|
||||||
|
|
||||||
|
title = video['title']
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for video in video['files_processed']['video/mp4']:
|
||||||
|
if not video.get('src'):
|
||||||
|
continue
|
||||||
|
resolution = video.get('resolution')
|
||||||
|
height = int_or_none(self._search_regex(
|
||||||
|
r'^(\d+)[pP]$', resolution or '', 'height', default=None))
|
||||||
|
formats.append({
|
||||||
|
'url': self._proto_relative_url(video['src']),
|
||||||
|
'format_id': resolution,
|
||||||
|
'height': height,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not formats:
|
||||||
|
youtube_id = video.get('youtube_id')
|
||||||
|
if youtube_id:
|
||||||
|
return self.url_result(youtube_id, 'Youtube')
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': video.get('description'),
|
||||||
|
'thumbnail': self._proto_relative_url(video.get('screencap')),
|
||||||
|
'timestamp': parse_iso8601(video.get('created')),
|
||||||
|
'formats': formats,
|
||||||
|
}
|
57
youtube_dl/extractor/seeker.py
Normal file
57
youtube_dl/extractor/seeker.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class SeekerIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?seeker\.com/(?P<display_id>.*)-(?P<article_id>\d+)\.html'
|
||||||
|
_TESTS = [{
|
||||||
|
# player.loadRevision3Item
|
||||||
|
'url': 'http://www.seeker.com/should-trump-be-required-to-release-his-tax-returns-1833805621.html',
|
||||||
|
'md5': '30c1dc4030cc715cf05b423d0947ac18',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '76243',
|
||||||
|
'ext': 'webm',
|
||||||
|
'title': 'Should Trump Be Required To Release His Tax Returns?',
|
||||||
|
'description': 'Donald Trump has been secretive about his "big," "beautiful" tax returns. So what can we learn if he decides to release them?',
|
||||||
|
'uploader': 'Seeker Daily',
|
||||||
|
'uploader_id': 'seekerdaily',
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.seeker.com/changes-expected-at-zoos-following-recent-gorilla-lion-shootings-1834116536.html',
|
||||||
|
'playlist': [
|
||||||
|
{
|
||||||
|
'md5': '83bcd157cab89ad7318dd7b8c9cf1306',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '67558',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'The Pros & Cons Of Zoos',
|
||||||
|
'description': 'Zoos are often depicted as a terrible place for animals to live, but is there any truth to this?',
|
||||||
|
'uploader': 'DNews',
|
||||||
|
'uploader_id': 'dnews',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1834116536',
|
||||||
|
'title': 'After Gorilla Killing, Changes Ahead for Zoos',
|
||||||
|
'description': 'The largest association of zoos and others are hoping to learn from recent incidents that led to the shooting deaths of a gorilla and two lions.',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id, article_id = re.match(self._VALID_URL, url).groups()
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
mobj = re.search(r"player\.loadRevision3Item\('([^']+)'\s*,\s*(\d+)\);", webpage)
|
||||||
|
if mobj:
|
||||||
|
playlist_type, playlist_id = mobj.groups()
|
||||||
|
return self.url_result(
|
||||||
|
'revision3:%s:%s' % (playlist_type, playlist_id), 'Revision3Embed', playlist_id)
|
||||||
|
else:
|
||||||
|
entries = [self.url_result('revision3:video_id:%s' % video_id, 'Revision3Embed', video_id) for video_id in re.findall(
|
||||||
|
r'<iframe[^>]+src=[\'"](?:https?:)?//api\.seekernetwork\.com/player/embed\?videoId=(\d+)', webpage)]
|
||||||
|
return self.playlist_result(
|
||||||
|
entries, article_id, self._og_search_title(webpage), self._og_search_description(webpage))
|
@@ -96,20 +96,18 @@ class SpankwireIE(InfoExtractor):
|
|||||||
formats = []
|
formats = []
|
||||||
for height, video_url in zip(heights, video_urls):
|
for height, video_url in zip(heights, video_urls):
|
||||||
path = compat_urllib_parse_urlparse(video_url).path
|
path = compat_urllib_parse_urlparse(video_url).path
|
||||||
_, quality = path.split('/')[4].split('_')[:2]
|
m = re.search(r'/(?P<height>\d+)[pP]_(?P<tbr>\d+)[kK]', path)
|
||||||
f = {
|
if m:
|
||||||
'url': video_url,
|
tbr = int(m.group('tbr'))
|
||||||
'height': height,
|
height = int(m.group('height'))
|
||||||
}
|
|
||||||
tbr = self._search_regex(r'^(\d+)[Kk]$', quality, 'tbr', default=None)
|
|
||||||
if tbr:
|
|
||||||
f.update({
|
|
||||||
'tbr': int(tbr),
|
|
||||||
'format_id': '%dp' % height,
|
|
||||||
})
|
|
||||||
else:
|
else:
|
||||||
f['format_id'] = quality
|
tbr = None
|
||||||
formats.append(f)
|
formats.append({
|
||||||
|
'url': video_url,
|
||||||
|
'format_id': '%dp' % height,
|
||||||
|
'height': height,
|
||||||
|
'tbr': tbr,
|
||||||
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
age_limit = self._rta_search(webpage)
|
age_limit = self._rta_search(webpage)
|
||||||
|
38
youtube_dl/extractor/sportschau.py
Normal file
38
youtube_dl/extractor/sportschau.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .wdr import WDRBaseIE
|
||||||
|
from ..utils import get_element_by_attribute
|
||||||
|
|
||||||
|
|
||||||
|
class SportschauIE(WDRBaseIE):
|
||||||
|
IE_NAME = 'Sportschau'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?sportschau\.de/(?:[^/]+/)+video-?(?P<id>[^/#?]+)\.html'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://www.sportschau.de/uefaeuro2016/videos/video-dfb-team-geht-gut-gelaunt-ins-spiel-gegen-polen-100.html',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'mdb-1140188',
|
||||||
|
'display_id': 'dfb-team-geht-gut-gelaunt-ins-spiel-gegen-polen-100',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'DFB-Team geht gut gelaunt ins Spiel gegen Polen',
|
||||||
|
'description': 'Vor dem zweiten Gruppenspiel gegen Polen herrscht gute Stimmung im deutschen Team. Insbesondere Bastian Schweinsteiger strotzt vor Optimismus nach seinem Tor gegen die Ukraine.',
|
||||||
|
'upload_date': '20160615',
|
||||||
|
},
|
||||||
|
'skip': 'Geo-restricted to Germany',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
title = get_element_by_attribute('class', 'headline', webpage)
|
||||||
|
description = self._html_search_meta('description', webpage, 'description')
|
||||||
|
|
||||||
|
info = self._extract_wdr_video(webpage, video_id)
|
||||||
|
|
||||||
|
info.update({
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
})
|
||||||
|
|
||||||
|
return info
|
@@ -5,6 +5,7 @@ import re
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
sanitized_Request,
|
sanitized_Request,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
)
|
)
|
||||||
@@ -14,7 +15,7 @@ class StreamcloudIE(InfoExtractor):
|
|||||||
IE_NAME = 'streamcloud.eu'
|
IE_NAME = 'streamcloud.eu'
|
||||||
_VALID_URL = r'https?://streamcloud\.eu/(?P<id>[a-zA-Z0-9_-]+)(?:/(?P<fname>[^#?]*)\.html)?'
|
_VALID_URL = r'https?://streamcloud\.eu/(?P<id>[a-zA-Z0-9_-]+)(?:/(?P<fname>[^#?]*)\.html)?'
|
||||||
|
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://streamcloud.eu/skp9j99s4bpz/youtube-dl_test_video_____________-BaW_jenozKc.mp4.html',
|
'url': 'http://streamcloud.eu/skp9j99s4bpz/youtube-dl_test_video_____________-BaW_jenozKc.mp4.html',
|
||||||
'md5': '6bea4c7fa5daaacc2a946b7146286686',
|
'md5': '6bea4c7fa5daaacc2a946b7146286686',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -23,7 +24,10 @@ class StreamcloudIE(InfoExtractor):
|
|||||||
'title': 'youtube-dl test video \'/\\ ä ↭',
|
'title': 'youtube-dl test video \'/\\ ä ↭',
|
||||||
},
|
},
|
||||||
'skip': 'Only available from the EU'
|
'skip': 'Only available from the EU'
|
||||||
}
|
}, {
|
||||||
|
'url': 'http://streamcloud.eu/ua8cmfh1nbe6/NSHIP-148--KUC-NG--H264-.mp4.html',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
@@ -31,6 +35,10 @@ class StreamcloudIE(InfoExtractor):
|
|||||||
|
|
||||||
orig_webpage = self._download_webpage(url, video_id)
|
orig_webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
if '>File Not Found<' in orig_webpage:
|
||||||
|
raise ExtractorError(
|
||||||
|
'Video %s does not exist' % video_id, expected=True)
|
||||||
|
|
||||||
fields = re.findall(r'''(?x)<input\s+
|
fields = re.findall(r'''(?x)<input\s+
|
||||||
type="(?:hidden|submit)"\s+
|
type="(?:hidden|submit)"\s+
|
||||||
name="([^"]+)"\s+
|
name="([^"]+)"\s+
|
||||||
|
@@ -11,6 +11,7 @@ class TeachingChannelIE(InfoExtractor):
|
|||||||
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'https://www.teachingchannel.org/videos/teacher-teaming-evolution',
|
'url': 'https://www.teachingchannel.org/videos/teacher-teaming-evolution',
|
||||||
|
'md5': '3d6361864d7cac20b57c8784da17166f',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'F3bnlzbToeI6pLEfRyrlfooIILUjz4nM',
|
'id': 'F3bnlzbToeI6pLEfRyrlfooIILUjz4nM',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -19,9 +20,9 @@ class TeachingChannelIE(InfoExtractor):
|
|||||||
'duration': 422.255,
|
'duration': 422.255,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
'add_ie': ['Ooyala'],
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
55
youtube_dl/extractor/telewebion.py
Normal file
55
youtube_dl/extractor/telewebion.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class TelewebionIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://www\.telewebion\.com/#!/episode/(?P<id>\d+)'
|
||||||
|
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://www.telewebion.com/#!/episode/1263668/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1263668',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'قرعه\u200cکشی لیگ قهرمانان اروپا',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg',
|
||||||
|
'view_count': int,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
secure_token = self._download_webpage(
|
||||||
|
'http://m.s2.telewebion.com/op/op?action=getSecurityToken', video_id)
|
||||||
|
episode_details = self._download_json(
|
||||||
|
'http://m.s2.telewebion.com/op/op', video_id,
|
||||||
|
query={'action': 'getEpisodeDetails', 'episode_id': video_id})
|
||||||
|
|
||||||
|
m3u8_url = 'http://m.s1.telewebion.com/smil/%s.m3u8?filepath=%s&m3u8=1&secure_token=%s' % (
|
||||||
|
video_id, episode_details['file_path'], secure_token)
|
||||||
|
formats = self._extract_m3u8_formats(
|
||||||
|
m3u8_url, video_id, ext='mp4', m3u8_id='hls')
|
||||||
|
|
||||||
|
picture_paths = [
|
||||||
|
episode_details.get('picture_path'),
|
||||||
|
episode_details.get('large_picture_path'),
|
||||||
|
]
|
||||||
|
|
||||||
|
thumbnails = [{
|
||||||
|
'url': picture_path,
|
||||||
|
'preference': idx,
|
||||||
|
} for idx, picture_path in enumerate(picture_paths) if picture_path is not None]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': episode_details['title'],
|
||||||
|
'formats': formats,
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
'view_count': episode_details.get('view_count'),
|
||||||
|
}
|
@@ -6,7 +6,7 @@ from .common import InfoExtractor
|
|||||||
|
|
||||||
class TF1IE(InfoExtractor):
|
class TF1IE(InfoExtractor):
|
||||||
"""TF1 uses the wat.tv player."""
|
"""TF1 uses the wat.tv player."""
|
||||||
_VALID_URL = r'https?://(?:(?:videos|www|lci)\.tf1|www\.tfou)\.fr/(?:[^/]+/)*(?P<id>.+?)\.html'
|
_VALID_URL = r'https?://(?:(?:videos|www|lci)\.tf1|(?:www\.)?(?:tfou|ushuaiatv|histoire|tvbreizh))\.fr/(?:[^/]+/)*(?P<id>[^/?#.]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://videos.tf1.fr/auto-moto/citroen-grand-c4-picasso-2013-presentation-officielle-8062060.html',
|
'url': 'http://videos.tf1.fr/auto-moto/citroen-grand-c4-picasso-2013-presentation-officielle-8062060.html',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -48,6 +48,6 @@ class TF1IE(InfoExtractor):
|
|||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
wat_id = self._html_search_regex(
|
wat_id = self._html_search_regex(
|
||||||
r'(["\'])(?:https?:)?//www\.wat\.tv/embedframe/.*?(?P<id>\d{8})(?:#.*?)?\1',
|
r'(["\'])(?:https?:)?//www\.wat\.tv/embedframe/.*?(?P<id>\d{8}).*?\1',
|
||||||
webpage, 'wat id', group='id')
|
webpage, 'wat id', group='id')
|
||||||
return self.url_result('wat:%s' % wat_id, 'Wat')
|
return self.url_result('wat:%s' % wat_id, 'Wat')
|
||||||
|
@@ -14,11 +14,13 @@ from ..compat import (
|
|||||||
compat_urllib_parse_urlparse,
|
compat_urllib_parse_urlparse,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
determine_ext,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
sanitized_Request,
|
sanitized_Request,
|
||||||
unsmuggle_url,
|
unsmuggle_url,
|
||||||
|
update_url_query,
|
||||||
xpath_with_ns,
|
xpath_with_ns,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
find_xpath_attr,
|
find_xpath_attr,
|
||||||
@@ -48,6 +50,12 @@ class ThePlatformBaseIE(OnceIE):
|
|||||||
if OnceIE.suitable(_format['url']):
|
if OnceIE.suitable(_format['url']):
|
||||||
formats.extend(self._extract_once_formats(_format['url']))
|
formats.extend(self._extract_once_formats(_format['url']))
|
||||||
else:
|
else:
|
||||||
|
media_url = _format['url']
|
||||||
|
if determine_ext(media_url) == 'm3u8':
|
||||||
|
hdnea2 = self._get_cookies(media_url).get('hdnea2')
|
||||||
|
if hdnea2:
|
||||||
|
_format['url'] = update_url_query(media_url, {'hdnea3': hdnea2.value})
|
||||||
|
|
||||||
formats.append(_format)
|
formats.append(_format)
|
||||||
|
|
||||||
subtitles = self._parse_smil_subtitles(meta, default_ns)
|
subtitles = self._parse_smil_subtitles(meta, default_ns)
|
||||||
@@ -151,6 +159,22 @@ class ThePlatformIE(ThePlatformBaseIE):
|
|||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _extract_urls(cls, webpage):
|
||||||
|
m = re.search(
|
||||||
|
r'''(?x)
|
||||||
|
<meta\s+
|
||||||
|
property=(["'])(?:og:video(?::(?:secure_)?url)?|twitter:player)\1\s+
|
||||||
|
content=(["'])(?P<url>https?://player\.theplatform\.com/p/.+?)\2
|
||||||
|
''', webpage)
|
||||||
|
if m:
|
||||||
|
return [m.group('url')]
|
||||||
|
|
||||||
|
matches = re.findall(
|
||||||
|
r'<(?:iframe|script)[^>]+src=(["\'])((?:https?:)?//player\.theplatform\.com/p/.+?)\1', webpage)
|
||||||
|
if matches:
|
||||||
|
return list(zip(*matches))[1]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _sign_url(url, sig_key, sig_secret, life=600, include_qs=False):
|
def _sign_url(url, sig_key, sig_secret, life=600, include_qs=False):
|
||||||
flags = '10' if include_qs else '00'
|
flags = '10' if include_qs else '00'
|
||||||
@@ -253,9 +277,9 @@ class ThePlatformIE(ThePlatformBaseIE):
|
|||||||
|
|
||||||
|
|
||||||
class ThePlatformFeedIE(ThePlatformBaseIE):
|
class ThePlatformFeedIE(ThePlatformBaseIE):
|
||||||
_URL_TEMPLATE = '%s//feed.theplatform.com/f/%s/%s?form=json&byGuid=%s'
|
_URL_TEMPLATE = '%s//feed.theplatform.com/f/%s/%s?form=json&%s'
|
||||||
_VALID_URL = r'https?://feed\.theplatform\.com/f/(?P<provider_id>[^/]+)/(?P<feed_id>[^?/]+)\?(?:[^&]+&)*byGuid=(?P<id>[a-zA-Z0-9_]+)'
|
_VALID_URL = r'https?://feed\.theplatform\.com/f/(?P<provider_id>[^/]+)/(?P<feed_id>[^?/]+)\?(?:[^&]+&)*(?P<filter>by(?:Gui|I)d=(?P<id>[\w-]+))'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
# From http://player.theplatform.com/p/7wvmTC/MSNBCEmbeddedOffSite?guid=n_hardball_5biden_140207
|
# From http://player.theplatform.com/p/7wvmTC/MSNBCEmbeddedOffSite?guid=n_hardball_5biden_140207
|
||||||
'url': 'http://feed.theplatform.com/f/7wvmTC/msnbc_video-p-test?form=json&pretty=true&range=-40&byGuid=n_hardball_5biden_140207',
|
'url': 'http://feed.theplatform.com/f/7wvmTC/msnbc_video-p-test?form=json&pretty=true&range=-40&byGuid=n_hardball_5biden_140207',
|
||||||
'md5': '6e32495b5073ab414471b615c5ded394',
|
'md5': '6e32495b5073ab414471b615c5ded394',
|
||||||
@@ -271,32 +295,38 @@ class ThePlatformFeedIE(ThePlatformBaseIE):
|
|||||||
'categories': ['MSNBC/Issues/Democrats', 'MSNBC/Issues/Elections/Election 2016'],
|
'categories': ['MSNBC/Issues/Democrats', 'MSNBC/Issues/Elections/Election 2016'],
|
||||||
'uploader': 'NBCU-NEWS',
|
'uploader': 'NBCU-NEWS',
|
||||||
},
|
},
|
||||||
}
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _extract_feed_info(self, provider_id, feed_id, filter_query, video_id, custom_fields=None, asset_types_query={}):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
real_url = self._URL_TEMPLATE % (self.http_scheme(), provider_id, feed_id, filter_query)
|
||||||
|
entry = self._download_json(real_url, video_id)['entries'][0]
|
||||||
video_id = mobj.group('id')
|
|
||||||
provider_id = mobj.group('provider_id')
|
|
||||||
feed_id = mobj.group('feed_id')
|
|
||||||
|
|
||||||
real_url = self._URL_TEMPLATE % (self.http_scheme(), provider_id, feed_id, video_id)
|
|
||||||
feed = self._download_json(real_url, video_id)
|
|
||||||
entry = feed['entries'][0]
|
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
first_video_id = None
|
first_video_id = None
|
||||||
duration = None
|
duration = None
|
||||||
|
asset_types = []
|
||||||
for item in entry['media$content']:
|
for item in entry['media$content']:
|
||||||
smil_url = item['plfile$url'] + '&mbr=true'
|
smil_url = item['plfile$url']
|
||||||
cur_video_id = ThePlatformIE._match_id(smil_url)
|
cur_video_id = ThePlatformIE._match_id(smil_url)
|
||||||
if first_video_id is None:
|
if first_video_id is None:
|
||||||
first_video_id = cur_video_id
|
first_video_id = cur_video_id
|
||||||
duration = float_or_none(item.get('plfile$duration'))
|
duration = float_or_none(item.get('plfile$duration'))
|
||||||
cur_formats, cur_subtitles = self._extract_theplatform_smil(smil_url, video_id, 'Downloading SMIL data for %s' % cur_video_id)
|
for asset_type in item['plfile$assetTypes']:
|
||||||
formats.extend(cur_formats)
|
if asset_type in asset_types:
|
||||||
subtitles = self._merge_subtitles(subtitles, cur_subtitles)
|
continue
|
||||||
|
asset_types.append(asset_type)
|
||||||
|
query = {
|
||||||
|
'mbr': 'true',
|
||||||
|
'formats': item['plfile$format'],
|
||||||
|
'assetTypes': asset_type,
|
||||||
|
}
|
||||||
|
if asset_type in asset_types_query:
|
||||||
|
query.update(asset_types_query[asset_type])
|
||||||
|
cur_formats, cur_subtitles = self._extract_theplatform_smil(update_url_query(
|
||||||
|
smil_url, query), video_id, 'Downloading SMIL data for %s' % asset_type)
|
||||||
|
formats.extend(cur_formats)
|
||||||
|
subtitles = self._merge_subtitles(subtitles, cur_subtitles)
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
@@ -320,5 +350,17 @@ class ThePlatformFeedIE(ThePlatformBaseIE):
|
|||||||
'timestamp': timestamp,
|
'timestamp': timestamp,
|
||||||
'categories': categories,
|
'categories': categories,
|
||||||
})
|
})
|
||||||
|
if custom_fields:
|
||||||
|
ret.update(custom_fields(entry))
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
|
||||||
|
video_id = mobj.group('id')
|
||||||
|
provider_id = mobj.group('provider_id')
|
||||||
|
feed_id = mobj.group('feed_id')
|
||||||
|
filter_query = mobj.group('filter')
|
||||||
|
|
||||||
|
return self._extract_feed_info(provider_id, feed_id, filter_query, video_id)
|
||||||
|
@@ -12,7 +12,7 @@ class TheSixtyOneIE(InfoExtractor):
|
|||||||
s|
|
s|
|
||||||
song/comments/list|
|
song/comments/list|
|
||||||
song
|
song
|
||||||
)/(?P<id>[A-Za-z0-9]+)/?$'''
|
)/(?:[^/]+/)?(?P<id>[A-Za-z0-9]+)/?$'''
|
||||||
_SONG_URL_TEMPLATE = 'http://thesixtyone.com/s/{0:}'
|
_SONG_URL_TEMPLATE = 'http://thesixtyone.com/s/{0:}'
|
||||||
_SONG_FILE_URL_TEMPLATE = 'http://{audio_server:}/thesixtyone_production/audio/{0:}_stream'
|
_SONG_FILE_URL_TEMPLATE = 'http://{audio_server:}/thesixtyone_production/audio/{0:}_stream'
|
||||||
_THUMBNAIL_URL_TEMPLATE = '{photo_base_url:}_desktop'
|
_THUMBNAIL_URL_TEMPLATE = '{photo_base_url:}_desktop'
|
||||||
@@ -45,6 +45,10 @@ class TheSixtyOneIE(InfoExtractor):
|
|||||||
'url': 'http://www.thesixtyone.com/song/SrE3zD7s1jt/',
|
'url': 'http://www.thesixtyone.com/song/SrE3zD7s1jt/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://www.thesixtyone.com/maryatmidnight/song/StrawberriesandCream/yvWtLp0c4GQ/',
|
||||||
|
'only_matching': True,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
_DECODE_MAP = {
|
_DECODE_MAP = {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
@@ -6,20 +6,13 @@ import re
|
|||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
class TvpIE(InfoExtractor):
|
class TVPIE(InfoExtractor):
|
||||||
IE_NAME = 'tvp.pl'
|
IE_NAME = 'tvp'
|
||||||
_VALID_URL = r'https?://(?:vod|www)\.tvp\.pl/.*/(?P<id>\d+)$'
|
IE_DESC = 'Telewizja Polska'
|
||||||
|
_VALID_URL = r'https?://[^/]+\.tvp\.(?:pl|info)/(?:(?!\d+/)[^/]+/)*(?P<id>\d+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://vod.tvp.pl/filmy-fabularne/filmy-za-darmo/ogniem-i-mieczem/wideo/odc-2/4278035',
|
'url': 'http://vod.tvp.pl/194536/i-seria-odc-13',
|
||||||
'md5': 'cdd98303338b8a7f7abab5cd14092bf2',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '4278035',
|
|
||||||
'ext': 'wmv',
|
|
||||||
'title': 'Ogniem i mieczem, odc. 2',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'http://vod.tvp.pl/seriale/obyczajowe/czas-honoru/sezon-1-1-13/i-seria-odc-13/194536',
|
|
||||||
'md5': '8aa518c15e5cc32dfe8db400dc921fbb',
|
'md5': '8aa518c15e5cc32dfe8db400dc921fbb',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '194536',
|
'id': '194536',
|
||||||
@@ -36,12 +29,22 @@ class TvpIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://vod.tvp.pl/seriale/obyczajowe/na-sygnale/sezon-2-27-/odc-39/17834272',
|
'url': 'http://vod.tvp.pl/seriale/obyczajowe/na-sygnale/sezon-2-27-/odc-39/17834272',
|
||||||
'md5': 'c3b15ed1af288131115ff17a17c19dda',
|
'only_matching': True,
|
||||||
'info_dict': {
|
}, {
|
||||||
'id': '17834272',
|
'url': 'http://wiadomosci.tvp.pl/25169746/24052016-1200',
|
||||||
'ext': 'mp4',
|
'only_matching': True,
|
||||||
'title': 'Na sygnale, odc. 39',
|
}, {
|
||||||
},
|
'url': 'http://krakow.tvp.pl/25511623/25lecie-mck-wyjatkowe-miejsce-na-mapie-krakowa',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://teleexpress.tvp.pl/25522307/wierni-wzieli-udzial-w-procesjach',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://sport.tvp.pl/25522165/krychowiak-uspokaja-w-sprawie-kontuzji-dwa-tygodnie-to-maksimum',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.tvp.info/25511919/trwa-rewolucja-wladza-zdecydowala-sie-na-pogwalcenie-konstytucji',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -92,8 +95,8 @@ class TvpIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TvpSeriesIE(InfoExtractor):
|
class TVPSeriesIE(InfoExtractor):
|
||||||
IE_NAME = 'tvp.pl:Series'
|
IE_NAME = 'tvp:series'
|
||||||
_VALID_URL = r'https?://vod\.tvp\.pl/(?:[^/]+/){2}(?P<id>[^/]+)/?$'
|
_VALID_URL = r'https?://vod\.tvp\.pl/(?:[^/]+/){2}(?P<id>[^/]+)/?$'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
@@ -127,7 +130,7 @@ class TvpSeriesIE(InfoExtractor):
|
|||||||
videos_paths = re.findall(
|
videos_paths = re.findall(
|
||||||
'(?s)class="shortTitle">.*?href="(/[^"]+)', playlist)
|
'(?s)class="shortTitle">.*?href="(/[^"]+)', playlist)
|
||||||
entries = [
|
entries = [
|
||||||
self.url_result('http://vod.tvp.pl%s' % v_path, ie=TvpIE.ie_key())
|
self.url_result('http://vod.tvp.pl%s' % v_path, ie=TVPIE.ie_key())
|
||||||
for v_path in videos_paths]
|
for v_path in videos_paths]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -16,6 +16,7 @@ from ..compat import (
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
js_to_json,
|
||||||
orderedSet,
|
orderedSet,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
@@ -260,7 +261,7 @@ class TwitchVodIE(TwitchItemBaseIE):
|
|||||||
'nauth': access_token['token'],
|
'nauth': access_token['token'],
|
||||||
'nauthsig': access_token['sig'],
|
'nauthsig': access_token['sig'],
|
||||||
})),
|
})),
|
||||||
item_id, 'mp4')
|
item_id, 'mp4', entry_protocol='m3u8_native')
|
||||||
|
|
||||||
self._prefer_source(formats)
|
self._prefer_source(formats)
|
||||||
info['formats'] = formats
|
info['formats'] = formats
|
||||||
@@ -454,3 +455,45 @@ class TwitchStreamIE(TwitchBaseIE):
|
|||||||
'formats': formats,
|
'formats': formats,
|
||||||
'is_live': True,
|
'is_live': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TwitchClipsIE(InfoExtractor):
|
||||||
|
IE_NAME = 'twitch:clips'
|
||||||
|
_VALID_URL = r'https?://clips\.twitch\.tv/(?:[^/]+/)*(?P<id>[^/?#&]+)'
|
||||||
|
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://clips.twitch.tv/ea/AggressiveCobraPoooound',
|
||||||
|
'md5': '761769e1eafce0ffebfb4089cb3847cd',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'AggressiveCobraPoooound',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'EA Play 2016 Live from the Novo Theatre',
|
||||||
|
'thumbnail': 're:^https?://.*\.jpg',
|
||||||
|
'creator': 'EA',
|
||||||
|
'uploader': 'stereotype_',
|
||||||
|
'uploader_id': 'stereotype_',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
clip = self._parse_json(
|
||||||
|
self._search_regex(
|
||||||
|
r'(?s)clipInfo\s*=\s*({.+?});', webpage, 'clip info'),
|
||||||
|
video_id, transform_source=js_to_json)
|
||||||
|
|
||||||
|
video_url = clip['clip_video_url']
|
||||||
|
title = clip['channel_title']
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'url': video_url,
|
||||||
|
'title': title,
|
||||||
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
|
'creator': clip.get('broadcaster_display_name') or clip.get('broadcaster_login'),
|
||||||
|
'uploader': clip.get('curator_login'),
|
||||||
|
'uploader_id': clip.get('curator_display_name'),
|
||||||
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import re
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
determine_ext,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
xpath_text,
|
xpath_text,
|
||||||
remove_end,
|
remove_end,
|
||||||
@@ -52,7 +53,7 @@ class TwitterCardIE(TwitterBaseIE):
|
|||||||
'id': 'dq4Oj5quskI',
|
'id': 'dq4Oj5quskI',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Ubuntu 11.10 Overview',
|
'title': 'Ubuntu 11.10 Overview',
|
||||||
'description': 'Take a quick peek at what\'s new and improved in Ubuntu 11.10.\n\nOnce installed take a look at 10 Things to Do After Installing: http://www.omgubuntu.co.uk/2011/10/10-things-to-do-after-installing-ubuntu-11-10/',
|
'description': 'Take a quick peek at what\'s new and improved in Ubuntu 11.10.\n\nOnce installed take a look at 10 Things to Do After Installing: http://www.omgubuntu.co.uk/2011/10/10...',
|
||||||
'upload_date': '20111013',
|
'upload_date': '20111013',
|
||||||
'uploader': 'OMG! Ubuntu!',
|
'uploader': 'OMG! Ubuntu!',
|
||||||
'uploader_id': 'omgubuntu',
|
'uploader_id': 'omgubuntu',
|
||||||
@@ -116,13 +117,16 @@ class TwitterCardIE(TwitterBaseIE):
|
|||||||
video_url = config.get('video_url') or config.get('playlist', [{}])[0].get('source')
|
video_url = config.get('video_url') or config.get('playlist', [{}])[0].get('source')
|
||||||
|
|
||||||
if video_url:
|
if video_url:
|
||||||
f = {
|
if determine_ext(video_url) == 'm3u8':
|
||||||
'url': video_url,
|
formats.extend(self._extract_m3u8_formats(video_url, video_id, ext='mp4', m3u8_id='hls'))
|
||||||
}
|
else:
|
||||||
|
f = {
|
||||||
|
'url': video_url,
|
||||||
|
}
|
||||||
|
|
||||||
_search_dimensions_in_video_url(f, video_url)
|
_search_dimensions_in_video_url(f, video_url)
|
||||||
|
|
||||||
formats.append(f)
|
formats.append(f)
|
||||||
|
|
||||||
vmap_url = config.get('vmapUrl') or config.get('vmap_url')
|
vmap_url = config.get('vmapUrl') or config.get('vmap_url')
|
||||||
if vmap_url:
|
if vmap_url:
|
||||||
@@ -207,6 +211,7 @@ class TwitterIE(InfoExtractor):
|
|||||||
'uploader_id': 'giphz',
|
'uploader_id': 'giphz',
|
||||||
},
|
},
|
||||||
'expected_warnings': ['height', 'width'],
|
'expected_warnings': ['height', 'width'],
|
||||||
|
'skip': 'Account suspended',
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://twitter.com/starwars/status/665052190608723968',
|
'url': 'https://twitter.com/starwars/status/665052190608723968',
|
||||||
'md5': '39b7199856dee6cd4432e72c74bc69d4',
|
'md5': '39b7199856dee6cd4432e72c74bc69d4',
|
||||||
@@ -239,10 +244,10 @@ class TwitterIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '700207533655363584',
|
'id': '700207533655363584',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'jay - BEAT PROD: @suhmeduh #Damndaniel',
|
'title': 'Donte The Dumbass - BEAT PROD: @suhmeduh #Damndaniel',
|
||||||
'description': 'jay on Twitter: "BEAT PROD: @suhmeduh https://t.co/HBrQ4AfpvZ #Damndaniel https://t.co/byBooq2ejZ"',
|
'description': 'Donte The Dumbass on Twitter: "BEAT PROD: @suhmeduh https://t.co/HBrQ4AfpvZ #Damndaniel https://t.co/byBooq2ejZ"',
|
||||||
'thumbnail': 're:^https?://.*\.jpg',
|
'thumbnail': 're:^https?://.*\.jpg',
|
||||||
'uploader': 'jay',
|
'uploader': 'Donte The Dumbass',
|
||||||
'uploader_id': 'jaydingeer',
|
'uploader_id': 'jaydingeer',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
@@ -262,7 +267,6 @@ class TwitterIE(InfoExtractor):
|
|||||||
'add_ie': ['Vine'],
|
'add_ie': ['Vine'],
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://twitter.com/captainamerica/status/719944021058060289',
|
'url': 'https://twitter.com/captainamerica/status/719944021058060289',
|
||||||
# md5 constantly changes
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '719944021058060289',
|
'id': '719944021058060289',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -271,6 +275,9 @@ class TwitterIE(InfoExtractor):
|
|||||||
'uploader_id': 'captainamerica',
|
'uploader_id': 'captainamerica',
|
||||||
'uploader': 'Captain America',
|
'uploader': 'Captain America',
|
||||||
},
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True, # requires ffmpeg
|
||||||
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -278,7 +285,11 @@ class TwitterIE(InfoExtractor):
|
|||||||
user_id = mobj.group('user_id')
|
user_id = mobj.group('user_id')
|
||||||
twid = mobj.group('id')
|
twid = mobj.group('id')
|
||||||
|
|
||||||
webpage = self._download_webpage(self._TEMPLATE_URL % (user_id, twid), twid)
|
webpage, urlh = self._download_webpage_handle(
|
||||||
|
self._TEMPLATE_URL % (user_id, twid), twid)
|
||||||
|
|
||||||
|
if 'twitter.com/account/suspended' in urlh.geturl():
|
||||||
|
raise ExtractorError('Account suspended by Twitter.', expected=True)
|
||||||
|
|
||||||
username = remove_end(self._og_search_title(webpage), ' on Twitter')
|
username = remove_end(self._og_search_title(webpage), ' on Twitter')
|
||||||
|
|
||||||
|
@@ -142,7 +142,9 @@ class UdemyIE(InfoExtractor):
|
|||||||
self._LOGIN_URL, None, 'Downloading login popup')
|
self._LOGIN_URL, None, 'Downloading login popup')
|
||||||
|
|
||||||
def is_logged(webpage):
|
def is_logged(webpage):
|
||||||
return any(p in webpage for p in ['href="https://www.udemy.com/user/logout/', '>Logout<'])
|
return any(re.search(p, webpage) for p in (
|
||||||
|
r'href=["\'](?:https://www\.udemy\.com)?/user/logout/',
|
||||||
|
r'>Logout<'))
|
||||||
|
|
||||||
# already logged in
|
# already logged in
|
||||||
if is_logged(login_popup):
|
if is_logged(login_popup):
|
||||||
|
@@ -2,10 +2,13 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
determine_ext,
|
||||||
|
int_or_none,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
ExtractorError,
|
|
||||||
)
|
)
|
||||||
from ..compat import compat_urlparse
|
from ..compat import compat_urlparse
|
||||||
|
|
||||||
@@ -16,13 +19,16 @@ class UDNEmbedIE(InfoExtractor):
|
|||||||
_VALID_URL = r'https?:' + _PROTOCOL_RELATIVE_VALID_URL
|
_VALID_URL = r'https?:' + _PROTOCOL_RELATIVE_VALID_URL
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://video.udn.com/embed/news/300040',
|
'url': 'http://video.udn.com/embed/news/300040',
|
||||||
'md5': 'de06b4c90b042c128395a88f0384817e',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '300040',
|
'id': '300040',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '生物老師男變女 全校挺"做自己"',
|
'title': '生物老師男變女 全校挺"做自己"',
|
||||||
'thumbnail': 're:^https?://.*\.jpg$',
|
'thumbnail': 're:^https?://.*\.jpg$',
|
||||||
}
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://video.udn.com/embed/news/300040',
|
'url': 'https://video.udn.com/embed/news/300040',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@@ -38,39 +44,53 @@ class UDNEmbedIE(InfoExtractor):
|
|||||||
page = self._download_webpage(url, video_id)
|
page = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
options = json.loads(js_to_json(self._html_search_regex(
|
options = json.loads(js_to_json(self._html_search_regex(
|
||||||
r'var options\s*=\s*([^;]+);', page, 'video urls dictionary')))
|
r'var\s+options\s*=\s*([^;]+);', page, 'video urls dictionary')))
|
||||||
|
|
||||||
video_urls = options['video']
|
video_urls = options['video']
|
||||||
|
|
||||||
if video_urls.get('youtube'):
|
if video_urls.get('youtube'):
|
||||||
return self.url_result(video_urls.get('youtube'), 'Youtube')
|
return self.url_result(video_urls.get('youtube'), 'Youtube')
|
||||||
|
|
||||||
try:
|
formats = []
|
||||||
del video_urls['youtube']
|
for video_type, api_url in video_urls.items():
|
||||||
except KeyError:
|
if not api_url:
|
||||||
pass
|
continue
|
||||||
|
|
||||||
formats = [{
|
video_url = self._download_webpage(
|
||||||
'url': self._download_webpage(
|
|
||||||
compat_urlparse.urljoin(url, api_url), video_id,
|
compat_urlparse.urljoin(url, api_url), video_id,
|
||||||
'retrieve url for %s video' % video_type),
|
note='retrieve url for %s video' % video_type)
|
||||||
'format_id': video_type,
|
|
||||||
'preference': 0 if video_type == 'mp4' else -1,
|
|
||||||
} for video_type, api_url in video_urls.items() if api_url]
|
|
||||||
|
|
||||||
if not formats:
|
ext = determine_ext(video_url)
|
||||||
raise ExtractorError('No videos found', expected=True)
|
if ext == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
video_url, video_id, ext='mp4', m3u8_id='hls'))
|
||||||
|
elif ext == 'f4m':
|
||||||
|
formats.extend(self._extract_f4m_formats(
|
||||||
|
video_url, video_id, f4m_id='hds'))
|
||||||
|
else:
|
||||||
|
mobj = re.search(r'_(?P<height>\d+)p_(?P<tbr>\d+).mp4', video_url)
|
||||||
|
a_format = {
|
||||||
|
'url': video_url,
|
||||||
|
# video_type may be 'mp4', which confuses YoutubeDL
|
||||||
|
'format_id': 'http-' + video_type,
|
||||||
|
}
|
||||||
|
if mobj:
|
||||||
|
a_format.update({
|
||||||
|
'height': int_or_none(mobj.group('height')),
|
||||||
|
'tbr': int_or_none(mobj.group('tbr')),
|
||||||
|
})
|
||||||
|
formats.append(a_format)
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
thumbnail = None
|
thumbnails = [{
|
||||||
|
'url': img_url,
|
||||||
if options.get('gallery') and len(options['gallery']):
|
'id': img_type,
|
||||||
thumbnail = options['gallery'][0].get('original')
|
} for img_type, img_url in options.get('gallery', [{}])[0].items() if img_url]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'title': options['title'],
|
'title': options['title'],
|
||||||
'thumbnail': thumbnail
|
'thumbnails': thumbnails,
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user