Merge branch 'master' of https://github.com/nficano/pytube into video-metadata-#195
Browse files- Pipfile +1 -7
- Pipfile.lock +91 -153
- README.md +1 -1
- pytube/__init__.py +2 -6
- pytube/__main__.py +8 -2
- pytube/compat.py +18 -0
- pytube/contrib/playlist.py +45 -4
- pytube/exceptions.py +4 -0
- pytube/itags.py +2 -2
- pytube/query.py +2 -2
- pytube/streams.py +38 -2
- setup.cfg +3 -4
- setup.py +1 -1
- tests/test_playlist.py +51 -26
- tests/test_query.py +1 -0
Pipfile
CHANGED
@@ -1,18 +1,11 @@
|
|
1 |
[[source]]
|
2 |
-
|
3 |
url = "https://pypi.python.org/simple"
|
4 |
verify_ssl = true
|
5 |
name = "pypi"
|
6 |
|
7 |
-
|
8 |
[packages]
|
9 |
|
10 |
-
"flake8" = "*"
|
11 |
-
"enum34" = "*"
|
12 |
-
|
13 |
-
|
14 |
[dev-packages]
|
15 |
-
|
16 |
bumpversion = "*"
|
17 |
coveralls = "*"
|
18 |
"flake8" = "*"
|
@@ -26,3 +19,4 @@ sphinx-rtd-theme = "*"
|
|
26 |
configparser = "*"
|
27 |
"urllib3" = "*"
|
28 |
pyopenssl = "*"
|
|
|
|
1 |
[[source]]
|
|
|
2 |
url = "https://pypi.python.org/simple"
|
3 |
verify_ssl = true
|
4 |
name = "pypi"
|
5 |
|
|
|
6 |
[packages]
|
7 |
|
|
|
|
|
|
|
|
|
8 |
[dev-packages]
|
|
|
9 |
bumpversion = "*"
|
10 |
coveralls = "*"
|
11 |
"flake8" = "*"
|
|
|
19 |
configparser = "*"
|
20 |
"urllib3" = "*"
|
21 |
pyopenssl = "*"
|
22 |
+
"enum34" = "*"
|
Pipfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
{
|
2 |
"_meta": {
|
3 |
"hash": {
|
4 |
-
"sha256": "
|
5 |
},
|
6 |
"pipfile-spec": 6,
|
7 |
"requires": {},
|
@@ -13,53 +13,7 @@
|
|
13 |
}
|
14 |
]
|
15 |
},
|
16 |
-
"default": {
|
17 |
-
"configparser": {
|
18 |
-
"hashes": [
|
19 |
-
"sha256:5308b47021bc2340965c371f0f058cc6971a04502638d4244225c49d80db273a"
|
20 |
-
],
|
21 |
-
"markers": "python_version < '3.2'",
|
22 |
-
"version": "==3.5.0"
|
23 |
-
},
|
24 |
-
"enum34": {
|
25 |
-
"hashes": [
|
26 |
-
"sha256:2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850",
|
27 |
-
"sha256:644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a",
|
28 |
-
"sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79",
|
29 |
-
"sha256:8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1"
|
30 |
-
],
|
31 |
-
"markers": "python_version < '3.4'",
|
32 |
-
"version": "==1.1.6"
|
33 |
-
},
|
34 |
-
"flake8": {
|
35 |
-
"hashes": [
|
36 |
-
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
|
37 |
-
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
|
38 |
-
],
|
39 |
-
"version": "==3.5.0"
|
40 |
-
},
|
41 |
-
"mccabe": {
|
42 |
-
"hashes": [
|
43 |
-
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
44 |
-
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
45 |
-
],
|
46 |
-
"version": "==0.6.1"
|
47 |
-
},
|
48 |
-
"pycodestyle": {
|
49 |
-
"hashes": [
|
50 |
-
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
|
51 |
-
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
|
52 |
-
],
|
53 |
-
"version": "==2.3.1"
|
54 |
-
},
|
55 |
-
"pyflakes": {
|
56 |
-
"hashes": [
|
57 |
-
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
|
58 |
-
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
|
59 |
-
],
|
60 |
-
"version": "==1.6.0"
|
61 |
-
}
|
62 |
-
},
|
63 |
"develop": {
|
64 |
"alabaster": {
|
65 |
"hashes": [
|
@@ -77,17 +31,17 @@
|
|
77 |
},
|
78 |
"aspy.yaml": {
|
79 |
"hashes": [
|
80 |
-
"sha256:
|
81 |
-
"sha256:
|
82 |
],
|
83 |
-
"version": "==1.
|
84 |
},
|
85 |
"attrs": {
|
86 |
"hashes": [
|
87 |
-
"sha256:
|
88 |
-
"sha256:
|
89 |
],
|
90 |
-
"version": "==
|
91 |
},
|
92 |
"babel": {
|
93 |
"hashes": [
|
@@ -101,21 +55,22 @@
|
|
101 |
"sha256:6744c873dd7aafc24453d8b6a1a0d6d109faf63cd0cd19cb78fd46e74932c77e",
|
102 |
"sha256:6753d9ff3552013e2130f7bc03c1007e24473b4835952679653fb132367bdd57"
|
103 |
],
|
|
|
104 |
"version": "==0.5.3"
|
105 |
},
|
106 |
"cached-property": {
|
107 |
"hashes": [
|
108 |
-
"sha256:
|
109 |
-
"sha256:
|
110 |
],
|
111 |
-
"version": "==1.4.
|
112 |
},
|
113 |
"certifi": {
|
114 |
"hashes": [
|
115 |
-
"sha256:
|
116 |
-
"sha256:
|
117 |
],
|
118 |
-
"version": "==2018.
|
119 |
},
|
120 |
"cffi": {
|
121 |
"hashes": [
|
@@ -147,7 +102,7 @@
|
|
147 |
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
|
148 |
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
|
149 |
],
|
150 |
-
"markers": "platform_python_implementation != '
|
151 |
"version": "==1.11.5"
|
152 |
},
|
153 |
"cfgv": {
|
@@ -168,7 +123,7 @@
|
|
168 |
"hashes": [
|
169 |
"sha256:5308b47021bc2340965c371f0f058cc6971a04502638d4244225c49d80db273a"
|
170 |
],
|
171 |
-
"
|
172 |
"version": "==3.5.0"
|
173 |
},
|
174 |
"coverage": {
|
@@ -217,35 +172,30 @@
|
|
217 |
"sha256:32569a43c9dbc13fa8199247580a4ab182ef439f51f65bb7f8316d377a1340e8",
|
218 |
"sha256:664794748d2e5673e347ec476159a9d87f43e0d2d44950e98ed0e27b98da8346"
|
219 |
],
|
|
|
220 |
"version": "==1.3.0"
|
221 |
},
|
222 |
"cryptography": {
|
223 |
"hashes": [
|
224 |
-
"sha256:
|
225 |
-
"sha256:
|
226 |
-
"sha256:
|
227 |
-
"sha256:
|
228 |
-
"sha256:
|
229 |
-
"sha256:
|
230 |
-
"sha256:
|
231 |
-
"sha256:
|
232 |
-
"sha256:
|
233 |
-
"sha256:
|
234 |
-
"sha256:
|
235 |
-
"sha256:
|
236 |
-
"sha256:
|
237 |
-
"sha256:
|
238 |
-
"sha256:
|
239 |
-
"sha256:
|
240 |
-
"sha256:
|
241 |
-
|
242 |
-
|
243 |
-
"sha256:d18df9cf3f3212df28d445ea82ce702c4d7a35817ef7a38ee38879ffa8f7e857",
|
244 |
-
"sha256:e4d967371c5b6b2e67855066471d844c5d52d210c36c28d49a8507b96e2c5291",
|
245 |
-
"sha256:ee245f185fae723133511e2450be08a66c2eebb53ad27c0c19b228029f4748a5",
|
246 |
-
"sha256:fc2208d95d9ecc8032f5e38330d5ace2e3b0b998e42b08c30c35b2ab3a4a3bc8"
|
247 |
-
],
|
248 |
-
"version": "==2.1.4"
|
249 |
},
|
250 |
"docopt": {
|
251 |
"hashes": [
|
@@ -268,7 +218,7 @@
|
|
268 |
"sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79",
|
269 |
"sha256:8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1"
|
270 |
],
|
271 |
-
"
|
272 |
"version": "==1.1.6"
|
273 |
},
|
274 |
"flake8": {
|
@@ -276,22 +226,15 @@
|
|
276 |
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
|
277 |
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
|
278 |
],
|
|
|
279 |
"version": "==3.5.0"
|
280 |
},
|
281 |
-
"funcsigs": {
|
282 |
-
"hashes": [
|
283 |
-
"sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca",
|
284 |
-
"sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"
|
285 |
-
],
|
286 |
-
"markers": "python_version < '3.3'",
|
287 |
-
"version": "==1.0.2"
|
288 |
-
},
|
289 |
"identify": {
|
290 |
"hashes": [
|
291 |
-
"sha256:
|
292 |
-
"sha256:
|
293 |
],
|
294 |
-
"version": "==1.0.
|
295 |
},
|
296 |
"idna": {
|
297 |
"hashes": [
|
@@ -307,13 +250,6 @@
|
|
307 |
],
|
308 |
"version": "==1.0.0"
|
309 |
},
|
310 |
-
"ipaddress": {
|
311 |
-
"hashes": [
|
312 |
-
"sha256:200d8686011d470b5e4de207d803445deee427455cd0cb7c982b68cf82524f81"
|
313 |
-
],
|
314 |
-
"markers": "python_version < '3'",
|
315 |
-
"version": "==1.0.19"
|
316 |
-
},
|
317 |
"jinja2": {
|
318 |
"hashes": [
|
319 |
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
@@ -339,14 +275,22 @@
|
|
339 |
"sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1",
|
340 |
"sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba"
|
341 |
],
|
342 |
-
"
|
343 |
"version": "==2.0.0"
|
344 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
345 |
"nodeenv": {
|
346 |
"hashes": [
|
347 |
-
"sha256:
|
348 |
],
|
349 |
-
"version": "==1.
|
350 |
},
|
351 |
"packaging": {
|
352 |
"hashes": [
|
@@ -357,30 +301,33 @@
|
|
357 |
},
|
358 |
"pbr": {
|
359 |
"hashes": [
|
360 |
-
"sha256:
|
361 |
-
"sha256:
|
362 |
],
|
363 |
-
"version": "==
|
364 |
},
|
365 |
"pluggy": {
|
366 |
"hashes": [
|
367 |
-
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff"
|
|
|
|
|
368 |
],
|
369 |
"version": "==0.6.0"
|
370 |
},
|
371 |
"pre-commit": {
|
372 |
"hashes": [
|
373 |
-
"sha256:
|
374 |
-
"sha256:
|
375 |
],
|
376 |
-
"
|
|
|
377 |
},
|
378 |
"py": {
|
379 |
"hashes": [
|
380 |
-
"sha256:
|
381 |
-
"sha256:
|
382 |
],
|
383 |
-
"version": "==1.5.
|
384 |
},
|
385 |
"pycodestyle": {
|
386 |
"hashes": [
|
@@ -414,6 +361,7 @@
|
|
414 |
"sha256:07a2de1a54de07448732a81e38a55df7da109b2f47f599f8bb35b0cbec69d4bd",
|
415 |
"sha256:2c10cfba46a52c0b0950118981d61e72c1e5b1aac451ca1bc77de1a679456773"
|
416 |
],
|
|
|
417 |
"version": "==17.5.0"
|
418 |
},
|
419 |
"pyparsing": {
|
@@ -430,38 +378,34 @@
|
|
430 |
},
|
431 |
"pytest": {
|
432 |
"hashes": [
|
433 |
-
"sha256:
|
434 |
-
"sha256:
|
435 |
],
|
436 |
-
"
|
|
|
437 |
},
|
438 |
"pytest-cov": {
|
439 |
"hashes": [
|
440 |
"sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d",
|
441 |
"sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec"
|
442 |
],
|
|
|
443 |
"version": "==2.5.1"
|
444 |
},
|
445 |
"pytest-mock": {
|
446 |
"hashes": [
|
447 |
-
"sha256:
|
448 |
-
"sha256:
|
449 |
],
|
450 |
-
"
|
|
|
451 |
},
|
452 |
"pytz": {
|
453 |
"hashes": [
|
454 |
-
"sha256:
|
455 |
-
"sha256:
|
456 |
-
"sha256:410bcd1d6409026fbaa65d9ed33bf6dd8b1e94a499e32168acfc7b332e4095c0",
|
457 |
-
"sha256:5bd55c744e6feaa4d599a6cbd8228b4f8f9ba96de2c38d56f08e534b3c9edf0d",
|
458 |
-
"sha256:61242a9abc626379574a166dc0e96a66cd7c3b27fc10868003fa210be4bff1c9",
|
459 |
-
"sha256:887ab5e5b32e4d0c86efddd3d055c1f363cbaa583beb8da5e22d2fa2f64d51ef",
|
460 |
-
"sha256:ba18e6a243b3625513d85239b3e49055a2f0318466e0b8a92b8fb8ca7ccdf55f",
|
461 |
-
"sha256:ed6509d9af298b7995d69a440e2822288f2eca1681b8cce37673dbb10091e5fe",
|
462 |
-
"sha256:f93ddcdd6342f94cea379c73cddb5724e0d6d0a1c91c9bdef364dc0368ba4fda"
|
463 |
],
|
464 |
-
"version": "==2018.
|
465 |
},
|
466 |
"pyyaml": {
|
467 |
"hashes": [
|
@@ -505,17 +449,19 @@
|
|
505 |
},
|
506 |
"sphinx": {
|
507 |
"hashes": [
|
508 |
-
"sha256:
|
509 |
-
"sha256:
|
510 |
],
|
511 |
-
"
|
|
|
512 |
},
|
513 |
"sphinx-rtd-theme": {
|
514 |
"hashes": [
|
515 |
-
"sha256:
|
516 |
-
"sha256:
|
517 |
],
|
518 |
-
"
|
|
|
519 |
},
|
520 |
"sphinxcontrib-websupport": {
|
521 |
"hashes": [
|
@@ -524,28 +470,20 @@
|
|
524 |
],
|
525 |
"version": "==1.0.1"
|
526 |
},
|
527 |
-
"typing": {
|
528 |
-
"hashes": [
|
529 |
-
"sha256:3a887b021a77b292e151afb75323dea88a7bc1b3dfa92176cff8e44c8b68bddf",
|
530 |
-
"sha256:b2c689d54e1144bbcfd191b0832980a21c2dbcf7b5ff7a66248a60c90e951eb8",
|
531 |
-
"sha256:d400a9344254803a2368533e4533a4200d21eb7b6b729c173bc38201a74db3f2"
|
532 |
-
],
|
533 |
-
"markers": "python_version < '3.5'",
|
534 |
-
"version": "==3.6.4"
|
535 |
-
},
|
536 |
"urllib3": {
|
537 |
"hashes": [
|
538 |
"sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
|
539 |
"sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
|
540 |
],
|
|
|
541 |
"version": "==1.22"
|
542 |
},
|
543 |
"virtualenv": {
|
544 |
"hashes": [
|
545 |
-
"sha256:
|
546 |
-
"sha256:
|
547 |
],
|
548 |
-
"version": "==15.
|
549 |
}
|
550 |
}
|
551 |
}
|
|
|
1 |
{
|
2 |
"_meta": {
|
3 |
"hash": {
|
4 |
+
"sha256": "c601d22d66227c0d7c54837b8aad349a5f578cf1cf540ab38f56b48fe1313cab"
|
5 |
},
|
6 |
"pipfile-spec": 6,
|
7 |
"requires": {},
|
|
|
13 |
}
|
14 |
]
|
15 |
},
|
16 |
+
"default": {},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
"develop": {
|
18 |
"alabaster": {
|
19 |
"hashes": [
|
|
|
31 |
},
|
32 |
"aspy.yaml": {
|
33 |
"hashes": [
|
34 |
+
"sha256:c959530fab398e2391516bc8d5146489f9273b07d87dd8ba5e8b73406f7cc1fa",
|
35 |
+
"sha256:da95110d120a9168c9f43601b9cb732f006d8f193ee2c9b402c823026e4a9387"
|
36 |
],
|
37 |
+
"version": "==1.1.0"
|
38 |
},
|
39 |
"attrs": {
|
40 |
"hashes": [
|
41 |
+
"sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265",
|
42 |
+
"sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b"
|
43 |
],
|
44 |
+
"version": "==18.1.0"
|
45 |
},
|
46 |
"babel": {
|
47 |
"hashes": [
|
|
|
55 |
"sha256:6744c873dd7aafc24453d8b6a1a0d6d109faf63cd0cd19cb78fd46e74932c77e",
|
56 |
"sha256:6753d9ff3552013e2130f7bc03c1007e24473b4835952679653fb132367bdd57"
|
57 |
],
|
58 |
+
"index": "pypi",
|
59 |
"version": "==0.5.3"
|
60 |
},
|
61 |
"cached-property": {
|
62 |
"hashes": [
|
63 |
+
"sha256:67acb3ee8234245e8aea3784a492272239d9c4b487eba2fdcce9d75460d34520",
|
64 |
+
"sha256:bf093e640b7294303c7cc7ba3212f00b7a07d0416c1d923465995c9ef860a139"
|
65 |
],
|
66 |
+
"version": "==1.4.2"
|
67 |
},
|
68 |
"certifi": {
|
69 |
"hashes": [
|
70 |
+
"sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7",
|
71 |
+
"sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"
|
72 |
],
|
73 |
+
"version": "==2018.4.16"
|
74 |
},
|
75 |
"cffi": {
|
76 |
"hashes": [
|
|
|
102 |
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
|
103 |
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
|
104 |
],
|
105 |
+
"markers": "platform_python_implementation != 'pypy'",
|
106 |
"version": "==1.11.5"
|
107 |
},
|
108 |
"cfgv": {
|
|
|
123 |
"hashes": [
|
124 |
"sha256:5308b47021bc2340965c371f0f058cc6971a04502638d4244225c49d80db273a"
|
125 |
],
|
126 |
+
"index": "pypi",
|
127 |
"version": "==3.5.0"
|
128 |
},
|
129 |
"coverage": {
|
|
|
172 |
"sha256:32569a43c9dbc13fa8199247580a4ab182ef439f51f65bb7f8316d377a1340e8",
|
173 |
"sha256:664794748d2e5673e347ec476159a9d87f43e0d2d44950e98ed0e27b98da8346"
|
174 |
],
|
175 |
+
"index": "pypi",
|
176 |
"version": "==1.3.0"
|
177 |
},
|
178 |
"cryptography": {
|
179 |
"hashes": [
|
180 |
+
"sha256:3f3b65d5a16e6b52fba63dc860b62ca9832f51f1a2ae5083c78b6840275f12dd",
|
181 |
+
"sha256:551a3abfe0c8c6833df4192a63371aa2ff43afd8f570ed345d31f251d78e7e04",
|
182 |
+
"sha256:5cb990056b7cadcca26813311187ad751ea644712022a3976443691168781b6f",
|
183 |
+
"sha256:60bda7f12ecb828358be53095fc9c6edda7de8f1ef571f96c00b2363643fa3cd",
|
184 |
+
"sha256:6fef51ec447fe9f8351894024e94736862900d3a9aa2961528e602eb65c92bdb",
|
185 |
+
"sha256:77d0ad229d47a6e0272d00f6bf8ac06ce14715a9fd02c9a97f5a2869aab3ccb2",
|
186 |
+
"sha256:808fe471b1a6b777f026f7dc7bd9a4959da4bfab64972f2bbe91e22527c1c037",
|
187 |
+
"sha256:9b62fb4d18529c84b961efd9187fecbb48e89aa1a0f9f4161c61b7fc42a101bd",
|
188 |
+
"sha256:9e5bed45ec6b4f828866ac6a6bedf08388ffcfa68abe9e94b34bb40977aba531",
|
189 |
+
"sha256:9fc295bf69130a342e7a19a39d7bbeb15c0bcaabc7382ec33ef3b2b7d18d2f63",
|
190 |
+
"sha256:abd070b5849ed64e6d349199bef955ee0ad99aefbad792f0c587f8effa681a5e",
|
191 |
+
"sha256:ba6a774749b6e510cffc2fb98535f717e0e5fd91c7c99a61d223293df79ab351",
|
192 |
+
"sha256:c332118647f084c983c6a3e1dba0f3bcb051f69d12baccac68db8d62d177eb8a",
|
193 |
+
"sha256:d6f46e862ee36df81e6342c2177ba84e70f722d9dc9c6c394f9f1f434c4a5563",
|
194 |
+
"sha256:db6013746f73bf8edd9c3d1d3f94db635b9422f503db3fc5ef105233d4c011ab",
|
195 |
+
"sha256:f57008eaff597c69cf692c3518f6d4800f0309253bb138b526a37fe9ef0c7471",
|
196 |
+
"sha256:f6c821ac253c19f2ad4c8691633ae1d1a17f120d5b01ea1d256d7b602bc59887"
|
197 |
+
],
|
198 |
+
"version": "==2.2.2"
|
|
|
|
|
|
|
|
|
|
|
|
|
199 |
},
|
200 |
"docopt": {
|
201 |
"hashes": [
|
|
|
218 |
"sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79",
|
219 |
"sha256:8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1"
|
220 |
],
|
221 |
+
"index": "pypi",
|
222 |
"version": "==1.1.6"
|
223 |
},
|
224 |
"flake8": {
|
|
|
226 |
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
|
227 |
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
|
228 |
],
|
229 |
+
"index": "pypi",
|
230 |
"version": "==3.5.0"
|
231 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
232 |
"identify": {
|
233 |
"hashes": [
|
234 |
+
"sha256:8c127f455e8503eb3a5ed5388527719e1fef00a41b5e58dc036bc116f3bb8a16",
|
235 |
+
"sha256:bb5bdf324b4a24def86757c8dd8a4e91a9c28bbf1bf8505d702ce4b8d2508270"
|
236 |
],
|
237 |
+
"version": "==1.0.16"
|
238 |
},
|
239 |
"idna": {
|
240 |
"hashes": [
|
|
|
250 |
],
|
251 |
"version": "==1.0.0"
|
252 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
253 |
"jinja2": {
|
254 |
"hashes": [
|
255 |
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
|
|
275 |
"sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1",
|
276 |
"sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba"
|
277 |
],
|
278 |
+
"index": "pypi",
|
279 |
"version": "==2.0.0"
|
280 |
},
|
281 |
+
"more-itertools": {
|
282 |
+
"hashes": [
|
283 |
+
"sha256:0dd8f72eeab0d2c3bd489025bb2f6a1b8342f9b198f6fc37b52d15cfa4531fea",
|
284 |
+
"sha256:11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2442e",
|
285 |
+
"sha256:c9ce7eccdcb901a2c75d326ea134e0886abfbea5f93e91cc95de9507c0816c44"
|
286 |
+
],
|
287 |
+
"version": "==4.1.0"
|
288 |
+
},
|
289 |
"nodeenv": {
|
290 |
"hashes": [
|
291 |
+
"sha256:dd0a34001090ff042cfdb4b0c8d6a6f7ec9baa49733f00b695bb8a8b4700ba6c"
|
292 |
],
|
293 |
+
"version": "==1.3.0"
|
294 |
},
|
295 |
"packaging": {
|
296 |
"hashes": [
|
|
|
301 |
},
|
302 |
"pbr": {
|
303 |
"hashes": [
|
304 |
+
"sha256:4e8a0ed6a8705a26768f4c3da26026013b157821fe5f95881599556ea9d91c19",
|
305 |
+
"sha256:dae4aaa78eafcad10ce2581fc34d694faa616727837fd8e55c1a00951ad6744f"
|
306 |
],
|
307 |
+
"version": "==4.0.2"
|
308 |
},
|
309 |
"pluggy": {
|
310 |
"hashes": [
|
311 |
+
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff",
|
312 |
+
"sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c",
|
313 |
+
"sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5"
|
314 |
],
|
315 |
"version": "==0.6.0"
|
316 |
},
|
317 |
"pre-commit": {
|
318 |
"hashes": [
|
319 |
+
"sha256:01bb5f44606735ca30c8be641fa24f5760fcc599a0260ead0067bcde2f0305f9",
|
320 |
+
"sha256:823452163aa9fb024a9ff30947ba7f5a2778708db7554a4d36438b9bbead6bbb"
|
321 |
],
|
322 |
+
"index": "pypi",
|
323 |
+
"version": "==1.8.2"
|
324 |
},
|
325 |
"py": {
|
326 |
"hashes": [
|
327 |
+
"sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881",
|
328 |
+
"sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a"
|
329 |
],
|
330 |
+
"version": "==1.5.3"
|
331 |
},
|
332 |
"pycodestyle": {
|
333 |
"hashes": [
|
|
|
361 |
"sha256:07a2de1a54de07448732a81e38a55df7da109b2f47f599f8bb35b0cbec69d4bd",
|
362 |
"sha256:2c10cfba46a52c0b0950118981d61e72c1e5b1aac451ca1bc77de1a679456773"
|
363 |
],
|
364 |
+
"index": "pypi",
|
365 |
"version": "==17.5.0"
|
366 |
},
|
367 |
"pyparsing": {
|
|
|
378 |
},
|
379 |
"pytest": {
|
380 |
"hashes": [
|
381 |
+
"sha256:54713b26c97538db6ff0703a12b19aeaeb60b5e599de542e7fca0ec83b9038e8",
|
382 |
+
"sha256:829230122facf05a5f81a6d4dfe6454a04978ea3746853b2b84567ecf8e5c526"
|
383 |
],
|
384 |
+
"index": "pypi",
|
385 |
+
"version": "==3.5.1"
|
386 |
},
|
387 |
"pytest-cov": {
|
388 |
"hashes": [
|
389 |
"sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d",
|
390 |
"sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec"
|
391 |
],
|
392 |
+
"index": "pypi",
|
393 |
"version": "==2.5.1"
|
394 |
},
|
395 |
"pytest-mock": {
|
396 |
"hashes": [
|
397 |
+
"sha256:53801e621223d34724926a5c98bd90e8e417ce35264365d39d6c896388dcc928",
|
398 |
+
"sha256:d89a8209d722b8307b5e351496830d5cc5e192336003a485443ae9adeb7dd4c0"
|
399 |
],
|
400 |
+
"index": "pypi",
|
401 |
+
"version": "==1.10.0"
|
402 |
},
|
403 |
"pytz": {
|
404 |
"hashes": [
|
405 |
+
"sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555",
|
406 |
+
"sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
407 |
],
|
408 |
+
"version": "==2018.4"
|
409 |
},
|
410 |
"pyyaml": {
|
411 |
"hashes": [
|
|
|
449 |
},
|
450 |
"sphinx": {
|
451 |
"hashes": [
|
452 |
+
"sha256:2e7ad92e96eff1b2006cf9f0cdb2743dacbae63755458594e9e8238b0c3dc60b",
|
453 |
+
"sha256:e9b1a75a3eae05dded19c80eb17325be675e0698975baae976df603b6ed1eb10"
|
454 |
],
|
455 |
+
"index": "pypi",
|
456 |
+
"version": "==1.7.4"
|
457 |
},
|
458 |
"sphinx-rtd-theme": {
|
459 |
"hashes": [
|
460 |
+
"sha256:32424dac2779f0840b4788fbccb032ba2496c1ca47a439ad2510c8b1e55dfd33",
|
461 |
+
"sha256:6d0481532b5f441b075127a2d755f430f1f8410a50112b1af6b069518548381d"
|
462 |
],
|
463 |
+
"index": "pypi",
|
464 |
+
"version": "==0.3.1"
|
465 |
},
|
466 |
"sphinxcontrib-websupport": {
|
467 |
"hashes": [
|
|
|
470 |
],
|
471 |
"version": "==1.0.1"
|
472 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
473 |
"urllib3": {
|
474 |
"hashes": [
|
475 |
"sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
|
476 |
"sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
|
477 |
],
|
478 |
+
"index": "pypi",
|
479 |
"version": "==1.22"
|
480 |
},
|
481 |
"virtualenv": {
|
482 |
"hashes": [
|
483 |
+
"sha256:1d7e241b431e7afce47e77f8843a276f652699d1fa4f93b9d8ce0076fd7b0b54",
|
484 |
+
"sha256:e8e05d4714a1c51a2f5921e62f547fcb0f713ebbe959e0a7f585cc8bef71d11f"
|
485 |
],
|
486 |
+
"version": "==15.2.0"
|
487 |
}
|
488 |
}
|
489 |
}
|
README.md
CHANGED
@@ -13,7 +13,7 @@
|
|
13 |
</div>
|
14 |
|
15 |
# pytube
|
16 |
-
*pytube* is a lightweight, dependency-free Python library (and command-line utility) for downloading YouTube Videos.
|
17 |
|
18 |
## Description
|
19 |
YouTube is the most popular video-sharing platform in the world and as a hacker you may encounter a situation where you want to script something to download videos. For this I present to you *pytube*.
|
|
|
13 |
</div>
|
14 |
|
15 |
# pytube
|
16 |
+
*pytube* is a very serious, lightweight, dependency-free Python library (and command-line utility) for downloading YouTube Videos.
|
17 |
|
18 |
## Description
|
19 |
YouTube is the most popular video-sharing platform in the world and as a hacker you may encounter a situation where you want to script something to download videos. For this I present to you *pytube*.
|
pytube/__init__.py
CHANGED
@@ -2,14 +2,10 @@
|
|
2 |
# flake8: noqa
|
3 |
# noreorder
|
4 |
"""
|
5 |
-
Pytube
|
6 |
-
|
7 |
-
Pytube aims to be lightweight, dependency-free, extensively documented and
|
8 |
-
follows best practice patterns.
|
9 |
-
|
10 |
"""
|
11 |
__title__ = 'pytube'
|
12 |
-
__version__ = '9.
|
13 |
__author__ = 'Nick Ficano'
|
14 |
__license__ = 'MIT License'
|
15 |
__copyright__ = 'Copyright 2017 Nick Ficano'
|
|
|
2 |
# flake8: noqa
|
3 |
# noreorder
|
4 |
"""
|
5 |
+
Pytube: a very serious Python library for downloading YouTube Videos.
|
|
|
|
|
|
|
|
|
6 |
"""
|
7 |
__title__ = 'pytube'
|
8 |
+
__version__ = '9.2.3'
|
9 |
__author__ = 'Nick Ficano'
|
10 |
__license__ = 'MIT License'
|
11 |
__copyright__ = 'Copyright 2017 Nick Ficano'
|
pytube/__main__.py
CHANGED
@@ -19,10 +19,11 @@ from pytube import mixins
|
|
19 |
from pytube import request
|
20 |
from pytube import Stream
|
21 |
from pytube import StreamQuery
|
|
|
22 |
from pytube.compat import parse_qsl
|
|
|
23 |
from pytube.helpers import apply_mixin
|
24 |
|
25 |
-
|
26 |
logger = logging.getLogger(__name__)
|
27 |
|
28 |
|
@@ -31,7 +32,7 @@ class YouTube(object):
|
|
31 |
|
32 |
def __init__(
|
33 |
self, url=None, defer_prefetch_init=False, on_progress_callback=None,
|
34 |
-
on_complete_callback=None,
|
35 |
):
|
36 |
"""Construct a :class:`YouTube <YouTube>`.
|
37 |
|
@@ -80,6 +81,9 @@ class YouTube(object):
|
|
80 |
'on_complete': on_complete_callback,
|
81 |
}
|
82 |
|
|
|
|
|
|
|
83 |
if not defer_prefetch_init:
|
84 |
self.prefetch_init()
|
85 |
|
@@ -154,6 +158,8 @@ class YouTube(object):
|
|
154 |
|
155 |
"""
|
156 |
self.watch_html = request.get(url=self.watch_url)
|
|
|
|
|
157 |
self.embed_html = request.get(url=self.embed_url)
|
158 |
self.age_restricted = extract.is_age_restricted(self.watch_html)
|
159 |
self.vid_info_url = extract.video_info_url(
|
|
|
19 |
from pytube import request
|
20 |
from pytube import Stream
|
21 |
from pytube import StreamQuery
|
22 |
+
from pytube.compat import install_proxy
|
23 |
from pytube.compat import parse_qsl
|
24 |
+
from pytube.exceptions import VideoUnavailable
|
25 |
from pytube.helpers import apply_mixin
|
26 |
|
|
|
27 |
logger = logging.getLogger(__name__)
|
28 |
|
29 |
|
|
|
32 |
|
33 |
def __init__(
|
34 |
self, url=None, defer_prefetch_init=False, on_progress_callback=None,
|
35 |
+
on_complete_callback=None, proxies=None,
|
36 |
):
|
37 |
"""Construct a :class:`YouTube <YouTube>`.
|
38 |
|
|
|
81 |
'on_complete': on_complete_callback,
|
82 |
}
|
83 |
|
84 |
+
if proxies:
|
85 |
+
install_proxy(proxies)
|
86 |
+
|
87 |
if not defer_prefetch_init:
|
88 |
self.prefetch_init()
|
89 |
|
|
|
158 |
|
159 |
"""
|
160 |
self.watch_html = request.get(url=self.watch_url)
|
161 |
+
if '<img class="icon meh" src="/yts/img' not in self.watch_html:
|
162 |
+
raise VideoUnavailable('This video is unavailable.')
|
163 |
self.embed_html = request.get(url=self.embed_url)
|
164 |
self.age_restricted = extract.is_age_restricted(self.watch_html)
|
165 |
self.vid_info_url = extract.video_info_url(
|
pytube/compat.py
CHANGED
@@ -10,6 +10,7 @@ PY33 = sys.version_info[0:2] >= (3, 3)
|
|
10 |
|
11 |
|
12 |
if PY2:
|
|
|
13 |
from urllib import urlencode
|
14 |
from urllib2 import URLError
|
15 |
from urllib2 import quote
|
@@ -18,6 +19,17 @@ if PY2:
|
|
18 |
from urlparse import parse_qsl
|
19 |
from HTMLParser import HTMLParser
|
20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
def unescape(s):
|
22 |
"""Strip HTML entries from a string."""
|
23 |
html_parser = HTMLParser()
|
@@ -34,6 +46,12 @@ elif PY3:
|
|
34 |
from urllib.parse import unquote
|
35 |
from urllib.parse import urlencode
|
36 |
from urllib.request import urlopen
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
|
38 |
def unicode(s):
|
39 |
"""No-op."""
|
|
|
10 |
|
11 |
|
12 |
if PY2:
|
13 |
+
import urllib2
|
14 |
from urllib import urlencode
|
15 |
from urllib2 import URLError
|
16 |
from urllib2 import quote
|
|
|
19 |
from urlparse import parse_qsl
|
20 |
from HTMLParser import HTMLParser
|
21 |
|
22 |
+
def install_proxy(proxy_handler):
|
23 |
+
"""
|
24 |
+
install global proxy.
|
25 |
+
:param proxy_handler:
|
26 |
+
:samp:`{"http":"http://my.proxy.com:1234", "https":"https://my.proxy.com:1234"}`
|
27 |
+
:return:
|
28 |
+
"""
|
29 |
+
proxy_support = urllib2.ProxyHandler(proxy_handler)
|
30 |
+
opener = urllib2.build_opener(proxy_support)
|
31 |
+
urllib2.install_opener(opener)
|
32 |
+
|
33 |
def unescape(s):
|
34 |
"""Strip HTML entries from a string."""
|
35 |
html_parser = HTMLParser()
|
|
|
46 |
from urllib.parse import unquote
|
47 |
from urllib.parse import urlencode
|
48 |
from urllib.request import urlopen
|
49 |
+
from urllib import request
|
50 |
+
|
51 |
+
def install_proxy(proxy_handler):
|
52 |
+
proxy_support = request.ProxyHandler(proxy_handler)
|
53 |
+
opener = request.build_opener(proxy_support)
|
54 |
+
request.install_opener(opener)
|
55 |
|
56 |
def unicode(s):
|
57 |
"""No-op."""
|
pytube/contrib/playlist.py
CHANGED
@@ -66,26 +66,67 @@ class Playlist(object):
|
|
66 |
complete_url = base_url + video_id
|
67 |
self.video_urls.append(complete_url)
|
68 |
|
69 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
"""Download all the videos in the the playlist. Initially, download
|
71 |
resolution is 720p (or highest available), later more option
|
72 |
should be added to download resolution of choice
|
73 |
|
74 |
TODO(nficano): Add option to download resolution of user's choice
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
"""
|
76 |
|
77 |
self.populate_video_urls()
|
78 |
-
logger.debug('total videos found: ', len(self.video_urls))
|
79 |
logger.debug('starting download')
|
80 |
|
|
|
|
|
81 |
for link in self.video_urls:
|
82 |
yt = YouTube(link)
|
83 |
-
|
84 |
# TODO: this should not be hardcoded to a single user's preference
|
85 |
dl_stream = yt.streams.filter(
|
86 |
progressive=True, subtype='mp4',
|
87 |
).order_by('resolution').desc().first()
|
88 |
|
89 |
logger.debug('download path: %s', download_path)
|
90 |
-
|
|
|
|
|
|
|
|
|
|
|
91 |
logger.debug('download complete')
|
|
|
66 |
complete_url = base_url + video_id
|
67 |
self.video_urls.append(complete_url)
|
68 |
|
69 |
+
def _path_num_prefix_generator(self, reverse=False):
|
70 |
+
"""
|
71 |
+
This generator function generates number prefixes, for the items
|
72 |
+
in the playlist.
|
73 |
+
If the number of digits required to name a file,is less than is
|
74 |
+
required to name the last file,it prepends 0s.
|
75 |
+
So if you have a playlist of 100 videos it will number them like:
|
76 |
+
001, 002, 003 ect, up to 100.
|
77 |
+
It also adds a space after the number.
|
78 |
+
:return: prefix string generator : generator
|
79 |
+
"""
|
80 |
+
digits = len(str(len(self.video_urls)))
|
81 |
+
if reverse:
|
82 |
+
start, stop, step = (len(self.video_urls), 0, -1)
|
83 |
+
else:
|
84 |
+
start, stop, step = (1, len(self.video_urls) + 1, 1)
|
85 |
+
return (str(i).zfill(digits) for i in range(start, stop, step))
|
86 |
+
|
87 |
+
def download_all(
|
88 |
+
self, download_path=None, prefix_number=True,
|
89 |
+
reverse_numbering=False,
|
90 |
+
):
|
91 |
"""Download all the videos in the the playlist. Initially, download
|
92 |
resolution is 720p (or highest available), later more option
|
93 |
should be added to download resolution of choice
|
94 |
|
95 |
TODO(nficano): Add option to download resolution of user's choice
|
96 |
+
|
97 |
+
:param download_path:
|
98 |
+
(optional) Output path for the playlist If one is not
|
99 |
+
specified, defaults to the current working directory.
|
100 |
+
This is passed along to the Stream objects.
|
101 |
+
:type download_path: str or None
|
102 |
+
:param prefix_number:
|
103 |
+
(optional) Automatically numbers playlists using the
|
104 |
+
_path_num_prefix_generator function.
|
105 |
+
:type prefix_number: bool
|
106 |
+
:param reverse_numbering:
|
107 |
+
(optional) Lets you number playlists in reverse, since some
|
108 |
+
playlists are ordered newest -> oldests.
|
109 |
+
:type reverse_numbering: bool
|
110 |
"""
|
111 |
|
112 |
self.populate_video_urls()
|
113 |
+
logger.debug('total videos found: %d', len(self.video_urls))
|
114 |
logger.debug('starting download')
|
115 |
|
116 |
+
prefix_gen = self._path_num_prefix_generator(reverse_numbering)
|
117 |
+
|
118 |
for link in self.video_urls:
|
119 |
yt = YouTube(link)
|
|
|
120 |
# TODO: this should not be hardcoded to a single user's preference
|
121 |
dl_stream = yt.streams.filter(
|
122 |
progressive=True, subtype='mp4',
|
123 |
).order_by('resolution').desc().first()
|
124 |
|
125 |
logger.debug('download path: %s', download_path)
|
126 |
+
if prefix_number:
|
127 |
+
prefix = next(prefix_gen)
|
128 |
+
logger.debug('file prefix is: %s', prefix)
|
129 |
+
dl_stream.download(download_path, filename_prefix=prefix)
|
130 |
+
else:
|
131 |
+
dl_stream.download(download_path)
|
132 |
logger.debug('download complete')
|
pytube/exceptions.py
CHANGED
@@ -34,3 +34,7 @@ class ExtractError(PytubeError):
|
|
34 |
|
35 |
class RegexMatchError(ExtractError):
|
36 |
"""Regex pattern did not return any matches."""
|
|
|
|
|
|
|
|
|
|
34 |
|
35 |
class RegexMatchError(ExtractError):
|
36 |
"""Regex pattern did not return any matches."""
|
37 |
+
|
38 |
+
|
39 |
+
class VideoUnavailable(PytubeError):
|
40 |
+
"""Video is unavailable."""
|
pytube/itags.py
CHANGED
@@ -57,9 +57,9 @@ ITAGS = {
|
|
57 |
246: ('480p', None),
|
58 |
247: ('720p', None),
|
59 |
248: ('1080p', None),
|
60 |
-
264: ('
|
61 |
266: ('2160p', None),
|
62 |
-
271: ('
|
63 |
272: ('2160p', None),
|
64 |
278: ('144p', None),
|
65 |
298: ('720p', None),
|
|
|
57 |
246: ('480p', None),
|
58 |
247: ('720p', None),
|
59 |
248: ('1080p', None),
|
60 |
+
264: ('1440p', None),
|
61 |
266: ('2160p', None),
|
62 |
+
271: ('1440p', None),
|
63 |
272: ('2160p', None),
|
64 |
278: ('144p', None),
|
65 |
298: ('720p', None),
|
pytube/query.py
CHANGED
@@ -187,7 +187,7 @@ class StreamQuery:
|
|
187 |
def get_by_itag(self, itag):
|
188 |
"""Get the corresponding :class:`Stream <Stream>` for a given itag.
|
189 |
|
190 |
-
:param str itag:
|
191 |
YouTube format identifier code.
|
192 |
:rtype: :class:`Stream <Stream>` or None
|
193 |
:returns:
|
@@ -196,7 +196,7 @@ class StreamQuery:
|
|
196 |
|
197 |
"""
|
198 |
try:
|
199 |
-
return self.itag_index[itag]
|
200 |
except KeyError:
|
201 |
pass
|
202 |
|
|
|
187 |
def get_by_itag(self, itag):
|
188 |
"""Get the corresponding :class:`Stream <Stream>` for a given itag.
|
189 |
|
190 |
+
:param str int itag:
|
191 |
YouTube format identifier code.
|
192 |
:rtype: :class:`Stream <Stream>` or None
|
193 |
:returns:
|
|
|
196 |
|
197 |
"""
|
198 |
try:
|
199 |
+
return self.itag_index[int(itag)]
|
200 |
except KeyError:
|
201 |
pass
|
202 |
|
pytube/streams.py
CHANGED
@@ -9,6 +9,7 @@ separately).
|
|
9 |
"""
|
10 |
from __future__ import absolute_import
|
11 |
|
|
|
12 |
import logging
|
13 |
import os
|
14 |
import pprint
|
@@ -173,7 +174,7 @@ class Stream(object):
|
|
173 |
filename = safe_filename(title)
|
174 |
return '{filename}.{s.subtype}'.format(filename=filename, s=self)
|
175 |
|
176 |
-
def download(self, output_path=None, filename=None):
|
177 |
"""Write the media stream to disk.
|
178 |
|
179 |
:param output_path:
|
@@ -184,8 +185,15 @@ class Stream(object):
|
|
184 |
(optional) Output filename (stem only) for writing media file.
|
185 |
If one is not specified, the default filename is used.
|
186 |
:type filename: str or None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
187 |
|
188 |
-
:rtype:
|
189 |
|
190 |
"""
|
191 |
output_path = output_path or os.getcwd()
|
@@ -194,6 +202,13 @@ class Stream(object):
|
|
194 |
filename = '{filename}.{s.subtype}'.format(filename=safe, s=self)
|
195 |
filename = filename or self.default_filename
|
196 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
197 |
# file path
|
198 |
fp = os.path.join(output_path, filename)
|
199 |
bytes_remaining = self.filesize
|
@@ -209,6 +224,27 @@ class Stream(object):
|
|
209 |
# send to the on_progress callback.
|
210 |
self.on_progress(chunk, fh, bytes_remaining)
|
211 |
self.on_complete(fh)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
212 |
|
213 |
def on_progress(self, chunk, file_handler, bytes_remaining):
|
214 |
"""On progress callback function.
|
|
|
9 |
"""
|
10 |
from __future__ import absolute_import
|
11 |
|
12 |
+
import io
|
13 |
import logging
|
14 |
import os
|
15 |
import pprint
|
|
|
174 |
filename = safe_filename(title)
|
175 |
return '{filename}.{s.subtype}'.format(filename=filename, s=self)
|
176 |
|
177 |
+
def download(self, output_path=None, filename=None, filename_prefix=None):
|
178 |
"""Write the media stream to disk.
|
179 |
|
180 |
:param output_path:
|
|
|
185 |
(optional) Output filename (stem only) for writing media file.
|
186 |
If one is not specified, the default filename is used.
|
187 |
:type filename: str or None
|
188 |
+
:param filename_prefix:
|
189 |
+
(optional) A string that will be prepended to the filename.
|
190 |
+
For example a number in a playlist or the name of a series.
|
191 |
+
If one is not specified, nothing will be prepended
|
192 |
+
This is seperate from filename so you can use the default
|
193 |
+
filename but still add a prefix.
|
194 |
+
:type filename_prefix: str or None
|
195 |
|
196 |
+
:rtype: str
|
197 |
|
198 |
"""
|
199 |
output_path = output_path or os.getcwd()
|
|
|
202 |
filename = '{filename}.{s.subtype}'.format(filename=safe, s=self)
|
203 |
filename = filename or self.default_filename
|
204 |
|
205 |
+
if filename_prefix:
|
206 |
+
filename = '{prefix}{filename}'\
|
207 |
+
.format(
|
208 |
+
prefix=safe_filename(filename_prefix),
|
209 |
+
filename=filename,
|
210 |
+
)
|
211 |
+
|
212 |
# file path
|
213 |
fp = os.path.join(output_path, filename)
|
214 |
bytes_remaining = self.filesize
|
|
|
224 |
# send to the on_progress callback.
|
225 |
self.on_progress(chunk, fh, bytes_remaining)
|
226 |
self.on_complete(fh)
|
227 |
+
return fp
|
228 |
+
|
229 |
+
def stream_to_buffer(self):
|
230 |
+
"""Write the media stream to buffer
|
231 |
+
|
232 |
+
:rtype: io.BytesIO buffer
|
233 |
+
"""
|
234 |
+
buffer = io.BytesIO()
|
235 |
+
bytes_remaining = self.filesize
|
236 |
+
logger.debug(
|
237 |
+
'downloading (%s total bytes) file to BytesIO buffer',
|
238 |
+
self.filesize,
|
239 |
+
)
|
240 |
+
|
241 |
+
for chunk in request.get(self.url, streaming=True):
|
242 |
+
# reduce the (bytes) remainder by the length of the chunk.
|
243 |
+
bytes_remaining -= len(chunk)
|
244 |
+
# send to the on_progress callback.
|
245 |
+
self.on_progress(chunk, buffer, bytes_remaining)
|
246 |
+
self.on_complete(buffer)
|
247 |
+
return buffer
|
248 |
|
249 |
def on_progress(self, chunk, file_handler, bytes_remaining):
|
250 |
"""On progress callback function.
|
setup.cfg
CHANGED
@@ -1,9 +1,9 @@
|
|
1 |
[bumpversion]
|
2 |
commit = True
|
3 |
tag = True
|
4 |
-
current_version = 9.
|
5 |
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?
|
6 |
-
serialize =
|
7 |
{major}.{minor}.{patch}
|
8 |
|
9 |
[metadata]
|
@@ -15,6 +15,5 @@ description-file = README.md
|
|
15 |
|
16 |
[coverage:run]
|
17 |
source = pytube
|
18 |
-
omit =
|
19 |
pytube/compat.py
|
20 |
-
|
|
|
1 |
[bumpversion]
|
2 |
commit = True
|
3 |
tag = True
|
4 |
+
current_version = 9.2.3
|
5 |
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?
|
6 |
+
serialize =
|
7 |
{major}.{minor}.{patch}
|
8 |
|
9 |
[metadata]
|
|
|
15 |
|
16 |
[coverage:run]
|
17 |
source = pytube
|
18 |
+
omit =
|
19 |
pytube/compat.py
|
|
setup.py
CHANGED
@@ -14,7 +14,7 @@ with open('LICENSE') as readme_file:
|
|
14 |
|
15 |
setup(
|
16 |
name='pytube',
|
17 |
-
version='9.
|
18 |
author='Nick Ficano',
|
19 |
author_email='nficano@gmail.com',
|
20 |
packages=['pytube', 'pytube.contrib'],
|
|
|
14 |
|
15 |
setup(
|
16 |
name='pytube',
|
17 |
+
version='9.2.3',
|
18 |
author='Nick Ficano',
|
19 |
author_email='nficano@gmail.com',
|
20 |
packages=['pytube', 'pytube.contrib'],
|
tests/test_playlist.py
CHANGED
@@ -1,12 +1,14 @@
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
from pytube import Playlist
|
3 |
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
def test_construct():
|
6 |
-
ob = Playlist(
|
7 |
-
'https://www.youtube.com/watch?v=m5q2GCsteQs&list='
|
8 |
-
'PL525f8ds9RvsXDl44X6Wwh9t3fCzFNApw',
|
9 |
-
)
|
10 |
expected = 'https://www.youtube.com/' \
|
11 |
'playlist?list=' \
|
12 |
'PL525f8ds9RvsXDl44X6Wwh9t3fCzFNApw'
|
@@ -14,25 +16,8 @@ def test_construct():
|
|
14 |
assert ob.construct_playlist_url() == expected
|
15 |
|
16 |
|
17 |
-
def test_link_parse():
|
18 |
-
ob = Playlist(
|
19 |
-
'https://www.youtube.com/watch?v=m5q2GCsteQs&list='
|
20 |
-
'PL525f8ds9RvsXDl44X6Wwh9t3fCzFNApw',
|
21 |
-
)
|
22 |
-
|
23 |
-
expected = [
|
24 |
-
'/watch?v=m5q2GCsteQs',
|
25 |
-
'/watch?v=5YK63cXyJ2Q',
|
26 |
-
'/watch?v=Rzt4rUPFYD4',
|
27 |
-
]
|
28 |
-
assert ob.parse_links() == expected
|
29 |
-
|
30 |
-
|
31 |
def test_populate():
|
32 |
-
ob = Playlist(
|
33 |
-
'https://www.youtube.com/watch?v=m5q2GCsteQs&list='
|
34 |
-
'PL525f8ds9RvsXDl44X6Wwh9t3fCzFNApw',
|
35 |
-
)
|
36 |
expected = [
|
37 |
'https://www.youtube.com/watch?v=m5q2GCsteQs',
|
38 |
'https://www.youtube.com/watch?v=5YK63cXyJ2Q',
|
@@ -43,10 +28,50 @@ def test_populate():
|
|
43 |
assert ob.video_urls == expected
|
44 |
|
45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
def test_download():
|
47 |
-
ob = Playlist(
|
48 |
-
'https://www.youtube.com/watch?v=lByG_AgKS9k&list='
|
49 |
-
'PL525f8ds9RvuerPZ3bZygmNiYw2sP4BDk',
|
50 |
-
)
|
51 |
ob.download_all()
|
52 |
ob.download_all('.')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# -*- coding: utf-8 -*-
|
2 |
from pytube import Playlist
|
3 |
|
4 |
+
short_test_pl = 'https://www.youtube.com/watch?v=' \
|
5 |
+
'm5q2GCsteQs&list=PL525f8ds9RvsXDl44X6Wwh9t3fCzFNApw'
|
6 |
+
long_test_pl = 'https://www.youtube.com/watch?v=' \
|
7 |
+
'9CHDoAsX1yo&list=UUXuqSBlHAE6Xw-yeJA0Tunw'
|
8 |
+
|
9 |
|
10 |
def test_construct():
|
11 |
+
ob = Playlist(short_test_pl)
|
|
|
|
|
|
|
12 |
expected = 'https://www.youtube.com/' \
|
13 |
'playlist?list=' \
|
14 |
'PL525f8ds9RvsXDl44X6Wwh9t3fCzFNApw'
|
|
|
16 |
assert ob.construct_playlist_url() == expected
|
17 |
|
18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
def test_populate():
|
20 |
+
ob = Playlist(short_test_pl)
|
|
|
|
|
|
|
21 |
expected = [
|
22 |
'https://www.youtube.com/watch?v=m5q2GCsteQs',
|
23 |
'https://www.youtube.com/watch?v=5YK63cXyJ2Q',
|
|
|
28 |
assert ob.video_urls == expected
|
29 |
|
30 |
|
31 |
+
def test_link_parse():
|
32 |
+
ob = Playlist(short_test_pl)
|
33 |
+
|
34 |
+
expected = [
|
35 |
+
'/watch?v=m5q2GCsteQs',
|
36 |
+
'/watch?v=5YK63cXyJ2Q',
|
37 |
+
'/watch?v=Rzt4rUPFYD4',
|
38 |
+
]
|
39 |
+
assert ob.parse_links() == expected
|
40 |
+
|
41 |
+
|
42 |
def test_download():
|
43 |
+
ob = Playlist(short_test_pl)
|
|
|
|
|
|
|
44 |
ob.download_all()
|
45 |
ob.download_all('.')
|
46 |
+
ob.download_all(prefix_number=False)
|
47 |
+
ob.download_all('.', prefix_number=False)
|
48 |
+
|
49 |
+
|
50 |
+
def test_numbering():
|
51 |
+
ob = Playlist(short_test_pl)
|
52 |
+
ob.populate_video_urls()
|
53 |
+
gen = ob._path_num_prefix_generator(reverse=False)
|
54 |
+
assert '1' in next(gen)
|
55 |
+
assert '2' in next(gen)
|
56 |
+
|
57 |
+
ob = Playlist(short_test_pl)
|
58 |
+
ob.populate_video_urls()
|
59 |
+
gen = ob._path_num_prefix_generator(reverse=True)
|
60 |
+
assert str(len(ob.video_urls)) in next(gen)
|
61 |
+
assert str(len(ob.video_urls) - 1) in next(gen)
|
62 |
+
|
63 |
+
ob = Playlist(long_test_pl)
|
64 |
+
ob.populate_video_urls()
|
65 |
+
gen = ob._path_num_prefix_generator(reverse=False)
|
66 |
+
nxt = next(gen)
|
67 |
+
assert len(nxt) > 1
|
68 |
+
assert '1' in nxt
|
69 |
+
nxt = next(gen)
|
70 |
+
assert len(nxt) > 1
|
71 |
+
assert '2' in nxt
|
72 |
+
|
73 |
+
ob = Playlist(long_test_pl)
|
74 |
+
ob.populate_video_urls()
|
75 |
+
gen = ob._path_num_prefix_generator(reverse=True)
|
76 |
+
assert str(len(ob.video_urls)) in next(gen)
|
77 |
+
assert str(len(ob.video_urls) - 1) in next(gen)
|
tests/test_query.py
CHANGED
@@ -111,6 +111,7 @@ def test_get_by_itag(cipher_signature):
|
|
111 |
:class:`Stream <Stream>`.
|
112 |
"""
|
113 |
assert cipher_signature.streams.get_by_itag(22).itag == '22'
|
|
|
114 |
|
115 |
|
116 |
def test_get_by_non_existent_itag(cipher_signature):
|
|
|
111 |
:class:`Stream <Stream>`.
|
112 |
"""
|
113 |
assert cipher_signature.streams.get_by_itag(22).itag == '22'
|
114 |
+
assert cipher_signature.streams.get_by_itag('22').itag == '22'
|
115 |
|
116 |
|
117 |
def test_get_by_non_existent_itag(cipher_signature):
|