black formatting
Browse files- .flake8 +3 -0
- Pipfile.lock +148 -142
- pytube/__init__.py +6 -6
- pytube/__main__.py +44 -48
- pytube/captions.py +17 -22
- pytube/cipher.py +38 -37
- pytube/cli.py +35 -34
- pytube/contrib/playlist.py +35 -37
- pytube/exceptions.py +1 -1
- pytube/extract.py +30 -31
- pytube/helpers.py +29 -19
- pytube/itags.py +87 -89
- pytube/logging.py +3 -3
- pytube/mixins.py +52 -41
- pytube/query.py +25 -18
- pytube/request.py +4 -8
- pytube/streams.py +40 -47
.flake8
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
[flake8]
|
2 |
+
ignore = E231,E203,W605
|
3 |
+
max-line-length = 88
|
Pipfile.lock
CHANGED
@@ -1,11 +1,11 @@
|
|
1 |
{
|
2 |
"_meta": {
|
3 |
"hash": {
|
4 |
-
"sha256": "
|
5 |
},
|
6 |
"pipfile-spec": 6,
|
7 |
"requires": {
|
8 |
-
"python_version": "3.
|
9 |
},
|
10 |
"sources": [
|
11 |
{
|
@@ -19,24 +19,17 @@
|
|
19 |
"develop": {
|
20 |
"aspy.yaml": {
|
21 |
"hashes": [
|
22 |
-
"sha256:
|
23 |
-
"sha256:
|
24 |
-
],
|
25 |
-
"version": "==1.2.0"
|
26 |
-
},
|
27 |
-
"atomicwrites": {
|
28 |
-
"hashes": [
|
29 |
-
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
|
30 |
-
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
|
31 |
],
|
32 |
"version": "==1.3.0"
|
33 |
},
|
34 |
"attrs": {
|
35 |
"hashes": [
|
36 |
-
"sha256:
|
37 |
-
"sha256:
|
38 |
],
|
39 |
-
"version": "==19.
|
40 |
},
|
41 |
"bleach": {
|
42 |
"hashes": [
|
@@ -55,17 +48,17 @@
|
|
55 |
},
|
56 |
"certifi": {
|
57 |
"hashes": [
|
58 |
-
"sha256:
|
59 |
-
"sha256:
|
60 |
],
|
61 |
-
"version": "==2019.
|
62 |
},
|
63 |
"cfgv": {
|
64 |
"hashes": [
|
65 |
-
"sha256:
|
66 |
-
"sha256:
|
67 |
],
|
68 |
-
"version": "==
|
69 |
},
|
70 |
"chardet": {
|
71 |
"hashes": [
|
@@ -76,47 +69,47 @@
|
|
76 |
},
|
77 |
"coverage": {
|
78 |
"hashes": [
|
79 |
-
"sha256:
|
80 |
-
"sha256:
|
81 |
-
"sha256:
|
82 |
-
"sha256:
|
83 |
-
"sha256:
|
84 |
-
"sha256:
|
85 |
-
"sha256:
|
86 |
-
"sha256:
|
87 |
-
"sha256:
|
88 |
-
"sha256:
|
89 |
-
"sha256:
|
90 |
-
"sha256:
|
91 |
-
"sha256:
|
92 |
-
"sha256:
|
93 |
-
"sha256:
|
94 |
-
"sha256:
|
95 |
-
"sha256:
|
96 |
-
"sha256:
|
97 |
-
"sha256:
|
98 |
-
"sha256:
|
99 |
-
"sha256:
|
100 |
-
"sha256:
|
101 |
-
"sha256:
|
102 |
-
"sha256:
|
103 |
-
"sha256:
|
104 |
-
"sha256:
|
105 |
-
"sha256:
|
106 |
-
"sha256:
|
107 |
-
"sha256:
|
108 |
-
"sha256:
|
109 |
-
"sha256:
|
110 |
-
],
|
111 |
-
"version": "==
|
112 |
},
|
113 |
"coveralls": {
|
114 |
"hashes": [
|
115 |
-
"sha256:
|
116 |
-
"sha256:
|
117 |
],
|
118 |
"index": "pypi",
|
119 |
-
"version": "==1.
|
120 |
},
|
121 |
"docopt": {
|
122 |
"hashes": [
|
@@ -126,11 +119,10 @@
|
|
126 |
},
|
127 |
"docutils": {
|
128 |
"hashes": [
|
129 |
-
"sha256:
|
130 |
-
"sha256:
|
131 |
-
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
|
132 |
],
|
133 |
-
"version": "==0.
|
134 |
},
|
135 |
"entrypoints": {
|
136 |
"hashes": [
|
@@ -151,18 +143,18 @@
|
|
151 |
},
|
152 |
"flake8": {
|
153 |
"hashes": [
|
154 |
-
"sha256:
|
155 |
-
"sha256:
|
156 |
],
|
157 |
"index": "pypi",
|
158 |
-
"version": "==3.7.
|
159 |
},
|
160 |
"identify": {
|
161 |
"hashes": [
|
162 |
-
"sha256:
|
163 |
-
"sha256:
|
164 |
],
|
165 |
-
"version": "==1.4.
|
166 |
},
|
167 |
"idna": {
|
168 |
"hashes": [
|
@@ -173,18 +165,18 @@
|
|
173 |
},
|
174 |
"importlib-metadata": {
|
175 |
"hashes": [
|
176 |
-
"sha256:
|
177 |
-
"sha256:
|
178 |
],
|
179 |
-
"
|
|
|
180 |
},
|
181 |
-
"
|
182 |
"hashes": [
|
183 |
-
"sha256:
|
184 |
-
"sha256:
|
185 |
],
|
186 |
-
"
|
187 |
-
"version": "==1.0.2"
|
188 |
},
|
189 |
"mccabe": {
|
190 |
"hashes": [
|
@@ -195,11 +187,11 @@
|
|
195 |
},
|
196 |
"mock": {
|
197 |
"hashes": [
|
198 |
-
"sha256:
|
199 |
-
"sha256:
|
200 |
],
|
201 |
"index": "pypi",
|
202 |
-
"version": "==
|
203 |
},
|
204 |
"more-itertools": {
|
205 |
"hashes": [
|
@@ -212,24 +204,24 @@
|
|
212 |
},
|
213 |
"nodeenv": {
|
214 |
"hashes": [
|
215 |
-
"sha256:
|
216 |
],
|
217 |
-
"version": "==1.3.
|
218 |
},
|
219 |
-
"
|
220 |
"hashes": [
|
221 |
-
"sha256:
|
222 |
-
"sha256:
|
223 |
],
|
224 |
-
"
|
225 |
-
"version": "==2.3.3"
|
226 |
},
|
227 |
-
"
|
228 |
"hashes": [
|
229 |
-
"sha256:
|
230 |
-
"sha256:
|
231 |
],
|
232 |
-
"
|
|
|
233 |
},
|
234 |
"pkginfo": {
|
235 |
"hashes": [
|
@@ -240,25 +232,25 @@
|
|
240 |
},
|
241 |
"pluggy": {
|
242 |
"hashes": [
|
243 |
-
"sha256:
|
244 |
-
"sha256:
|
245 |
],
|
246 |
-
"version": "==0.
|
247 |
},
|
248 |
"pre-commit": {
|
249 |
"hashes": [
|
250 |
-
"sha256:
|
251 |
-
"sha256:
|
252 |
],
|
253 |
"index": "pypi",
|
254 |
-
"version": "==1.
|
255 |
},
|
256 |
"py": {
|
257 |
"hashes": [
|
258 |
-
"sha256:
|
259 |
-
"sha256:
|
260 |
],
|
261 |
-
"version": "==1.8.
|
262 |
},
|
263 |
"pycodestyle": {
|
264 |
"hashes": [
|
@@ -276,50 +268,57 @@
|
|
276 |
},
|
277 |
"pygments": {
|
278 |
"hashes": [
|
279 |
-
"sha256:
|
280 |
-
"sha256:
|
281 |
],
|
282 |
-
"version": "==2.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
283 |
},
|
284 |
"pytest": {
|
285 |
"hashes": [
|
286 |
-
"sha256:
|
287 |
-
"sha256:
|
288 |
],
|
289 |
"index": "pypi",
|
290 |
-
"version": "==
|
291 |
},
|
292 |
"pytest-cov": {
|
293 |
"hashes": [
|
294 |
-
"sha256:
|
295 |
-
"sha256:
|
296 |
],
|
297 |
"index": "pypi",
|
298 |
-
"version": "==2.
|
299 |
},
|
300 |
"pytest-mock": {
|
301 |
"hashes": [
|
302 |
-
"sha256:
|
303 |
-
"sha256:
|
304 |
],
|
305 |
"index": "pypi",
|
306 |
-
"version": "==
|
307 |
},
|
308 |
"pyyaml": {
|
309 |
"hashes": [
|
310 |
-
"sha256:
|
311 |
-
"sha256:
|
312 |
-
"sha256:
|
313 |
-
"sha256:
|
314 |
-
"sha256:
|
315 |
-
"sha256:
|
316 |
-
"sha256:
|
317 |
-
"sha256:
|
318 |
-
"sha256:
|
319 |
-
"sha256:
|
320 |
-
"sha256:
|
321 |
],
|
322 |
-
"version": "==5.
|
323 |
},
|
324 |
"readme-renderer": {
|
325 |
"hashes": [
|
@@ -330,10 +329,10 @@
|
|
330 |
},
|
331 |
"requests": {
|
332 |
"hashes": [
|
333 |
-
"sha256:
|
334 |
-
"sha256:
|
335 |
],
|
336 |
-
"version": "==2.
|
337 |
},
|
338 |
"requests-toolbelt": {
|
339 |
"hashes": [
|
@@ -361,10 +360,10 @@
|
|
361 |
},
|
362 |
"six": {
|
363 |
"hashes": [
|
364 |
-
"sha256:
|
365 |
-
"sha256:
|
366 |
],
|
367 |
-
"version": "==1.
|
368 |
},
|
369 |
"toml": {
|
370 |
"hashes": [
|
@@ -375,32 +374,39 @@
|
|
375 |
},
|
376 |
"tqdm": {
|
377 |
"hashes": [
|
378 |
-
"sha256:
|
379 |
-
"sha256:
|
380 |
],
|
381 |
-
"version": "==4.
|
382 |
},
|
383 |
"twine": {
|
384 |
"hashes": [
|
385 |
-
"sha256:
|
386 |
-
"sha256:
|
387 |
],
|
388 |
"index": "pypi",
|
389 |
-
"version": "==1.
|
390 |
},
|
391 |
"urllib3": {
|
392 |
"hashes": [
|
393 |
-
"sha256:
|
394 |
-
"sha256:
|
395 |
],
|
396 |
-
"version": "==1.
|
397 |
},
|
398 |
"virtualenv": {
|
399 |
"hashes": [
|
400 |
-
"sha256:
|
401 |
-
"sha256:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
402 |
],
|
403 |
-
"version": "==
|
404 |
},
|
405 |
"webencodings": {
|
406 |
"hashes": [
|
@@ -411,10 +417,10 @@
|
|
411 |
},
|
412 |
"zipp": {
|
413 |
"hashes": [
|
414 |
-
"sha256:
|
415 |
-
"sha256:
|
416 |
],
|
417 |
-
"version": "==0.
|
418 |
}
|
419 |
}
|
420 |
}
|
|
|
1 |
{
|
2 |
"_meta": {
|
3 |
"hash": {
|
4 |
+
"sha256": "c5544cbf0a2e670df097b55640123acd8cff56464512bc7b53d1692f2c1c0823"
|
5 |
},
|
6 |
"pipfile-spec": 6,
|
7 |
"requires": {
|
8 |
+
"python_version": "3.7"
|
9 |
},
|
10 |
"sources": [
|
11 |
{
|
|
|
19 |
"develop": {
|
20 |
"aspy.yaml": {
|
21 |
"hashes": [
|
22 |
+
"sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc",
|
23 |
+
"sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
],
|
25 |
"version": "==1.3.0"
|
26 |
},
|
27 |
"attrs": {
|
28 |
"hashes": [
|
29 |
+
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
30 |
+
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
|
31 |
],
|
32 |
+
"version": "==19.3.0"
|
33 |
},
|
34 |
"bleach": {
|
35 |
"hashes": [
|
|
|
48 |
},
|
49 |
"certifi": {
|
50 |
"hashes": [
|
51 |
+
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
|
52 |
+
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
|
53 |
],
|
54 |
+
"version": "==2019.11.28"
|
55 |
},
|
56 |
"cfgv": {
|
57 |
"hashes": [
|
58 |
+
"sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144",
|
59 |
+
"sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289"
|
60 |
],
|
61 |
+
"version": "==2.0.1"
|
62 |
},
|
63 |
"chardet": {
|
64 |
"hashes": [
|
|
|
69 |
},
|
70 |
"coverage": {
|
71 |
"hashes": [
|
72 |
+
"sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3",
|
73 |
+
"sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c",
|
74 |
+
"sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0",
|
75 |
+
"sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477",
|
76 |
+
"sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a",
|
77 |
+
"sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf",
|
78 |
+
"sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691",
|
79 |
+
"sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73",
|
80 |
+
"sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987",
|
81 |
+
"sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894",
|
82 |
+
"sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e",
|
83 |
+
"sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef",
|
84 |
+
"sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf",
|
85 |
+
"sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68",
|
86 |
+
"sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8",
|
87 |
+
"sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954",
|
88 |
+
"sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2",
|
89 |
+
"sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40",
|
90 |
+
"sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc",
|
91 |
+
"sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc",
|
92 |
+
"sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e",
|
93 |
+
"sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d",
|
94 |
+
"sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f",
|
95 |
+
"sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc",
|
96 |
+
"sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301",
|
97 |
+
"sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea",
|
98 |
+
"sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb",
|
99 |
+
"sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af",
|
100 |
+
"sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52",
|
101 |
+
"sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37",
|
102 |
+
"sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0"
|
103 |
+
],
|
104 |
+
"version": "==5.0.3"
|
105 |
},
|
106 |
"coveralls": {
|
107 |
"hashes": [
|
108 |
+
"sha256:2da39aeaef986757653f0a442ba2bef22a8ec602c8bacbc69d39f468dfae12ec",
|
109 |
+
"sha256:906e07a12b2ac04b8ad782d06173975fe5ff815fe9df3bfedd2c099bc5791aec"
|
110 |
],
|
111 |
"index": "pypi",
|
112 |
+
"version": "==1.10.0"
|
113 |
},
|
114 |
"docopt": {
|
115 |
"hashes": [
|
|
|
119 |
},
|
120 |
"docutils": {
|
121 |
"hashes": [
|
122 |
+
"sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af",
|
123 |
+
"sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"
|
|
|
124 |
],
|
125 |
+
"version": "==0.16"
|
126 |
},
|
127 |
"entrypoints": {
|
128 |
"hashes": [
|
|
|
143 |
},
|
144 |
"flake8": {
|
145 |
"hashes": [
|
146 |
+
"sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
|
147 |
+
"sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"
|
148 |
],
|
149 |
"index": "pypi",
|
150 |
+
"version": "==3.7.9"
|
151 |
},
|
152 |
"identify": {
|
153 |
"hashes": [
|
154 |
+
"sha256:418f3b2313ac0b531139311a6b426854e9cbdfcfb6175447a5039aa6291d8b30",
|
155 |
+
"sha256:8ad99ed1f3a965612dcb881435bf58abcfbeb05e230bb8c352b51e8eac103360"
|
156 |
],
|
157 |
+
"version": "==1.4.10"
|
158 |
},
|
159 |
"idna": {
|
160 |
"hashes": [
|
|
|
165 |
},
|
166 |
"importlib-metadata": {
|
167 |
"hashes": [
|
168 |
+
"sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
|
169 |
+
"sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"
|
170 |
],
|
171 |
+
"markers": "python_version < '3.8'",
|
172 |
+
"version": "==1.4.0"
|
173 |
},
|
174 |
+
"keyring": {
|
175 |
"hashes": [
|
176 |
+
"sha256:1f393f7466314068961c7e1d508120c092bd71fa54e3d93b76180b526d4abc56",
|
177 |
+
"sha256:24ae23ab2d6adc59138339e56843e33ec7b0a6b2f06302662477085c6c0aca00"
|
178 |
],
|
179 |
+
"version": "==21.1.0"
|
|
|
180 |
},
|
181 |
"mccabe": {
|
182 |
"hashes": [
|
|
|
187 |
},
|
188 |
"mock": {
|
189 |
"hashes": [
|
190 |
+
"sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3",
|
191 |
+
"sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8"
|
192 |
],
|
193 |
"index": "pypi",
|
194 |
+
"version": "==3.0.5"
|
195 |
},
|
196 |
"more-itertools": {
|
197 |
"hashes": [
|
|
|
204 |
},
|
205 |
"nodeenv": {
|
206 |
"hashes": [
|
207 |
+
"sha256:561057acd4ae3809e665a9aaaf214afff110bbb6a6d5c8a96121aea6878408b3"
|
208 |
],
|
209 |
+
"version": "==1.3.4"
|
210 |
},
|
211 |
+
"packaging": {
|
212 |
"hashes": [
|
213 |
+
"sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb",
|
214 |
+
"sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8"
|
215 |
],
|
216 |
+
"version": "==20.0"
|
|
|
217 |
},
|
218 |
+
"pathlib2": {
|
219 |
"hashes": [
|
220 |
+
"sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db",
|
221 |
+
"sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"
|
222 |
],
|
223 |
+
"index": "pypi",
|
224 |
+
"version": "==2.3.5"
|
225 |
},
|
226 |
"pkginfo": {
|
227 |
"hashes": [
|
|
|
232 |
},
|
233 |
"pluggy": {
|
234 |
"hashes": [
|
235 |
+
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
236 |
+
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
237 |
],
|
238 |
+
"version": "==0.13.1"
|
239 |
},
|
240 |
"pre-commit": {
|
241 |
"hashes": [
|
242 |
+
"sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850",
|
243 |
+
"sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029"
|
244 |
],
|
245 |
"index": "pypi",
|
246 |
+
"version": "==1.21.0"
|
247 |
},
|
248 |
"py": {
|
249 |
"hashes": [
|
250 |
+
"sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
|
251 |
+
"sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
|
252 |
],
|
253 |
+
"version": "==1.8.1"
|
254 |
},
|
255 |
"pycodestyle": {
|
256 |
"hashes": [
|
|
|
268 |
},
|
269 |
"pygments": {
|
270 |
"hashes": [
|
271 |
+
"sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b",
|
272 |
+
"sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"
|
273 |
],
|
274 |
+
"version": "==2.5.2"
|
275 |
+
},
|
276 |
+
"pyparsing": {
|
277 |
+
"hashes": [
|
278 |
+
"sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
|
279 |
+
"sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
|
280 |
+
],
|
281 |
+
"version": "==2.4.6"
|
282 |
},
|
283 |
"pytest": {
|
284 |
"hashes": [
|
285 |
+
"sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa",
|
286 |
+
"sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4"
|
287 |
],
|
288 |
"index": "pypi",
|
289 |
+
"version": "==5.3.2"
|
290 |
},
|
291 |
"pytest-cov": {
|
292 |
"hashes": [
|
293 |
+
"sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b",
|
294 |
+
"sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"
|
295 |
],
|
296 |
"index": "pypi",
|
297 |
+
"version": "==2.8.1"
|
298 |
},
|
299 |
"pytest-mock": {
|
300 |
"hashes": [
|
301 |
+
"sha256:b35eb281e93aafed138db25c8772b95d3756108b601947f89af503f8c629413f",
|
302 |
+
"sha256:cb67402d87d5f53c579263d37971a164743dc33c159dfb4fb4a86f37c5552307"
|
303 |
],
|
304 |
"index": "pypi",
|
305 |
+
"version": "==2.0.0"
|
306 |
},
|
307 |
"pyyaml": {
|
308 |
"hashes": [
|
309 |
+
"sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
|
310 |
+
"sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf",
|
311 |
+
"sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5",
|
312 |
+
"sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e",
|
313 |
+
"sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811",
|
314 |
+
"sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e",
|
315 |
+
"sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d",
|
316 |
+
"sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20",
|
317 |
+
"sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689",
|
318 |
+
"sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994",
|
319 |
+
"sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"
|
320 |
],
|
321 |
+
"version": "==5.3"
|
322 |
},
|
323 |
"readme-renderer": {
|
324 |
"hashes": [
|
|
|
329 |
},
|
330 |
"requests": {
|
331 |
"hashes": [
|
332 |
+
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
333 |
+
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
|
334 |
],
|
335 |
+
"version": "==2.22.0"
|
336 |
},
|
337 |
"requests-toolbelt": {
|
338 |
"hashes": [
|
|
|
360 |
},
|
361 |
"six": {
|
362 |
"hashes": [
|
363 |
+
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
|
364 |
+
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
|
365 |
],
|
366 |
+
"version": "==1.13.0"
|
367 |
},
|
368 |
"toml": {
|
369 |
"hashes": [
|
|
|
374 |
},
|
375 |
"tqdm": {
|
376 |
"hashes": [
|
377 |
+
"sha256:4789ccbb6fc122b5a6a85d512e4e41fc5acad77216533a6f2b8ce51e0f265c23",
|
378 |
+
"sha256:efab950cf7cc1e4d8ee50b2bb9c8e4a89f8307b49e0b2c9cfef3ec4ca26655eb"
|
379 |
],
|
380 |
+
"version": "==4.41.1"
|
381 |
},
|
382 |
"twine": {
|
383 |
"hashes": [
|
384 |
+
"sha256:c1af8ca391e43b0a06bbc155f7f67db0bf0d19d284bfc88d1675da497a946124",
|
385 |
+
"sha256:d561a5e511f70275e5a485a6275ff61851c16ffcb3a95a602189161112d9f160"
|
386 |
],
|
387 |
"index": "pypi",
|
388 |
+
"version": "==3.1.1"
|
389 |
},
|
390 |
"urllib3": {
|
391 |
"hashes": [
|
392 |
+
"sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
|
393 |
+
"sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
|
394 |
],
|
395 |
+
"version": "==1.25.7"
|
396 |
},
|
397 |
"virtualenv": {
|
398 |
"hashes": [
|
399 |
+
"sha256:0d62c70883c0342d59c11d0ddac0d954d0431321a41ab20851facf2b222598f3",
|
400 |
+
"sha256:55059a7a676e4e19498f1aad09b8313a38fcc0cdbe4fdddc0e9b06946d21b4bb"
|
401 |
+
],
|
402 |
+
"version": "==16.7.9"
|
403 |
+
},
|
404 |
+
"wcwidth": {
|
405 |
+
"hashes": [
|
406 |
+
"sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603",
|
407 |
+
"sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"
|
408 |
],
|
409 |
+
"version": "==0.1.8"
|
410 |
},
|
411 |
"webencodings": {
|
412 |
"hashes": [
|
|
|
417 |
},
|
418 |
"zipp": {
|
419 |
"hashes": [
|
420 |
+
"sha256:8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
|
421 |
+
"sha256:d38fbe01bbf7a3593a32bc35a9c4453c32bc42b98c377f9bff7e9f8da157786c"
|
422 |
],
|
423 |
+
"version": "==1.0.0"
|
424 |
}
|
425 |
}
|
426 |
}
|
pytube/__init__.py
CHANGED
@@ -4,11 +4,11 @@
|
|
4 |
"""
|
5 |
Pytube: a very serious Python library for downloading YouTube Videos.
|
6 |
"""
|
7 |
-
__title__ =
|
8 |
-
__version__ =
|
9 |
-
__author__ =
|
10 |
-
__license__ =
|
11 |
-
__copyright__ =
|
12 |
|
13 |
from pytube.logging import create_logger
|
14 |
from pytube.query import CaptionQuery
|
@@ -19,4 +19,4 @@ from pytube.contrib.playlist import Playlist
|
|
19 |
from pytube.__main__ import YouTube
|
20 |
|
21 |
logger = create_logger()
|
22 |
-
logger.info(
|
|
|
4 |
"""
|
5 |
Pytube: a very serious Python library for downloading YouTube Videos.
|
6 |
"""
|
7 |
+
__title__ = "pytube"
|
8 |
+
__version__ = "9.5.3"
|
9 |
+
__author__ = "Nick Ficano"
|
10 |
+
__license__ = "MIT License"
|
11 |
+
__copyright__ = "Copyright 2019 Nick Ficano"
|
12 |
|
13 |
from pytube.logging import create_logger
|
14 |
from pytube.query import CaptionQuery
|
|
|
19 |
from pytube.__main__ import YouTube
|
20 |
|
21 |
logger = create_logger()
|
22 |
+
logger.info("%s v%s", __title__, __version__)
|
pytube/__main__.py
CHANGED
@@ -12,6 +12,7 @@ from __future__ import absolute_import
|
|
12 |
import json
|
13 |
import logging
|
14 |
from urllib.parse import parse_qsl
|
|
|
15 |
|
16 |
from pytube import Caption
|
17 |
from pytube import CaptionQuery
|
@@ -31,8 +32,12 @@ class YouTube(object):
|
|
31 |
"""Core developer interface for pytube."""
|
32 |
|
33 |
def __init__(
|
34 |
-
self,
|
35 |
-
|
|
|
|
|
|
|
|
|
36 |
):
|
37 |
"""Construct a :class:`YouTube <YouTube>`.
|
38 |
|
@@ -48,16 +53,16 @@ class YouTube(object):
|
|
48 |
complete events.
|
49 |
|
50 |
"""
|
51 |
-
self.js = None
|
52 |
self.js_url = None # the url to the js, parsed from watch html
|
53 |
|
54 |
# note: vid_info may eventually be removed. It sounds like it once had
|
55 |
# additional formats, but that doesn't appear to still be the case.
|
56 |
|
57 |
-
self.vid_info = None
|
58 |
self.vid_info_url = None # the url to vid info, parsed from watch html
|
59 |
|
60 |
-
self.watch_html = None
|
61 |
self.embed_html = None
|
62 |
self.player_config_args = None # inline js in the html containing
|
63 |
# streams
|
@@ -77,8 +82,8 @@ class YouTube(object):
|
|
77 |
# (Borg pattern).
|
78 |
self.stream_monostate = {
|
79 |
# user defined callback functions.
|
80 |
-
|
81 |
-
|
82 |
}
|
83 |
|
84 |
if proxies:
|
@@ -107,34 +112,30 @@ class YouTube(object):
|
|
107 |
:rtype: None
|
108 |
|
109 |
"""
|
110 |
-
logger.info(
|
111 |
|
112 |
self.vid_info = {k: v for k, v in parse_qsl(self.vid_info)}
|
113 |
if self.age_restricted:
|
114 |
self.player_config_args = self.vid_info
|
115 |
else:
|
116 |
-
self.player_config_args = extract.get_ytplayer_config(
|
117 |
-
|
118 |
-
|
119 |
|
120 |
# Fix for KeyError: 'title' issue #434
|
121 |
-
if
|
122 |
-
i_start = (
|
123 |
-
|
124 |
-
.lower()
|
125 |
-
.index('<title>') + len('<title>')
|
126 |
-
)
|
127 |
-
i_end = self.watch_html.lower().index('</title>')
|
128 |
title = self.watch_html[i_start:i_end].strip()
|
129 |
-
index = title.lower().rfind(
|
130 |
title = title[:index] if index > 0 else title
|
131 |
-
self.player_config_args[
|
132 |
|
133 |
self.vid_descr = extract.get_vid_descr(self.watch_html)
|
134 |
# https://github.com/nficano/pytube/issues/165
|
135 |
-
stream_maps = [
|
136 |
-
if
|
137 |
-
stream_maps.append(
|
138 |
|
139 |
# unscramble the progressive and adaptive stream manifests.
|
140 |
for fmt in stream_maps:
|
@@ -145,9 +146,7 @@ class YouTube(object):
|
|
145 |
try:
|
146 |
mixins.apply_signature(self.player_config_args, fmt, self.js)
|
147 |
except TypeError:
|
148 |
-
self.js_url = extract.js_url(
|
149 |
-
self.embed_html, self.age_restricted,
|
150 |
-
)
|
151 |
self.js = request.get(self.js_url)
|
152 |
mixins.apply_signature(self.player_config_args, fmt, self.js)
|
153 |
|
@@ -155,10 +154,10 @@ class YouTube(object):
|
|
155 |
self.initialize_stream_objects(fmt)
|
156 |
|
157 |
# load the player_response object (contains subtitle information)
|
158 |
-
apply_mixin(self.player_config_args,
|
159 |
|
160 |
self.initialize_caption_objects()
|
161 |
-
logger.info(
|
162 |
|
163 |
def prefetch(self):
|
164 |
"""Eagerly download all necessary data.
|
@@ -172,7 +171,7 @@ class YouTube(object):
|
|
172 |
"""
|
173 |
self.watch_html = request.get(url=self.watch_url)
|
174 |
if '<img class="icon meh" src="/yts/img' not in self.watch_html:
|
175 |
-
raise VideoUnavailable(
|
176 |
self.embed_html = request.get(url=self.embed_url)
|
177 |
self.age_restricted = extract.is_age_restricted(self.watch_html)
|
178 |
self.vid_info_url = extract.video_info_url(
|
@@ -219,15 +218,14 @@ class YouTube(object):
|
|
219 |
:rtype: None
|
220 |
|
221 |
"""
|
222 |
-
if
|
223 |
return
|
224 |
# https://github.com/nficano/pytube/issues/167
|
225 |
caption_tracks = (
|
226 |
-
self.player_config_args
|
227 |
-
.get(
|
228 |
-
.get(
|
229 |
-
.get(
|
230 |
-
.get('captionTracks', [])
|
231 |
)
|
232 |
for caption_track in caption_tracks:
|
233 |
self.caption_tracks.append(Caption(caption_track))
|
@@ -255,7 +253,7 @@ class YouTube(object):
|
|
255 |
:rtype: str
|
256 |
|
257 |
"""
|
258 |
-
return self.player_config_args[
|
259 |
|
260 |
@property
|
261 |
def title(self):
|
@@ -264,7 +262,7 @@ class YouTube(object):
|
|
264 |
:rtype: str
|
265 |
|
266 |
"""
|
267 |
-
return self.player_config_args[
|
268 |
|
269 |
@property
|
270 |
def description(self):
|
@@ -283,10 +281,9 @@ class YouTube(object):
|
|
283 |
|
284 |
"""
|
285 |
return (
|
286 |
-
self.player_config_args
|
287 |
-
.get(
|
288 |
-
.get(
|
289 |
-
.get('averageRating')
|
290 |
)
|
291 |
|
292 |
@property
|
@@ -296,7 +293,7 @@ class YouTube(object):
|
|
296 |
:rtype: str
|
297 |
|
298 |
"""
|
299 |
-
return self.player_config_args[
|
300 |
|
301 |
@property
|
302 |
def views(self):
|
@@ -306,10 +303,9 @@ class YouTube(object):
|
|
306 |
|
307 |
"""
|
308 |
return (
|
309 |
-
self.player_config_args
|
310 |
-
.get(
|
311 |
-
.get(
|
312 |
-
.get('viewCount')
|
313 |
)
|
314 |
|
315 |
def register_on_progress_callback(self, func):
|
@@ -322,7 +318,7 @@ class YouTube(object):
|
|
322 |
:rtype: None
|
323 |
|
324 |
"""
|
325 |
-
self.stream_monostate[
|
326 |
|
327 |
def register_on_complete_callback(self, func):
|
328 |
"""Register a download complete callback function post initialization.
|
@@ -333,4 +329,4 @@ class YouTube(object):
|
|
333 |
:rtype: None
|
334 |
|
335 |
"""
|
336 |
-
self.stream_monostate[
|
|
|
12 |
import json
|
13 |
import logging
|
14 |
from urllib.parse import parse_qsl
|
15 |
+
from html import unescape
|
16 |
|
17 |
from pytube import Caption
|
18 |
from pytube import CaptionQuery
|
|
|
32 |
"""Core developer interface for pytube."""
|
33 |
|
34 |
def __init__(
|
35 |
+
self,
|
36 |
+
url=None,
|
37 |
+
defer_prefetch_init=False,
|
38 |
+
on_progress_callback=None,
|
39 |
+
on_complete_callback=None,
|
40 |
+
proxies=None,
|
41 |
):
|
42 |
"""Construct a :class:`YouTube <YouTube>`.
|
43 |
|
|
|
53 |
complete events.
|
54 |
|
55 |
"""
|
56 |
+
self.js = None # js fetched by js_url
|
57 |
self.js_url = None # the url to the js, parsed from watch html
|
58 |
|
59 |
# note: vid_info may eventually be removed. It sounds like it once had
|
60 |
# additional formats, but that doesn't appear to still be the case.
|
61 |
|
62 |
+
self.vid_info = None # content fetched by vid_info_url
|
63 |
self.vid_info_url = None # the url to vid info, parsed from watch html
|
64 |
|
65 |
+
self.watch_html = None # the html of /watch?v=<video_id>
|
66 |
self.embed_html = None
|
67 |
self.player_config_args = None # inline js in the html containing
|
68 |
# streams
|
|
|
82 |
# (Borg pattern).
|
83 |
self.stream_monostate = {
|
84 |
# user defined callback functions.
|
85 |
+
"on_progress": on_progress_callback,
|
86 |
+
"on_complete": on_complete_callback,
|
87 |
}
|
88 |
|
89 |
if proxies:
|
|
|
112 |
:rtype: None
|
113 |
|
114 |
"""
|
115 |
+
logger.info("init started")
|
116 |
|
117 |
self.vid_info = {k: v for k, v in parse_qsl(self.vid_info)}
|
118 |
if self.age_restricted:
|
119 |
self.player_config_args = self.vid_info
|
120 |
else:
|
121 |
+
self.player_config_args = extract.get_ytplayer_config(self.watch_html,)[
|
122 |
+
"args"
|
123 |
+
]
|
124 |
|
125 |
# Fix for KeyError: 'title' issue #434
|
126 |
+
if "title" not in self.player_config_args:
|
127 |
+
i_start = self.watch_html.lower().index("<title>") + len("<title>")
|
128 |
+
i_end = self.watch_html.lower().index("</title>")
|
|
|
|
|
|
|
|
|
129 |
title = self.watch_html[i_start:i_end].strip()
|
130 |
+
index = title.lower().rfind(" - youtube")
|
131 |
title = title[:index] if index > 0 else title
|
132 |
+
self.player_config_args["title"] = unescape(title)
|
133 |
|
134 |
self.vid_descr = extract.get_vid_descr(self.watch_html)
|
135 |
# https://github.com/nficano/pytube/issues/165
|
136 |
+
stream_maps = ["url_encoded_fmt_stream_map"]
|
137 |
+
if "adaptive_fmts" in self.player_config_args:
|
138 |
+
stream_maps.append("adaptive_fmts")
|
139 |
|
140 |
# unscramble the progressive and adaptive stream manifests.
|
141 |
for fmt in stream_maps:
|
|
|
146 |
try:
|
147 |
mixins.apply_signature(self.player_config_args, fmt, self.js)
|
148 |
except TypeError:
|
149 |
+
self.js_url = extract.js_url(self.embed_html, self.age_restricted,)
|
|
|
|
|
150 |
self.js = request.get(self.js_url)
|
151 |
mixins.apply_signature(self.player_config_args, fmt, self.js)
|
152 |
|
|
|
154 |
self.initialize_stream_objects(fmt)
|
155 |
|
156 |
# load the player_response object (contains subtitle information)
|
157 |
+
apply_mixin(self.player_config_args, "player_response", json.loads)
|
158 |
|
159 |
self.initialize_caption_objects()
|
160 |
+
logger.info("init finished successfully")
|
161 |
|
162 |
def prefetch(self):
|
163 |
"""Eagerly download all necessary data.
|
|
|
171 |
"""
|
172 |
self.watch_html = request.get(url=self.watch_url)
|
173 |
if '<img class="icon meh" src="/yts/img' not in self.watch_html:
|
174 |
+
raise VideoUnavailable("This video is unavailable.")
|
175 |
self.embed_html = request.get(url=self.embed_url)
|
176 |
self.age_restricted = extract.is_age_restricted(self.watch_html)
|
177 |
self.vid_info_url = extract.video_info_url(
|
|
|
218 |
:rtype: None
|
219 |
|
220 |
"""
|
221 |
+
if "captions" not in self.player_config_args["player_response"]:
|
222 |
return
|
223 |
# https://github.com/nficano/pytube/issues/167
|
224 |
caption_tracks = (
|
225 |
+
self.player_config_args.get("player_response", {})
|
226 |
+
.get("captions", {})
|
227 |
+
.get("playerCaptionsTracklistRenderer", {})
|
228 |
+
.get("captionTracks", [])
|
|
|
229 |
)
|
230 |
for caption_track in caption_tracks:
|
231 |
self.caption_tracks.append(Caption(caption_track))
|
|
|
253 |
:rtype: str
|
254 |
|
255 |
"""
|
256 |
+
return self.player_config_args["thumbnail_url"]
|
257 |
|
258 |
@property
|
259 |
def title(self):
|
|
|
262 |
:rtype: str
|
263 |
|
264 |
"""
|
265 |
+
return self.player_config_args["title"]
|
266 |
|
267 |
@property
|
268 |
def description(self):
|
|
|
281 |
|
282 |
"""
|
283 |
return (
|
284 |
+
self.player_config_args.get("player_response", {})
|
285 |
+
.get("videoDetails", {})
|
286 |
+
.get("averageRating")
|
|
|
287 |
)
|
288 |
|
289 |
@property
|
|
|
293 |
:rtype: str
|
294 |
|
295 |
"""
|
296 |
+
return self.player_config_args["length_seconds"]
|
297 |
|
298 |
@property
|
299 |
def views(self):
|
|
|
303 |
|
304 |
"""
|
305 |
return (
|
306 |
+
self.player_config_args.get("player_response", {})
|
307 |
+
.get("videoDetails", {})
|
308 |
+
.get("viewCount")
|
|
|
309 |
)
|
310 |
|
311 |
def register_on_progress_callback(self, func):
|
|
|
318 |
:rtype: None
|
319 |
|
320 |
"""
|
321 |
+
self.stream_monostate["on_progress"] = func
|
322 |
|
323 |
def register_on_complete_callback(self, func):
|
324 |
"""Register a download complete callback function post initialization.
|
|
|
329 |
:rtype: None
|
330 |
|
331 |
"""
|
332 |
+
self.stream_monostate["on_complete"] = func
|
pytube/captions.py
CHANGED
@@ -7,6 +7,7 @@ import xml.etree.ElementTree as ElementTree
|
|
7 |
from pytube import request
|
8 |
from html import unescape
|
9 |
|
|
|
10 |
class Caption:
|
11 |
"""Container for caption tracks."""
|
12 |
|
@@ -16,9 +17,9 @@ class Caption:
|
|
16 |
:param dict caption_track:
|
17 |
Caption track data extracted from ``watch_html``.
|
18 |
"""
|
19 |
-
self.url = caption_track.get(
|
20 |
-
self.name = caption_track[
|
21 |
-
self.code = caption_track[
|
22 |
|
23 |
@property
|
24 |
def xml_captions(self):
|
@@ -44,8 +45,8 @@ class Caption:
|
|
44 |
'00:00:03,890'
|
45 |
"""
|
46 |
frac, whole = math.modf(d)
|
47 |
-
time_fmt = time.strftime(
|
48 |
-
ms =
|
49 |
return time_fmt + ms
|
50 |
|
51 |
def xml_caption_to_srt(self, xml_captions):
|
@@ -57,27 +58,21 @@ class Caption:
|
|
57 |
segments = []
|
58 |
root = ElementTree.fromstring(xml_captions)
|
59 |
for i, child in enumerate(root.getchildren()):
|
60 |
-
text = child.text or
|
61 |
-
caption = unescape(
|
62 |
-
|
63 |
-
|
64 |
-
.replace(' ', ' '),
|
65 |
-
)
|
66 |
-
duration = float(child.attrib['dur'])
|
67 |
-
start = float(child.attrib['start'])
|
68 |
end = start + duration
|
69 |
sequence_number = i + 1 # convert from 0-indexed to 1.
|
70 |
-
line = (
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
text=caption,
|
76 |
-
)
|
77 |
)
|
78 |
segments.append(line)
|
79 |
-
return
|
80 |
|
81 |
def __repr__(self):
|
82 |
"""Printable object representation."""
|
83 |
-
return'<Caption lang="{s.name}" code="{s.code}">'.format(s=self)
|
|
|
7 |
from pytube import request
|
8 |
from html import unescape
|
9 |
|
10 |
+
|
11 |
class Caption:
|
12 |
"""Container for caption tracks."""
|
13 |
|
|
|
17 |
:param dict caption_track:
|
18 |
Caption track data extracted from ``watch_html``.
|
19 |
"""
|
20 |
+
self.url = caption_track.get("baseUrl")
|
21 |
+
self.name = caption_track["name"]["simpleText"]
|
22 |
+
self.code = caption_track["languageCode"]
|
23 |
|
24 |
@property
|
25 |
def xml_captions(self):
|
|
|
45 |
'00:00:03,890'
|
46 |
"""
|
47 |
frac, whole = math.modf(d)
|
48 |
+
time_fmt = time.strftime("%H:%M:%S,", time.gmtime(whole))
|
49 |
+
ms = "{:.3f}".format(frac).replace("0.", "")
|
50 |
return time_fmt + ms
|
51 |
|
52 |
def xml_caption_to_srt(self, xml_captions):
|
|
|
58 |
segments = []
|
59 |
root = ElementTree.fromstring(xml_captions)
|
60 |
for i, child in enumerate(root.getchildren()):
|
61 |
+
text = child.text or ""
|
62 |
+
caption = unescape(text.replace("\n", " ").replace(" ", " "),)
|
63 |
+
duration = float(child.attrib["dur"])
|
64 |
+
start = float(child.attrib["start"])
|
|
|
|
|
|
|
|
|
65 |
end = start + duration
|
66 |
sequence_number = i + 1 # convert from 0-indexed to 1.
|
67 |
+
line = "{seq}\n{start} --> {end}\n{text}\n".format(
|
68 |
+
seq=sequence_number,
|
69 |
+
start=self.float_to_srt_time_format(start),
|
70 |
+
end=self.float_to_srt_time_format(end),
|
71 |
+
text=caption,
|
|
|
|
|
72 |
)
|
73 |
segments.append(line)
|
74 |
+
return "\n".join(segments).strip()
|
75 |
|
76 |
def __repr__(self):
|
77 |
"""Printable object representation."""
|
78 |
+
return '<Caption lang="{s.name}" code="{s.code}">'.format(s=self)
|
pytube/cipher.py
CHANGED
@@ -37,20 +37,20 @@ def get_initial_function_name(js):
|
|
37 |
# c&&d.set("signature", EE(c));
|
38 |
|
39 |
pattern = [
|
40 |
-
r
|
41 |
-
r
|
42 |
r'(?P<sig>[a-zA-Z0-9$]+)\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)', # noqa: E501
|
43 |
r'(["\'])signature\1\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
|
44 |
-
r
|
45 |
-
r
|
46 |
-
r
|
47 |
-
r
|
48 |
-
r
|
49 |
-
r
|
50 |
-
r
|
51 |
]
|
52 |
|
53 |
-
logger.debug(
|
54 |
return regex_search(pattern, js, group=1)
|
55 |
|
56 |
|
@@ -76,9 +76,9 @@ def get_transform_plan(js):
|
|
76 |
'DE.kT(a,21)']
|
77 |
"""
|
78 |
name = re.escape(get_initial_function_name(js))
|
79 |
-
pattern = r
|
80 |
-
logger.debug(
|
81 |
-
return regex_search(pattern, js, group=1).split(
|
82 |
|
83 |
|
84 |
def get_transform_object(js, var):
|
@@ -103,12 +103,12 @@ def get_transform_object(js, var):
|
|
103 |
'kT:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c}']
|
104 |
|
105 |
"""
|
106 |
-
pattern = r
|
107 |
-
logger.debug(
|
108 |
return (
|
109 |
regex_search(pattern, js, group=1, flags=re.DOTALL)
|
110 |
-
.replace(
|
111 |
-
.split(
|
112 |
)
|
113 |
|
114 |
|
@@ -129,7 +129,7 @@ def get_transform_map(js, var):
|
|
129 |
mapper = {}
|
130 |
for obj in transform_object:
|
131 |
# AJ:function(a){a.reverse()} => AJ, function(a){a.reverse()}
|
132 |
-
name, function = obj.split(
|
133 |
fn = map_functions(function)
|
134 |
mapper[name] = fn
|
135 |
return mapper
|
@@ -169,7 +169,7 @@ def splice(arr, b):
|
|
169 |
>>> splice([1, 2, 3, 4], 2)
|
170 |
[1, 2]
|
171 |
"""
|
172 |
-
return arr[:b] + arr[b * 2:]
|
173 |
|
174 |
|
175 |
def swap(arr, b):
|
@@ -187,7 +187,7 @@ def swap(arr, b):
|
|
187 |
[3, 2, 1, 4]
|
188 |
"""
|
189 |
r = b % len(arr)
|
190 |
-
return list(chain([arr[r]], arr[1:r], [arr[0]], arr[r + 1:]))
|
191 |
|
192 |
|
193 |
def map_functions(js_func):
|
@@ -199,15 +199,15 @@ def map_functions(js_func):
|
|
199 |
"""
|
200 |
mapper = (
|
201 |
# function(a){a.reverse()}
|
202 |
-
(
|
203 |
# function(a,b){a.splice(0,b)}
|
204 |
-
(
|
205 |
# function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c}
|
206 |
-
(
|
207 |
# function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}
|
208 |
(
|
209 |
-
|
210 |
-
|
211 |
),
|
212 |
)
|
213 |
|
@@ -215,8 +215,7 @@ def map_functions(js_func):
|
|
215 |
if re.search(pattern, js_func):
|
216 |
return fn
|
217 |
raise RegexMatchError(
|
218 |
-
|
219 |
-
js_func,
|
220 |
)
|
221 |
|
222 |
|
@@ -238,8 +237,8 @@ def parse_function(js_func):
|
|
238 |
('AJ', 15)
|
239 |
|
240 |
"""
|
241 |
-
logger.debug(
|
242 |
-
return regex_search(r
|
243 |
|
244 |
|
245 |
def get_signature(js, ciphered_signature):
|
@@ -258,7 +257,7 @@ def get_signature(js, ciphered_signature):
|
|
258 |
"""
|
259 |
tplan = get_transform_plan(js)
|
260 |
# DE.AJ(a,15) => DE, AJ(a,15)
|
261 |
-
var, _ = tplan[0].split(
|
262 |
tmap = get_transform_map(js, var)
|
263 |
signature = [s for s in ciphered_signature]
|
264 |
|
@@ -266,13 +265,15 @@ def get_signature(js, ciphered_signature):
|
|
266 |
name, argument = parse_function(js_func)
|
267 |
signature = tmap[name](signature, int(argument))
|
268 |
logger.debug(
|
269 |
-
|
|
|
270 |
{
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
},
|
|
|
276 |
),
|
277 |
)
|
278 |
-
return
|
|
|
37 |
# c&&d.set("signature", EE(c));
|
38 |
|
39 |
pattern = [
|
40 |
+
r"\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(", # noqa: E501
|
41 |
+
r"\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(", # noqa: E501
|
42 |
r'(?P<sig>[a-zA-Z0-9$]+)\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)', # noqa: E501
|
43 |
r'(["\'])signature\1\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
|
44 |
+
r"\.sig\|\|(?P<sig>[a-zA-Z0-9$]+)\(",
|
45 |
+
r"yt\.akamaized\.net/\)\s*\|\|\s*.*?\s*[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?:encodeURIComponent\s*\()?\s*(?P<si$", # noqa: E501
|
46 |
+
r"\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(", # noqa: E501
|
47 |
+
r"\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(", # noqa: E501
|
48 |
+
r"\bc\s*&&\s*a\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(", # noqa: E501
|
49 |
+
r"\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(", # noqa: E501
|
50 |
+
r"\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(", # noqa: E501
|
51 |
]
|
52 |
|
53 |
+
logger.debug("finding initial function name")
|
54 |
return regex_search(pattern, js, group=1)
|
55 |
|
56 |
|
|
|
76 |
'DE.kT(a,21)']
|
77 |
"""
|
78 |
name = re.escape(get_initial_function_name(js))
|
79 |
+
pattern = r"%s=function\(\w\){[a-z=\.\(\"\)]*;(.*);(?:.+)}" % name
|
80 |
+
logger.debug("getting transform plan")
|
81 |
+
return regex_search(pattern, js, group=1).split(";")
|
82 |
|
83 |
|
84 |
def get_transform_object(js, var):
|
|
|
103 |
'kT:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c}']
|
104 |
|
105 |
"""
|
106 |
+
pattern = r"var %s={(.*?)};" % re.escape(var)
|
107 |
+
logger.debug("getting transform object")
|
108 |
return (
|
109 |
regex_search(pattern, js, group=1, flags=re.DOTALL)
|
110 |
+
.replace("\n", " ")
|
111 |
+
.split(", ")
|
112 |
)
|
113 |
|
114 |
|
|
|
129 |
mapper = {}
|
130 |
for obj in transform_object:
|
131 |
# AJ:function(a){a.reverse()} => AJ, function(a){a.reverse()}
|
132 |
+
name, function = obj.split(":", 1)
|
133 |
fn = map_functions(function)
|
134 |
mapper[name] = fn
|
135 |
return mapper
|
|
|
169 |
>>> splice([1, 2, 3, 4], 2)
|
170 |
[1, 2]
|
171 |
"""
|
172 |
+
return arr[:b] + arr[b * 2 :]
|
173 |
|
174 |
|
175 |
def swap(arr, b):
|
|
|
187 |
[3, 2, 1, 4]
|
188 |
"""
|
189 |
r = b % len(arr)
|
190 |
+
return list(chain([arr[r]], arr[1:r], [arr[0]], arr[r + 1 :]))
|
191 |
|
192 |
|
193 |
def map_functions(js_func):
|
|
|
199 |
"""
|
200 |
mapper = (
|
201 |
# function(a){a.reverse()}
|
202 |
+
("{\w\.reverse\(\)}", reverse),
|
203 |
# function(a,b){a.splice(0,b)}
|
204 |
+
("{\w\.splice\(0,\w\)}", splice),
|
205 |
# function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c}
|
206 |
+
("{var\s\w=\w\[0\];\w\[0\]=\w\[\w\%\w.length\];\w\[\w\]=\w}", swap),
|
207 |
# function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}
|
208 |
(
|
209 |
+
"{var\s\w=\w\[0\];\w\[0\]=\w\[\w\%\w.length\];" "\w\[\w\%\w.length\]=\w}",
|
210 |
+
swap,
|
211 |
),
|
212 |
)
|
213 |
|
|
|
215 |
if re.search(pattern, js_func):
|
216 |
return fn
|
217 |
raise RegexMatchError(
|
218 |
+
"could not find python equivalent function for: ", js_func,
|
|
|
219 |
)
|
220 |
|
221 |
|
|
|
237 |
('AJ', 15)
|
238 |
|
239 |
"""
|
240 |
+
logger.debug("parsing transform function")
|
241 |
+
return regex_search(r"\w+\.(\w+)\(\w,(\d+)\)", js_func, groups=True)
|
242 |
|
243 |
|
244 |
def get_signature(js, ciphered_signature):
|
|
|
257 |
"""
|
258 |
tplan = get_transform_plan(js)
|
259 |
# DE.AJ(a,15) => DE, AJ(a,15)
|
260 |
+
var, _ = tplan[0].split(".")
|
261 |
tmap = get_transform_map(js, var)
|
262 |
signature = [s for s in ciphered_signature]
|
263 |
|
|
|
265 |
name, argument = parse_function(js_func)
|
266 |
signature = tmap[name](signature, int(argument))
|
267 |
logger.debug(
|
268 |
+
"applied transform function\n%s",
|
269 |
+
pprint.pformat(
|
270 |
{
|
271 |
+
"output": "".join(signature),
|
272 |
+
"js_function": name,
|
273 |
+
"argument": int(argument),
|
274 |
+
"function": tmap[name],
|
275 |
+
},
|
276 |
+
indent=2,
|
277 |
),
|
278 |
)
|
279 |
+
return "".join(signature)
|
pytube/cli.py
CHANGED
@@ -21,30 +21,34 @@ logger = logging.getLogger(__name__)
|
|
21 |
def main():
|
22 |
"""Command line application to download youtube videos."""
|
23 |
parser = argparse.ArgumentParser(description=main.__doc__)
|
24 |
-
parser.add_argument(
|
25 |
parser.add_argument(
|
26 |
-
|
27 |
-
version='%(prog)s ' + __version__,
|
28 |
)
|
29 |
parser.add_argument(
|
30 |
-
|
31 |
-
'The itag for the desired stream'
|
32 |
-
),
|
33 |
)
|
34 |
parser.add_argument(
|
35 |
-
|
36 |
-
|
37 |
-
|
|
|
|
|
|
|
38 |
),
|
39 |
)
|
40 |
parser.add_argument(
|
41 |
-
|
42 |
-
|
|
|
|
|
|
|
|
|
43 |
)
|
44 |
parser.add_argument(
|
45 |
-
|
46 |
-
|
47 |
-
),
|
48 |
)
|
49 |
|
50 |
args = parser.parse_args()
|
@@ -73,33 +77,33 @@ def build_playback_report(url):
|
|
73 |
yt = YouTube(url)
|
74 |
ts = int(dt.datetime.utcnow().timestamp())
|
75 |
fp = os.path.join(
|
76 |
-
os.getcwd(),
|
77 |
-
'yt-video-{yt.video_id}-{ts}.json.gz'.format(yt=yt, ts=ts),
|
78 |
)
|
79 |
|
80 |
js = yt.js
|
81 |
watch_html = yt.watch_html
|
82 |
vid_info = yt.vid_info
|
83 |
|
84 |
-
with gzip.open(fp,
|
85 |
fh.write(
|
86 |
-
json.dumps(
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
|
|
93 |
)
|
94 |
|
95 |
|
96 |
def get_terminal_size():
|
97 |
"""Return the terminal size in rows and columns."""
|
98 |
-
rows, columns = os.popen(
|
99 |
return int(rows), int(columns)
|
100 |
|
101 |
|
102 |
-
def display_progress_bar(bytes_received, filesize, ch=
|
103 |
"""Display a simple, pretty progress bar.
|
104 |
|
105 |
Example:
|
@@ -123,9 +127,9 @@ def display_progress_bar(bytes_received, filesize, ch='█', scale=0.55):
|
|
123 |
|
124 |
filled = int(round(max_width * bytes_received / float(filesize)))
|
125 |
remaining = max_width - filled
|
126 |
-
bar = ch * filled +
|
127 |
percent = round(100.0 * bytes_received / float(filesize), 1)
|
128 |
-
text =
|
129 |
sys.stdout.write(text)
|
130 |
sys.stdout.flush()
|
131 |
|
@@ -161,13 +165,10 @@ def download(url, itag):
|
|
161 |
# TODO(nficano): allow dash itags to be selected
|
162 |
yt = YouTube(url, on_progress_callback=on_progress)
|
163 |
stream = yt.streams.get_by_itag(itag)
|
164 |
-
print(
|
165 |
-
fn=stream.default_filename,
|
166 |
-
fs=stream.filesize,
|
167 |
-
))
|
168 |
try:
|
169 |
stream.download()
|
170 |
-
sys.stdout.write(
|
171 |
except KeyboardInterrupt:
|
172 |
sys.exit()
|
173 |
|
@@ -184,5 +185,5 @@ def display_streams(url):
|
|
184 |
print(stream)
|
185 |
|
186 |
|
187 |
-
if __name__ ==
|
188 |
main()
|
|
|
21 |
def main():
|
22 |
"""Command line application to download youtube videos."""
|
23 |
parser = argparse.ArgumentParser(description=main.__doc__)
|
24 |
+
parser.add_argument("url", help="The YouTube /watch url", nargs="?")
|
25 |
parser.add_argument(
|
26 |
+
"--version", action="version", version="%(prog)s " + __version__,
|
|
|
27 |
)
|
28 |
parser.add_argument(
|
29 |
+
"--itag", type=int, help=("The itag for the desired stream"),
|
|
|
|
|
30 |
)
|
31 |
parser.add_argument(
|
32 |
+
"-l",
|
33 |
+
"--list",
|
34 |
+
action="store_true",
|
35 |
+
help=(
|
36 |
+
"The list option causes pytube cli to return a list of streams "
|
37 |
+
"available to download"
|
38 |
),
|
39 |
)
|
40 |
parser.add_argument(
|
41 |
+
"-v",
|
42 |
+
"--verbose",
|
43 |
+
action="count",
|
44 |
+
default=0,
|
45 |
+
dest="verbosity",
|
46 |
+
help="Verbosity level",
|
47 |
)
|
48 |
parser.add_argument(
|
49 |
+
"--build-playback-report",
|
50 |
+
action="store_true",
|
51 |
+
help=("Save the html and js to disk"),
|
52 |
)
|
53 |
|
54 |
args = parser.parse_args()
|
|
|
77 |
yt = YouTube(url)
|
78 |
ts = int(dt.datetime.utcnow().timestamp())
|
79 |
fp = os.path.join(
|
80 |
+
os.getcwd(), "yt-video-{yt.video_id}-{ts}.json.gz".format(yt=yt, ts=ts),
|
|
|
81 |
)
|
82 |
|
83 |
js = yt.js
|
84 |
watch_html = yt.watch_html
|
85 |
vid_info = yt.vid_info
|
86 |
|
87 |
+
with gzip.open(fp, "wb") as fh:
|
88 |
fh.write(
|
89 |
+
json.dumps(
|
90 |
+
{
|
91 |
+
"url": url,
|
92 |
+
"js": js,
|
93 |
+
"watch_html": watch_html,
|
94 |
+
"video_info": vid_info,
|
95 |
+
}
|
96 |
+
).encode("utf8"),
|
97 |
)
|
98 |
|
99 |
|
100 |
def get_terminal_size():
|
101 |
"""Return the terminal size in rows and columns."""
|
102 |
+
rows, columns = os.popen("stty size", "r").read().split()
|
103 |
return int(rows), int(columns)
|
104 |
|
105 |
|
106 |
+
def display_progress_bar(bytes_received, filesize, ch="█", scale=0.55):
|
107 |
"""Display a simple, pretty progress bar.
|
108 |
|
109 |
Example:
|
|
|
127 |
|
128 |
filled = int(round(max_width * bytes_received / float(filesize)))
|
129 |
remaining = max_width - filled
|
130 |
+
bar = ch * filled + " " * remaining
|
131 |
percent = round(100.0 * bytes_received / float(filesize), 1)
|
132 |
+
text = " ↳ |{bar}| {percent}%\r".format(bar=bar, percent=percent)
|
133 |
sys.stdout.write(text)
|
134 |
sys.stdout.flush()
|
135 |
|
|
|
165 |
# TODO(nficano): allow dash itags to be selected
|
166 |
yt = YouTube(url, on_progress_callback=on_progress)
|
167 |
stream = yt.streams.get_by_itag(itag)
|
168 |
+
print("\n{fn} | {fs} bytes".format(fn=stream.default_filename, fs=stream.filesize,))
|
|
|
|
|
|
|
169 |
try:
|
170 |
stream.download()
|
171 |
+
sys.stdout.write("\n")
|
172 |
except KeyboardInterrupt:
|
173 |
sys.exit()
|
174 |
|
|
|
185 |
print(stream)
|
186 |
|
187 |
|
188 |
+
if __name__ == "__main__":
|
189 |
main()
|
pytube/contrib/playlist.py
CHANGED
@@ -31,9 +31,9 @@ class Playlist(object):
|
|
31 |
:return: playlist url
|
32 |
"""
|
33 |
|
34 |
-
if
|
35 |
-
base_url =
|
36 |
-
playlist_code = self.playlist_url.split(
|
37 |
return base_url + playlist_code
|
38 |
|
39 |
# url is already in the desired format, so just return it
|
@@ -44,12 +44,13 @@ class Playlist(object):
|
|
44 |
and returns the "load more" url if found.
|
45 |
"""
|
46 |
try:
|
47 |
-
load_more_url =
|
48 |
-
r
|
49 |
-
'action_continuation=.*?)
|
|
|
50 |
).group(1)
|
51 |
except AttributeError:
|
52 |
-
load_more_url =
|
53 |
return load_more_url
|
54 |
|
55 |
def parse_links(self):
|
@@ -62,25 +63,22 @@ class Playlist(object):
|
|
62 |
req = request.get(url)
|
63 |
|
64 |
# split the page source by line and process each line
|
65 |
-
content = [x for x in req.split(
|
66 |
-
link_list = [x.split('href="', 1)[1].split(
|
67 |
|
68 |
# The above only returns 100 or fewer links
|
69 |
# Simulating a browser request for the load more link
|
70 |
load_more_url = self._load_more_url(req)
|
71 |
-
while len(load_more_url):
|
72 |
-
logger.debug(
|
73 |
req = request.get(load_more_url)
|
74 |
load_more = json.loads(req)
|
75 |
videos = re.findall(
|
76 |
-
r
|
77 |
-
load_more['content_html'],
|
78 |
)
|
79 |
# remove duplicates
|
80 |
link_list.extend(list(OrderedDict.fromkeys(videos)))
|
81 |
-
load_more_url = self._load_more_url(
|
82 |
-
load_more['load_more_widget_html'],
|
83 |
-
)
|
84 |
|
85 |
return link_list
|
86 |
|
@@ -91,7 +89,7 @@ class Playlist(object):
|
|
91 |
:return: urls -> string
|
92 |
"""
|
93 |
|
94 |
-
base_url =
|
95 |
link_list = self.parse_links()
|
96 |
|
97 |
for video_id in link_list:
|
@@ -117,10 +115,7 @@ class Playlist(object):
|
|
117 |
return (str(i).zfill(digits) for i in range(start, stop, step))
|
118 |
|
119 |
def download_all(
|
120 |
-
self,
|
121 |
-
download_path=None,
|
122 |
-
prefix_number=True,
|
123 |
-
reverse_numbering=False,
|
124 |
):
|
125 |
"""Download all the videos in the the playlist. Initially, download
|
126 |
resolution is 720p (or highest available), later more option
|
@@ -144,8 +139,8 @@ class Playlist(object):
|
|
144 |
"""
|
145 |
|
146 |
self.populate_video_urls()
|
147 |
-
logger.debug(
|
148 |
-
logger.debug(
|
149 |
|
150 |
prefix_gen = self._path_num_prefix_generator(reverse_numbering)
|
151 |
|
@@ -157,22 +152,25 @@ class Playlist(object):
|
|
157 |
if not self.suppress_exception:
|
158 |
raise e
|
159 |
else:
|
160 |
-
logger.debug(
|
161 |
else:
|
162 |
# TODO: this should not be hardcoded to a single user's
|
163 |
# preference
|
164 |
-
dl_stream =
|
165 |
-
progressive=True, subtype=
|
166 |
-
|
167 |
-
|
168 |
-
|
|
|
|
|
|
|
169 |
if prefix_number:
|
170 |
prefix = next(prefix_gen)
|
171 |
-
logger.debug(
|
172 |
dl_stream.download(download_path, filename_prefix=prefix)
|
173 |
else:
|
174 |
dl_stream.download(download_path)
|
175 |
-
logger.debug(
|
176 |
|
177 |
def title(self):
|
178 |
"""return playlist title (name)
|
@@ -180,13 +178,13 @@ class Playlist(object):
|
|
180 |
try:
|
181 |
url = self.construct_playlist_url()
|
182 |
req = request.get(url)
|
183 |
-
open_tag =
|
184 |
-
end_tag =
|
185 |
-
matchresult = re.compile(open_tag +
|
186 |
matchresult = matchresult.search(req).group()
|
187 |
-
matchresult = matchresult.replace(open_tag,
|
188 |
-
matchresult = matchresult.replace(end_tag,
|
189 |
-
matchresult = matchresult.replace(
|
190 |
matchresult = matchresult.strip()
|
191 |
|
192 |
return matchresult
|
|
|
31 |
:return: playlist url
|
32 |
"""
|
33 |
|
34 |
+
if "watch?v=" in self.playlist_url:
|
35 |
+
base_url = "https://www.youtube.com/playlist?list="
|
36 |
+
playlist_code = self.playlist_url.split("&list=")[1]
|
37 |
return base_url + playlist_code
|
38 |
|
39 |
# url is already in the desired format, so just return it
|
|
|
44 |
and returns the "load more" url if found.
|
45 |
"""
|
46 |
try:
|
47 |
+
load_more_url = "https://www.youtube.com" + re.search(
|
48 |
+
r"data-uix-load-more-href=\"(/browse_ajax\?"
|
49 |
+
'action_continuation=.*?)"',
|
50 |
+
req,
|
51 |
).group(1)
|
52 |
except AttributeError:
|
53 |
+
load_more_url = ""
|
54 |
return load_more_url
|
55 |
|
56 |
def parse_links(self):
|
|
|
63 |
req = request.get(url)
|
64 |
|
65 |
# split the page source by line and process each line
|
66 |
+
content = [x for x in req.split("\n") if "pl-video-title-link" in x]
|
67 |
+
link_list = [x.split('href="', 1)[1].split("&", 1)[0] for x in content]
|
68 |
|
69 |
# The above only returns 100 or fewer links
|
70 |
# Simulating a browser request for the load more link
|
71 |
load_more_url = self._load_more_url(req)
|
72 |
+
while len(load_more_url): # there is an url found
|
73 |
+
logger.debug("load more url: %s" % load_more_url)
|
74 |
req = request.get(load_more_url)
|
75 |
load_more = json.loads(req)
|
76 |
videos = re.findall(
|
77 |
+
r"href=\"(/watch\?v=[\w-]*)", load_more["content_html"],
|
|
|
78 |
)
|
79 |
# remove duplicates
|
80 |
link_list.extend(list(OrderedDict.fromkeys(videos)))
|
81 |
+
load_more_url = self._load_more_url(load_more["load_more_widget_html"],)
|
|
|
|
|
82 |
|
83 |
return link_list
|
84 |
|
|
|
89 |
:return: urls -> string
|
90 |
"""
|
91 |
|
92 |
+
base_url = "https://www.youtube.com"
|
93 |
link_list = self.parse_links()
|
94 |
|
95 |
for video_id in link_list:
|
|
|
115 |
return (str(i).zfill(digits) for i in range(start, stop, step))
|
116 |
|
117 |
def download_all(
|
118 |
+
self, download_path=None, prefix_number=True, reverse_numbering=False,
|
|
|
|
|
|
|
119 |
):
|
120 |
"""Download all the videos in the the playlist. Initially, download
|
121 |
resolution is 720p (or highest available), later more option
|
|
|
139 |
"""
|
140 |
|
141 |
self.populate_video_urls()
|
142 |
+
logger.debug("total videos found: %d", len(self.video_urls))
|
143 |
+
logger.debug("starting download")
|
144 |
|
145 |
prefix_gen = self._path_num_prefix_generator(reverse_numbering)
|
146 |
|
|
|
152 |
if not self.suppress_exception:
|
153 |
raise e
|
154 |
else:
|
155 |
+
logger.debug("Exception suppressed")
|
156 |
else:
|
157 |
# TODO: this should not be hardcoded to a single user's
|
158 |
# preference
|
159 |
+
dl_stream = (
|
160 |
+
yt.streams.filter(progressive=True, subtype="mp4",)
|
161 |
+
.order_by("resolution")
|
162 |
+
.desc()
|
163 |
+
.first()
|
164 |
+
)
|
165 |
+
|
166 |
+
logger.debug("download path: %s", download_path)
|
167 |
if prefix_number:
|
168 |
prefix = next(prefix_gen)
|
169 |
+
logger.debug("file prefix is: %s", prefix)
|
170 |
dl_stream.download(download_path, filename_prefix=prefix)
|
171 |
else:
|
172 |
dl_stream.download(download_path)
|
173 |
+
logger.debug("download complete")
|
174 |
|
175 |
def title(self):
|
176 |
"""return playlist title (name)
|
|
|
178 |
try:
|
179 |
url = self.construct_playlist_url()
|
180 |
req = request.get(url)
|
181 |
+
open_tag = "<title>"
|
182 |
+
end_tag = "</title>"
|
183 |
+
matchresult = re.compile(open_tag + "(.+?)" + end_tag)
|
184 |
matchresult = matchresult.search(req).group()
|
185 |
+
matchresult = matchresult.replace(open_tag, "")
|
186 |
+
matchresult = matchresult.replace(end_tag, "")
|
187 |
+
matchresult = matchresult.replace("- YouTube", "")
|
188 |
matchresult = matchresult.strip()
|
189 |
|
190 |
return matchresult
|
pytube/exceptions.py
CHANGED
@@ -24,7 +24,7 @@ class ExtractError(PytubeError):
|
|
24 |
A YouTube video identifier.
|
25 |
"""
|
26 |
if video_id is not None:
|
27 |
-
msg =
|
28 |
|
29 |
super(ExtractError, self).__init__(msg)
|
30 |
|
|
|
24 |
A YouTube video identifier.
|
25 |
"""
|
26 |
if video_id is not None:
|
27 |
+
msg = "{video_id}: {msg}".format(video_id=video_id, msg=msg)
|
28 |
|
29 |
super(ExtractError, self).__init__(msg)
|
30 |
|
pytube/extract.py
CHANGED
@@ -13,25 +13,25 @@ from pytube.helpers import regex_search
|
|
13 |
class PytubeHTMLParser(HTMLParser):
|
14 |
in_vid_descr = False
|
15 |
in_vid_descr_br = False
|
16 |
-
vid_descr =
|
17 |
|
18 |
def handle_starttag(self, tag, attrs):
|
19 |
-
if tag ==
|
20 |
for attr in attrs:
|
21 |
-
if attr[0] ==
|
22 |
self.in_vid_descr = True
|
23 |
|
24 |
def handle_endtag(self, tag):
|
25 |
-
if self.in_vid_descr and tag ==
|
26 |
self.in_vid_descr = False
|
27 |
|
28 |
def handle_startendtag(self, tag, attrs):
|
29 |
-
if self.in_vid_descr and tag ==
|
30 |
self.in_vid_descr_br = True
|
31 |
|
32 |
def handle_data(self, data):
|
33 |
if self.in_vid_descr_br:
|
34 |
-
self.vid_descr +=
|
35 |
self.in_vid_descr_br = False
|
36 |
elif self.in_vid_descr:
|
37 |
self.vid_descr += data
|
@@ -47,7 +47,7 @@ def is_age_restricted(watch_html):
|
|
47 |
Whether or not the content is age restricted.
|
48 |
"""
|
49 |
try:
|
50 |
-
regex_search(r
|
51 |
except RegexMatchError:
|
52 |
return False
|
53 |
return True
|
@@ -68,7 +68,7 @@ def video_id(url):
|
|
68 |
:returns:
|
69 |
YouTube video id.
|
70 |
"""
|
71 |
-
return regex_search(r
|
72 |
|
73 |
|
74 |
def watch_url(video_id):
|
@@ -80,20 +80,19 @@ def watch_url(video_id):
|
|
80 |
:returns:
|
81 |
Sanitized YouTube watch url.
|
82 |
"""
|
83 |
-
return
|
84 |
|
85 |
|
86 |
def embed_url(video_id):
|
87 |
-
return
|
88 |
|
89 |
|
90 |
def eurl(video_id):
|
91 |
-
return
|
92 |
|
93 |
|
94 |
def video_info_url(
|
95 |
-
video_id, watch_url, watch_html, embed_html,
|
96 |
-
age_restricted,
|
97 |
):
|
98 |
"""Construct the video_info url.
|
99 |
|
@@ -116,20 +115,20 @@ def video_info_url(
|
|
116 |
sts = regex_search(r'"sts"\s*:\s*(\d+)', embed_html, group=1)
|
117 |
# Here we use ``OrderedDict`` so that the output is consistent between
|
118 |
# Python 2.7+.
|
119 |
-
params = OrderedDict(
|
120 |
-
(
|
121 |
-
|
122 |
-
('sts', sts),
|
123 |
-
])
|
124 |
else:
|
125 |
-
params = OrderedDict(
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
|
|
|
|
133 |
|
134 |
|
135 |
def js_url(html, age_restricted=False):
|
@@ -145,8 +144,8 @@ def js_url(html, age_restricted=False):
|
|
145 |
|
146 |
"""
|
147 |
ytplayer_config = get_ytplayer_config(html, age_restricted)
|
148 |
-
base_js = ytplayer_config[
|
149 |
-
return
|
150 |
|
151 |
|
152 |
def mime_type_codec(mime_type_codec):
|
@@ -168,9 +167,9 @@ def mime_type_codec(mime_type_codec):
|
|
168 |
The mime type and a list of codecs.
|
169 |
|
170 |
"""
|
171 |
-
pattern = r
|
172 |
mime_type, codecs = regex_search(pattern, mime_type_codec, groups=True)
|
173 |
-
return mime_type, [c.strip() for c in codecs.split(
|
174 |
|
175 |
|
176 |
def get_ytplayer_config(html, age_restricted=False):
|
@@ -191,7 +190,7 @@ def get_ytplayer_config(html, age_restricted=False):
|
|
191 |
if age_restricted:
|
192 |
pattern = r";yt\.setConfig\(\{'PLAYER_CONFIG':\s*({.*})(,'EXPERIMENT_FLAGS'|;)" # noqa: E501
|
193 |
else:
|
194 |
-
pattern = r
|
195 |
yt_player_config = regex_search(pattern, html, group=1)
|
196 |
return json.loads(yt_player_config)
|
197 |
|
|
|
13 |
class PytubeHTMLParser(HTMLParser):
|
14 |
in_vid_descr = False
|
15 |
in_vid_descr_br = False
|
16 |
+
vid_descr = ""
|
17 |
|
18 |
def handle_starttag(self, tag, attrs):
|
19 |
+
if tag == "p":
|
20 |
for attr in attrs:
|
21 |
+
if attr[0] == "id" and attr[1] == "eow-description":
|
22 |
self.in_vid_descr = True
|
23 |
|
24 |
def handle_endtag(self, tag):
|
25 |
+
if self.in_vid_descr and tag == "p":
|
26 |
self.in_vid_descr = False
|
27 |
|
28 |
def handle_startendtag(self, tag, attrs):
|
29 |
+
if self.in_vid_descr and tag == "br":
|
30 |
self.in_vid_descr_br = True
|
31 |
|
32 |
def handle_data(self, data):
|
33 |
if self.in_vid_descr_br:
|
34 |
+
self.vid_descr += "\n{}".format(data)
|
35 |
self.in_vid_descr_br = False
|
36 |
elif self.in_vid_descr:
|
37 |
self.vid_descr += data
|
|
|
47 |
Whether or not the content is age restricted.
|
48 |
"""
|
49 |
try:
|
50 |
+
regex_search(r"og:restrictions:age", watch_html, group=0)
|
51 |
except RegexMatchError:
|
52 |
return False
|
53 |
return True
|
|
|
68 |
:returns:
|
69 |
YouTube video id.
|
70 |
"""
|
71 |
+
return regex_search(r"(?:v=|\/)([0-9A-Za-z_-]{11}).*", url, group=1)
|
72 |
|
73 |
|
74 |
def watch_url(video_id):
|
|
|
80 |
:returns:
|
81 |
Sanitized YouTube watch url.
|
82 |
"""
|
83 |
+
return "https://youtube.com/watch?v=" + video_id
|
84 |
|
85 |
|
86 |
def embed_url(video_id):
|
87 |
+
return "https://www.youtube.com/embed/{}".format(video_id)
|
88 |
|
89 |
|
90 |
def eurl(video_id):
|
91 |
+
return "https://youtube.googleapis.com/v/{}".format(video_id)
|
92 |
|
93 |
|
94 |
def video_info_url(
|
95 |
+
video_id, watch_url, watch_html, embed_html, age_restricted,
|
|
|
96 |
):
|
97 |
"""Construct the video_info url.
|
98 |
|
|
|
115 |
sts = regex_search(r'"sts"\s*:\s*(\d+)', embed_html, group=1)
|
116 |
# Here we use ``OrderedDict`` so that the output is consistent between
|
117 |
# Python 2.7+.
|
118 |
+
params = OrderedDict(
|
119 |
+
[("video_id", video_id), ("eurl", eurl(video_id)), ("sts", sts),]
|
120 |
+
)
|
|
|
|
|
121 |
else:
|
122 |
+
params = OrderedDict(
|
123 |
+
[
|
124 |
+
("video_id", video_id),
|
125 |
+
("el", "$el"),
|
126 |
+
("ps", "default"),
|
127 |
+
("eurl", quote(watch_url)),
|
128 |
+
("hl", "en_US"),
|
129 |
+
]
|
130 |
+
)
|
131 |
+
return "https://youtube.com/get_video_info?" + urlencode(params)
|
132 |
|
133 |
|
134 |
def js_url(html, age_restricted=False):
|
|
|
144 |
|
145 |
"""
|
146 |
ytplayer_config = get_ytplayer_config(html, age_restricted)
|
147 |
+
base_js = ytplayer_config["assets"]["js"]
|
148 |
+
return "https://youtube.com" + base_js
|
149 |
|
150 |
|
151 |
def mime_type_codec(mime_type_codec):
|
|
|
167 |
The mime type and a list of codecs.
|
168 |
|
169 |
"""
|
170 |
+
pattern = r"(\w+\/\w+)\;\scodecs=\"([a-zA-Z-0-9.,\s]*)\""
|
171 |
mime_type, codecs = regex_search(pattern, mime_type_codec, groups=True)
|
172 |
+
return mime_type, [c.strip() for c in codecs.split(",")]
|
173 |
|
174 |
|
175 |
def get_ytplayer_config(html, age_restricted=False):
|
|
|
190 |
if age_restricted:
|
191 |
pattern = r";yt\.setConfig\(\{'PLAYER_CONFIG':\s*({.*})(,'EXPERIMENT_FLAGS'|;)" # noqa: E501
|
192 |
else:
|
193 |
+
pattern = r";ytplayer\.config\s*=\s*({.*?});"
|
194 |
yt_player_config = regex_search(pattern, html, group=1)
|
195 |
return json.loads(yt_player_config)
|
196 |
|
pytube/helpers.py
CHANGED
@@ -36,17 +36,13 @@ def regex_search(pattern, string, groups=False, group=None, flags=0):
|
|
36 |
results = regex.search(string)
|
37 |
if not results:
|
38 |
raise RegexMatchError(
|
39 |
-
|
40 |
-
.format(pattern=p),
|
41 |
)
|
42 |
else:
|
43 |
logger.debug(
|
44 |
-
|
45 |
pprint.pformat(
|
46 |
-
{
|
47 |
-
'pattern': p,
|
48 |
-
'results': results.group(0),
|
49 |
-
}, indent=2,
|
50 |
),
|
51 |
)
|
52 |
if groups:
|
@@ -60,17 +56,13 @@ def regex_search(pattern, string, groups=False, group=None, flags=0):
|
|
60 |
results = regex.search(string)
|
61 |
if not results:
|
62 |
raise RegexMatchError(
|
63 |
-
|
64 |
-
.format(pattern=pattern),
|
65 |
)
|
66 |
else:
|
67 |
logger.debug(
|
68 |
-
|
69 |
pprint.pformat(
|
70 |
-
{
|
71 |
-
'pattern': pattern,
|
72 |
-
'results': results.group(0),
|
73 |
-
}, indent=2,
|
74 |
),
|
75 |
)
|
76 |
if groups:
|
@@ -117,10 +109,28 @@ def safe_filename(s, max_length=255):
|
|
117 |
# Characters in range 0-31 (0x00-0x1F) are not allowed in ntfs filenames.
|
118 |
ntfs_chrs = [chr(i) for i in range(0, 31)]
|
119 |
chrs = [
|
120 |
-
'
|
121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
]
|
123 |
-
pattern =
|
124 |
regex = re.compile(pattern, re.UNICODE)
|
125 |
-
filename = regex.sub(
|
126 |
-
return filename[:max_length].rsplit(
|
|
|
36 |
results = regex.search(string)
|
37 |
if not results:
|
38 |
raise RegexMatchError(
|
39 |
+
"regex pattern ({pattern}) had zero matches".format(pattern=p),
|
|
|
40 |
)
|
41 |
else:
|
42 |
logger.debug(
|
43 |
+
"finished regex search: %s",
|
44 |
pprint.pformat(
|
45 |
+
{"pattern": p, "results": results.group(0),}, indent=2,
|
|
|
|
|
|
|
46 |
),
|
47 |
)
|
48 |
if groups:
|
|
|
56 |
results = regex.search(string)
|
57 |
if not results:
|
58 |
raise RegexMatchError(
|
59 |
+
"regex pattern ({pattern}) had zero matches".format(pattern=pattern),
|
|
|
60 |
)
|
61 |
else:
|
62 |
logger.debug(
|
63 |
+
"finished regex search: %s",
|
64 |
pprint.pformat(
|
65 |
+
{"pattern": pattern, "results": results.group(0),}, indent=2,
|
|
|
|
|
|
|
66 |
),
|
67 |
)
|
68 |
if groups:
|
|
|
109 |
# Characters in range 0-31 (0x00-0x1F) are not allowed in ntfs filenames.
|
110 |
ntfs_chrs = [chr(i) for i in range(0, 31)]
|
111 |
chrs = [
|
112 |
+
'"',
|
113 |
+
"\#",
|
114 |
+
"\$",
|
115 |
+
"\%",
|
116 |
+
"'",
|
117 |
+
"\*",
|
118 |
+
"\,",
|
119 |
+
"\.",
|
120 |
+
"\/",
|
121 |
+
"\:",
|
122 |
+
'"',
|
123 |
+
"\;",
|
124 |
+
"\<",
|
125 |
+
"\>",
|
126 |
+
"\?",
|
127 |
+
"\\",
|
128 |
+
"\^",
|
129 |
+
"\|",
|
130 |
+
"\~",
|
131 |
+
"\\\\",
|
132 |
]
|
133 |
+
pattern = "|".join(ntfs_chrs + chrs)
|
134 |
regex = re.compile(pattern, re.UNICODE)
|
135 |
+
filename = regex.sub("", s)
|
136 |
+
return filename[:max_length].rsplit(" ", 0)[0]
|
pytube/itags.py
CHANGED
@@ -2,91 +2,89 @@
|
|
2 |
"""This module contains a lookup table of YouTube's itag values."""
|
3 |
|
4 |
ITAGS = {
|
5 |
-
5: (
|
6 |
-
6: (
|
7 |
-
13: (
|
8 |
-
17: (
|
9 |
-
18: (
|
10 |
-
22: (
|
11 |
-
34: (
|
12 |
-
35: (
|
13 |
-
36: (
|
14 |
-
37: (
|
15 |
-
38: (
|
16 |
-
43: (
|
17 |
-
44: (
|
18 |
-
45: (
|
19 |
-
46: (
|
20 |
-
59: (
|
21 |
-
78: (
|
22 |
-
82: (
|
23 |
-
83: (
|
24 |
-
84: (
|
25 |
-
85: (
|
26 |
-
91: (
|
27 |
-
92: (
|
28 |
-
93: (
|
29 |
-
94: (
|
30 |
-
95: (
|
31 |
-
96: (
|
32 |
-
100: (
|
33 |
-
101: (
|
34 |
-
102: (
|
35 |
-
132: (
|
36 |
-
151: (
|
37 |
-
|
38 |
# DASH Video
|
39 |
-
133: (
|
40 |
-
134: (
|
41 |
-
135: (
|
42 |
-
136: (
|
43 |
-
137: (
|
44 |
-
138: (
|
45 |
-
160: (
|
46 |
-
167: (
|
47 |
-
168: (
|
48 |
-
169: (
|
49 |
-
170: (
|
50 |
-
212: (
|
51 |
-
218: (
|
52 |
-
219: (
|
53 |
-
242: (
|
54 |
-
243: (
|
55 |
-
244: (
|
56 |
-
245: (
|
57 |
-
246: (
|
58 |
-
247: (
|
59 |
-
248: (
|
60 |
-
264: (
|
61 |
-
266: (
|
62 |
-
271: (
|
63 |
-
272: (
|
64 |
-
278: (
|
65 |
-
298: (
|
66 |
-
299: (
|
67 |
-
302: (
|
68 |
-
303: (
|
69 |
-
308: (
|
70 |
-
313: (
|
71 |
-
315: (
|
72 |
-
330: (
|
73 |
-
331: (
|
74 |
-
332: (
|
75 |
-
333: (
|
76 |
-
334: (
|
77 |
-
335: (
|
78 |
-
336: (
|
79 |
-
337: (
|
80 |
-
|
81 |
# DASH Audio
|
82 |
-
139: (None,
|
83 |
-
140: (None,
|
84 |
-
141: (None,
|
85 |
-
171: (None,
|
86 |
-
172: (None,
|
87 |
-
249: (None,
|
88 |
-
250: (None,
|
89 |
-
251: (None,
|
90 |
256: (None, None),
|
91 |
258: (None, None),
|
92 |
325: (None, None),
|
@@ -111,10 +109,10 @@ def get_format_profile(itag):
|
|
111 |
else:
|
112 |
res, bitrate = None, None
|
113 |
return {
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
}
|
|
|
2 |
"""This module contains a lookup table of YouTube's itag values."""
|
3 |
|
4 |
ITAGS = {
|
5 |
+
5: ("240p", "64kbps"),
|
6 |
+
6: ("270p", "64kbps"),
|
7 |
+
13: ("144p", None),
|
8 |
+
17: ("144p", "24kbps"),
|
9 |
+
18: ("360p", "96kbps"),
|
10 |
+
22: ("720p", "192kbps"),
|
11 |
+
34: ("360p", "128kbps"),
|
12 |
+
35: ("480p", "128kbps"),
|
13 |
+
36: ("240p", None),
|
14 |
+
37: ("1080p", "192kbps"),
|
15 |
+
38: ("3072p", "192kbps"),
|
16 |
+
43: ("360p", "128kbps"),
|
17 |
+
44: ("480p", "128kbps"),
|
18 |
+
45: ("720p", "192kbps"),
|
19 |
+
46: ("1080p", "192kbps"),
|
20 |
+
59: ("480p", "128kbps"),
|
21 |
+
78: ("480p", "128kbps"),
|
22 |
+
82: ("360p", "128kbps"),
|
23 |
+
83: ("480p", "128kbps"),
|
24 |
+
84: ("720p", "192kbps"),
|
25 |
+
85: ("1080p", "192kbps"),
|
26 |
+
91: ("144p", "48kbps"),
|
27 |
+
92: ("240p", "48kbps"),
|
28 |
+
93: ("360p", "128kbps"),
|
29 |
+
94: ("480p", "128kbps"),
|
30 |
+
95: ("720p", "256kbps"),
|
31 |
+
96: ("1080p", "256kbps"),
|
32 |
+
100: ("360p", "128kbps"),
|
33 |
+
101: ("480p", "192kbps"),
|
34 |
+
102: ("720p", "192kbps"),
|
35 |
+
132: ("240p", "48kbps"),
|
36 |
+
151: ("720p", "24kbps"),
|
|
|
37 |
# DASH Video
|
38 |
+
133: ("240p", None),
|
39 |
+
134: ("360p", None),
|
40 |
+
135: ("480p", None),
|
41 |
+
136: ("720p", None),
|
42 |
+
137: ("1080p", None),
|
43 |
+
138: ("2160p", None),
|
44 |
+
160: ("144p", None),
|
45 |
+
167: ("360p", None),
|
46 |
+
168: ("480p", None),
|
47 |
+
169: ("720p", None),
|
48 |
+
170: ("1080p", None),
|
49 |
+
212: ("480p", None),
|
50 |
+
218: ("480p", None),
|
51 |
+
219: ("480p", None),
|
52 |
+
242: ("240p", None),
|
53 |
+
243: ("360p", None),
|
54 |
+
244: ("480p", None),
|
55 |
+
245: ("480p", None),
|
56 |
+
246: ("480p", None),
|
57 |
+
247: ("720p", None),
|
58 |
+
248: ("1080p", None),
|
59 |
+
264: ("1440p", None),
|
60 |
+
266: ("2160p", None),
|
61 |
+
271: ("1440p", None),
|
62 |
+
272: ("2160p", None),
|
63 |
+
278: ("144p", None),
|
64 |
+
298: ("720p", None),
|
65 |
+
299: ("1080p", None),
|
66 |
+
302: ("720p", None),
|
67 |
+
303: ("1080p", None),
|
68 |
+
308: ("1440p", None),
|
69 |
+
313: ("2160p", None),
|
70 |
+
315: ("2160p", None),
|
71 |
+
330: ("144p", None),
|
72 |
+
331: ("240p", None),
|
73 |
+
332: ("360p", None),
|
74 |
+
333: ("480p", None),
|
75 |
+
334: ("720p", None),
|
76 |
+
335: ("1080p", None),
|
77 |
+
336: ("1440p", None),
|
78 |
+
337: ("2160p", None),
|
|
|
79 |
# DASH Audio
|
80 |
+
139: (None, "48kbps"),
|
81 |
+
140: (None, "128kbps"),
|
82 |
+
141: (None, "256kbps"),
|
83 |
+
171: (None, "128kbps"),
|
84 |
+
172: (None, "256kbps"),
|
85 |
+
249: (None, "50kbps"),
|
86 |
+
250: (None, "70kbps"),
|
87 |
+
251: (None, "160kbps"),
|
88 |
256: (None, None),
|
89 |
258: (None, None),
|
90 |
325: (None, None),
|
|
|
109 |
else:
|
110 |
res, bitrate = None, None
|
111 |
return {
|
112 |
+
"resolution": res,
|
113 |
+
"abr": bitrate,
|
114 |
+
"is_live": itag in LIVE,
|
115 |
+
"is_3d": itag in _3D,
|
116 |
+
"is_hdr": itag in HDR,
|
117 |
+
"fps": 60 if itag in _60FPS else 30,
|
118 |
}
|
pytube/logging.py
CHANGED
@@ -11,15 +11,15 @@ def create_logger(level=logging.ERROR):
|
|
11 |
:param int level:
|
12 |
Describe the severity level of the logs to handle.
|
13 |
"""
|
14 |
-
fmt =
|
15 |
-
date_fmt =
|
16 |
formatter = logging.Formatter(fmt, datefmt=date_fmt)
|
17 |
|
18 |
handler = logging.StreamHandler()
|
19 |
handler.setFormatter(formatter)
|
20 |
|
21 |
# https://github.com/nficano/pytube/issues/163
|
22 |
-
logger = logging.getLogger(
|
23 |
logger.addHandler(handler)
|
24 |
logger.setLevel(level)
|
25 |
return logger
|
|
|
11 |
:param int level:
|
12 |
Describe the severity level of the logs to handle.
|
13 |
"""
|
14 |
+
fmt = "[%(asctime)s] %(levelname)s in %(module)s: %(message)s"
|
15 |
+
date_fmt = "%H:%M:%S"
|
16 |
formatter = logging.Formatter(fmt, datefmt=date_fmt)
|
17 |
|
18 |
handler = logging.StreamHandler()
|
19 |
handler.setFormatter(formatter)
|
20 |
|
21 |
# https://github.com/nficano/pytube/issues/163
|
22 |
+
logger = logging.getLogger("pytube")
|
23 |
logger.addHandler(handler)
|
24 |
logger.setLevel(level)
|
25 |
return logger
|
pytube/mixins.py
CHANGED
@@ -31,46 +31,41 @@ def apply_signature(config_args, fmt, js):
|
|
31 |
|
32 |
"""
|
33 |
stream_manifest = config_args[fmt]
|
34 |
-
live_stream =
|
35 |
-
|
36 |
-
|
|
|
|
|
37 |
for i, stream in enumerate(stream_manifest):
|
38 |
-
if
|
39 |
-
url = stream[
|
40 |
elif live_stream:
|
41 |
-
raise LiveStreamError(
|
42 |
# 403 Forbidden fix.
|
43 |
-
if (
|
44 |
-
|
45 |
-
's' not in stream and (
|
46 |
-
'&sig=' in url or '&lsig=' in url
|
47 |
-
)
|
48 |
-
)
|
49 |
):
|
50 |
# For certain videos, YouTube will just provide them pre-signed, in
|
51 |
# which case there's no real magic to download them and we can skip
|
52 |
# the whole signature descrambling entirely.
|
53 |
-
logger.debug(
|
54 |
continue
|
55 |
|
56 |
if js is not None:
|
57 |
-
signature = cipher.get_signature(js, stream[
|
58 |
else:
|
59 |
# signature not present in url (line 33), need js to descramble
|
60 |
# TypeError caught in __main__
|
61 |
-
raise TypeError(
|
62 |
|
63 |
logger.debug(
|
64 |
-
|
65 |
-
stream[
|
66 |
-
|
67 |
-
's': stream['s'],
|
68 |
-
'signature': signature,
|
69 |
-
}, indent=2,
|
70 |
-
),
|
71 |
)
|
72 |
# 403 forbidden fix
|
73 |
-
stream_manifest[i][
|
|
|
74 |
|
75 |
def apply_descrambler(stream_data, key):
|
76 |
"""Apply various in-place transforms to YouTube's media stream data.
|
@@ -92,33 +87,49 @@ def apply_descrambler(stream_data, key):
|
|
92 |
{'foo': [{'bar': '1', 'var': 'test'}, {'em': '5', 't': 'url encoded'}]}
|
93 |
|
94 |
"""
|
95 |
-
if key ==
|
96 |
-
|
97 |
-
|
98 |
-
formats
|
99 |
-
|
|
|
|
|
|
|
|
|
100 |
try:
|
101 |
-
stream_data[key] = [
|
102 |
-
|
103 |
-
|
104 |
-
|
|
|
|
|
|
|
|
|
|
|
105 |
except KeyError:
|
106 |
-
cipher_url = [
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
else:
|
113 |
stream_data[key] = [
|
114 |
{k: unquote(v) for k, v in parse_qsl(i)}
|
115 |
-
for i in stream_data[key].split(
|
116 |
]
|
117 |
logger.debug(
|
118 |
-
|
119 |
-
pprint.pformat(stream_data[key], indent=2),
|
120 |
)
|
121 |
|
|
|
122 |
def install_proxy(proxy_handler):
|
123 |
proxy_support = request.ProxyHandler(proxy_handler)
|
124 |
opener = request.build_opener(proxy_support)
|
|
|
31 |
|
32 |
"""
|
33 |
stream_manifest = config_args[fmt]
|
34 |
+
live_stream = (
|
35 |
+
json.loads(config_args["player_response"])
|
36 |
+
.get("playabilityStatus", {},)
|
37 |
+
.get("liveStreamability")
|
38 |
+
)
|
39 |
for i, stream in enumerate(stream_manifest):
|
40 |
+
if "url" in stream:
|
41 |
+
url = stream["url"]
|
42 |
elif live_stream:
|
43 |
+
raise LiveStreamError("Video is currently being streamed live")
|
44 |
# 403 Forbidden fix.
|
45 |
+
if "signature" in url or (
|
46 |
+
"s" not in stream and ("&sig=" in url or "&lsig=" in url)
|
|
|
|
|
|
|
|
|
47 |
):
|
48 |
# For certain videos, YouTube will just provide them pre-signed, in
|
49 |
# which case there's no real magic to download them and we can skip
|
50 |
# the whole signature descrambling entirely.
|
51 |
+
logger.debug("signature found, skip decipher")
|
52 |
continue
|
53 |
|
54 |
if js is not None:
|
55 |
+
signature = cipher.get_signature(js, stream["s"])
|
56 |
else:
|
57 |
# signature not present in url (line 33), need js to descramble
|
58 |
# TypeError caught in __main__
|
59 |
+
raise TypeError("JS is None")
|
60 |
|
61 |
logger.debug(
|
62 |
+
"finished descrambling signature for itag=%s\n%s",
|
63 |
+
stream["itag"],
|
64 |
+
pprint.pformat({"s": stream["s"], "signature": signature,}, indent=2,),
|
|
|
|
|
|
|
|
|
65 |
)
|
66 |
# 403 forbidden fix
|
67 |
+
stream_manifest[i]["url"] = url + "&sig=" + signature
|
68 |
+
|
69 |
|
70 |
def apply_descrambler(stream_data, key):
|
71 |
"""Apply various in-place transforms to YouTube's media stream data.
|
|
|
87 |
{'foo': [{'bar': '1', 'var': 'test'}, {'em': '5', 't': 'url encoded'}]}
|
88 |
|
89 |
"""
|
90 |
+
if key == "url_encoded_fmt_stream_map" and not stream_data.get(
|
91 |
+
"url_encoded_fmt_stream_map"
|
92 |
+
):
|
93 |
+
formats = json.loads(stream_data["player_response"])["streamingData"]["formats"]
|
94 |
+
formats.extend(
|
95 |
+
json.loads(stream_data["player_response"])["streamingData"][
|
96 |
+
"adaptiveFormats"
|
97 |
+
]
|
98 |
+
)
|
99 |
try:
|
100 |
+
stream_data[key] = [
|
101 |
+
{
|
102 |
+
u"url": format_item[u"url"],
|
103 |
+
u"type": format_item[u"mimeType"],
|
104 |
+
u"quality": format_item[u"quality"],
|
105 |
+
u"itag": format_item[u"itag"],
|
106 |
+
}
|
107 |
+
for format_item in formats
|
108 |
+
]
|
109 |
except KeyError:
|
110 |
+
cipher_url = [
|
111 |
+
parse_qs(formats[i]["cipher"]) for i, data in enumerate(formats)
|
112 |
+
]
|
113 |
+
stream_data[key] = [
|
114 |
+
{
|
115 |
+
u"url": cipher_url[i][u"url"][0],
|
116 |
+
u"s": cipher_url[i][u"s"][0],
|
117 |
+
u"type": format_item[u"mimeType"],
|
118 |
+
u"quality": format_item[u"quality"],
|
119 |
+
u"itag": format_item[u"itag"],
|
120 |
+
}
|
121 |
+
for i, format_item in enumerate(formats)
|
122 |
+
]
|
123 |
else:
|
124 |
stream_data[key] = [
|
125 |
{k: unquote(v) for k, v in parse_qsl(i)}
|
126 |
+
for i in stream_data[key].split(",")
|
127 |
]
|
128 |
logger.debug(
|
129 |
+
"applying descrambler\n%s", pprint.pformat(stream_data[key], indent=2),
|
|
|
130 |
)
|
131 |
|
132 |
+
|
133 |
def install_proxy(proxy_handler):
|
134 |
proxy_support = request.ProxyHandler(proxy_handler)
|
135 |
opener = request.build_opener(proxy_support)
|
pytube/query.py
CHANGED
@@ -15,12 +15,23 @@ class StreamQuery:
|
|
15 |
self.itag_index = {int(s.itag): s for s in fmt_streams}
|
16 |
|
17 |
def filter(
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
):
|
25 |
"""Apply the given filtering criterion.
|
26 |
|
@@ -129,16 +140,12 @@ class StreamQuery:
|
|
129 |
|
130 |
if only_audio:
|
131 |
filters.append(
|
132 |
-
lambda s: (
|
133 |
-
s.includes_audio_track and not s.includes_video_track
|
134 |
-
),
|
135 |
)
|
136 |
|
137 |
if only_video:
|
138 |
filters.append(
|
139 |
-
lambda s: (
|
140 |
-
s.includes_video_track and not s.includes_audio_track
|
141 |
-
),
|
142 |
)
|
143 |
|
144 |
if progressive:
|
@@ -167,21 +174,21 @@ class StreamQuery:
|
|
167 |
attr = getattr(stream, attribute_name)
|
168 |
if attr is None:
|
169 |
break
|
170 |
-
num =
|
171 |
-
integer_attr_repr[attr] = int(
|
172 |
|
173 |
# if every attribute has an integer representation
|
174 |
if integer_attr_repr and all(integer_attr_repr.values()):
|
|
|
175 |
def key(s):
|
176 |
return integer_attr_repr[getattr(s, attribute_name)]
|
|
|
177 |
else:
|
|
|
178 |
def key(s):
|
179 |
return getattr(s, attribute_name)
|
180 |
|
181 |
-
fmt_streams = sorted(
|
182 |
-
self.fmt_streams,
|
183 |
-
key=key,
|
184 |
-
)
|
185 |
return StreamQuery(fmt_streams)
|
186 |
|
187 |
def desc(self):
|
|
|
15 |
self.itag_index = {int(s.itag): s for s in fmt_streams}
|
16 |
|
17 |
def filter(
|
18 |
+
self,
|
19 |
+
fps=None,
|
20 |
+
res=None,
|
21 |
+
resolution=None,
|
22 |
+
mime_type=None,
|
23 |
+
type=None,
|
24 |
+
subtype=None,
|
25 |
+
file_extension=None,
|
26 |
+
abr=None,
|
27 |
+
bitrate=None,
|
28 |
+
video_codec=None,
|
29 |
+
audio_codec=None,
|
30 |
+
only_audio=None,
|
31 |
+
only_video=None,
|
32 |
+
progressive=None,
|
33 |
+
adaptive=None,
|
34 |
+
custom_filter_functions=None,
|
35 |
):
|
36 |
"""Apply the given filtering criterion.
|
37 |
|
|
|
140 |
|
141 |
if only_audio:
|
142 |
filters.append(
|
143 |
+
lambda s: (s.includes_audio_track and not s.includes_video_track),
|
|
|
|
|
144 |
)
|
145 |
|
146 |
if only_video:
|
147 |
filters.append(
|
148 |
+
lambda s: (s.includes_video_track and not s.includes_audio_track),
|
|
|
|
|
149 |
)
|
150 |
|
151 |
if progressive:
|
|
|
174 |
attr = getattr(stream, attribute_name)
|
175 |
if attr is None:
|
176 |
break
|
177 |
+
num = "".join(x for x in attr if x.isdigit())
|
178 |
+
integer_attr_repr[attr] = int("".join(num)) if num else None
|
179 |
|
180 |
# if every attribute has an integer representation
|
181 |
if integer_attr_repr and all(integer_attr_repr.values()):
|
182 |
+
|
183 |
def key(s):
|
184 |
return integer_attr_repr[getattr(s, attribute_name)]
|
185 |
+
|
186 |
else:
|
187 |
+
|
188 |
def key(s):
|
189 |
return getattr(s, attribute_name)
|
190 |
|
191 |
+
fmt_streams = sorted(self.fmt_streams, key=key,)
|
|
|
|
|
|
|
192 |
return StreamQuery(fmt_streams)
|
193 |
|
194 |
def desc(self):
|
pytube/request.py
CHANGED
@@ -2,12 +2,12 @@
|
|
2 |
"""Implements a simple wrapper around urlopen."""
|
3 |
from urllib.request import Request
|
4 |
from urllib.request import urlopen
|
|
|
5 |
# 403 forbidden fix
|
6 |
|
7 |
|
8 |
def get(
|
9 |
-
url=None, headers=False,
|
10 |
-
streaming=False, chunk_size=8 * 1024,
|
11 |
):
|
12 |
"""Send an http GET request.
|
13 |
|
@@ -22,7 +22,7 @@ def get(
|
|
22 |
"""
|
23 |
|
24 |
# https://github.com/nficano/pytube/pull/465
|
25 |
-
req = Request(url, headers={
|
26 |
response = urlopen(req)
|
27 |
|
28 |
if streaming:
|
@@ -30,11 +30,7 @@ def get(
|
|
30 |
elif headers:
|
31 |
# https://github.com/nficano/pytube/issues/160
|
32 |
return {k.lower(): v for k, v in response.info().items()}
|
33 |
-
return (
|
34 |
-
response
|
35 |
-
.read()
|
36 |
-
.decode('utf-8')
|
37 |
-
)
|
38 |
|
39 |
|
40 |
def stream_response(response, chunk_size=8 * 1024):
|
|
|
2 |
"""Implements a simple wrapper around urlopen."""
|
3 |
from urllib.request import Request
|
4 |
from urllib.request import urlopen
|
5 |
+
|
6 |
# 403 forbidden fix
|
7 |
|
8 |
|
9 |
def get(
|
10 |
+
url=None, headers=False, streaming=False, chunk_size=8 * 1024,
|
|
|
11 |
):
|
12 |
"""Send an http GET request.
|
13 |
|
|
|
22 |
"""
|
23 |
|
24 |
# https://github.com/nficano/pytube/pull/465
|
25 |
+
req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
26 |
response = urlopen(req)
|
27 |
|
28 |
if streaming:
|
|
|
30 |
elif headers:
|
31 |
# https://github.com/nficano/pytube/issues/160
|
32 |
return {k.lower(): v for k, v in response.info().items()}
|
33 |
+
return response.read().decode("utf-8")
|
|
|
|
|
|
|
|
|
34 |
|
35 |
|
36 |
def stream_response(response, chunk_size=8 * 1024):
|
pytube/streams.py
CHANGED
@@ -42,18 +42,18 @@ class Stream(object):
|
|
42 |
# (Borg pattern).
|
43 |
self._monostate = monostate
|
44 |
|
45 |
-
self.abr = None
|
46 |
-
self.fps = None
|
47 |
self.itag = None # stream format id (youtube nomenclature)
|
48 |
-
self.res = None
|
49 |
-
self.url = None
|
50 |
|
51 |
self._filesize = None # filesize in bytes
|
52 |
self.mime_type = None # content identifier (e.g.: video/mp4)
|
53 |
-
self.type = None
|
54 |
-
self.subtype = None
|
55 |
|
56 |
-
self.codecs = []
|
57 |
self.audio_codec = None # audio codec of the stream (e.g.: vorbis)
|
58 |
self.video_codec = None # video codec of the stream (e.g.: vp8)
|
59 |
|
@@ -77,7 +77,7 @@ class Stream(object):
|
|
77 |
self.mime_type, self.codecs = extract.mime_type_codec(self.type)
|
78 |
|
79 |
# 'video/webm' -> 'video', 'webm'
|
80 |
-
self.type, self.subtype = self.mime_type.split(
|
81 |
|
82 |
# ['vp8', 'vorbis'] -> video_codec: vp8, audio_codec: vorbis. DASH
|
83 |
# streams return NoneType for audio/video depending.
|
@@ -117,7 +117,7 @@ class Stream(object):
|
|
117 |
"""
|
118 |
if self.is_progressive:
|
119 |
return True
|
120 |
-
return self.type ==
|
121 |
|
122 |
@property
|
123 |
def includes_video_track(self):
|
@@ -127,7 +127,7 @@ class Stream(object):
|
|
127 |
"""
|
128 |
if self.is_progressive:
|
129 |
return True
|
130 |
-
return self.type ==
|
131 |
|
132 |
def parse_codecs(self):
|
133 |
"""Get the video/audio codecs from list of codecs.
|
@@ -162,7 +162,7 @@ class Stream(object):
|
|
162 |
"""
|
163 |
if self._filesize is None:
|
164 |
headers = request.get(self.url, headers=True)
|
165 |
-
self._filesize = int(headers[
|
166 |
return self._filesize
|
167 |
|
168 |
@property
|
@@ -175,17 +175,17 @@ class Stream(object):
|
|
175 |
"""
|
176 |
player_config_args = self.player_config_args or {}
|
177 |
|
178 |
-
if
|
179 |
-
return player_config_args[
|
180 |
|
181 |
-
details = self.player_config_args.get(
|
182 |
-
|
183 |
-
)
|
184 |
|
185 |
-
if
|
186 |
-
return details[
|
187 |
|
188 |
-
return
|
189 |
|
190 |
@property
|
191 |
def default_filename(self):
|
@@ -197,7 +197,7 @@ class Stream(object):
|
|
197 |
"""
|
198 |
|
199 |
filename = safe_filename(self.title)
|
200 |
-
return
|
201 |
|
202 |
def download(self, output_path=None, filename=None, filename_prefix=None):
|
203 |
"""Write the media stream to disk.
|
@@ -224,25 +224,22 @@ class Stream(object):
|
|
224 |
output_path = output_path or os.getcwd()
|
225 |
if filename:
|
226 |
safe = safe_filename(filename)
|
227 |
-
filename =
|
228 |
filename = filename or self.default_filename
|
229 |
|
230 |
if filename_prefix:
|
231 |
-
filename =
|
232 |
-
|
233 |
-
|
234 |
-
filename=filename,
|
235 |
-
)
|
236 |
|
237 |
# file path
|
238 |
fp = os.path.join(output_path, filename)
|
239 |
bytes_remaining = self.filesize
|
240 |
logger.debug(
|
241 |
-
|
242 |
-
self.filesize, fp,
|
243 |
)
|
244 |
|
245 |
-
with open(fp,
|
246 |
for chunk in request.get(self.url, streaming=True):
|
247 |
# reduce the (bytes) remainder by the length of the chunk.
|
248 |
bytes_remaining -= len(chunk)
|
@@ -259,8 +256,7 @@ class Stream(object):
|
|
259 |
buffer = io.BytesIO()
|
260 |
bytes_remaining = self.filesize
|
261 |
logger.debug(
|
262 |
-
|
263 |
-
self.filesize,
|
264 |
)
|
265 |
|
266 |
for chunk in request.get(self.url, streaming=True):
|
@@ -293,17 +289,15 @@ class Stream(object):
|
|
293 |
"""
|
294 |
file_handler.write(chunk)
|
295 |
logger.debug(
|
296 |
-
|
297 |
pprint.pformat(
|
298 |
-
{
|
299 |
-
|
300 |
-
'bytes_remaining': bytes_remaining,
|
301 |
-
}, indent=2,
|
302 |
),
|
303 |
)
|
304 |
-
on_progress = self._monostate[
|
305 |
if on_progress:
|
306 |
-
logger.debug(
|
307 |
on_progress(self, chunk, file_handler, bytes_remaining)
|
308 |
|
309 |
def on_complete(self, file_handle):
|
@@ -317,10 +311,10 @@ class Stream(object):
|
|
317 |
:rtype: None
|
318 |
|
319 |
"""
|
320 |
-
logger.debug(
|
321 |
-
on_complete = self._monostate[
|
322 |
if on_complete:
|
323 |
-
logger.debug(
|
324 |
on_complete(self, file_handle)
|
325 |
|
326 |
def __repr__(self):
|
@@ -335,13 +329,12 @@ class Stream(object):
|
|
335 |
if self.includes_video_track:
|
336 |
parts.extend(['res="{s.resolution}"', 'fps="{s.fps}fps"'])
|
337 |
if not self.is_adaptive:
|
338 |
-
parts.extend(
|
339 |
-
'vcodec="{s.video_codec}"',
|
340 |
-
|
341 |
-
])
|
342 |
else:
|
343 |
parts.extend(['vcodec="{s.video_codec}"'])
|
344 |
else:
|
345 |
parts.extend(['abr="{s.abr}"', 'acodec="{s.audio_codec}"'])
|
346 |
-
parts =
|
347 |
-
return
|
|
|
42 |
# (Borg pattern).
|
43 |
self._monostate = monostate
|
44 |
|
45 |
+
self.abr = None # average bitrate (audio streams only)
|
46 |
+
self.fps = None # frames per second (video streams only)
|
47 |
self.itag = None # stream format id (youtube nomenclature)
|
48 |
+
self.res = None # resolution (e.g.: 480p, 720p, 1080p)
|
49 |
+
self.url = None # signed download url
|
50 |
|
51 |
self._filesize = None # filesize in bytes
|
52 |
self.mime_type = None # content identifier (e.g.: video/mp4)
|
53 |
+
self.type = None # the part of the mime before the slash
|
54 |
+
self.subtype = None # the part of the mime after the slash
|
55 |
|
56 |
+
self.codecs = [] # audio/video encoders (e.g.: vp8, mp4a)
|
57 |
self.audio_codec = None # audio codec of the stream (e.g.: vorbis)
|
58 |
self.video_codec = None # video codec of the stream (e.g.: vp8)
|
59 |
|
|
|
77 |
self.mime_type, self.codecs = extract.mime_type_codec(self.type)
|
78 |
|
79 |
# 'video/webm' -> 'video', 'webm'
|
80 |
+
self.type, self.subtype = self.mime_type.split("/")
|
81 |
|
82 |
# ['vp8', 'vorbis'] -> video_codec: vp8, audio_codec: vorbis. DASH
|
83 |
# streams return NoneType for audio/video depending.
|
|
|
117 |
"""
|
118 |
if self.is_progressive:
|
119 |
return True
|
120 |
+
return self.type == "audio"
|
121 |
|
122 |
@property
|
123 |
def includes_video_track(self):
|
|
|
127 |
"""
|
128 |
if self.is_progressive:
|
129 |
return True
|
130 |
+
return self.type == "video"
|
131 |
|
132 |
def parse_codecs(self):
|
133 |
"""Get the video/audio codecs from list of codecs.
|
|
|
162 |
"""
|
163 |
if self._filesize is None:
|
164 |
headers = request.get(self.url, headers=True)
|
165 |
+
self._filesize = int(headers["content-length"])
|
166 |
return self._filesize
|
167 |
|
168 |
@property
|
|
|
175 |
"""
|
176 |
player_config_args = self.player_config_args or {}
|
177 |
|
178 |
+
if "title" in player_config_args:
|
179 |
+
return player_config_args["title"]
|
180 |
|
181 |
+
details = self.player_config_args.get("player_response", {},).get(
|
182 |
+
"videoDetails", {}
|
183 |
+
)
|
184 |
|
185 |
+
if "title" in details:
|
186 |
+
return details["title"]
|
187 |
|
188 |
+
return "Unknown YouTube Video Title"
|
189 |
|
190 |
@property
|
191 |
def default_filename(self):
|
|
|
197 |
"""
|
198 |
|
199 |
filename = safe_filename(self.title)
|
200 |
+
return "{filename}.{s.subtype}".format(filename=filename, s=self)
|
201 |
|
202 |
def download(self, output_path=None, filename=None, filename_prefix=None):
|
203 |
"""Write the media stream to disk.
|
|
|
224 |
output_path = output_path or os.getcwd()
|
225 |
if filename:
|
226 |
safe = safe_filename(filename)
|
227 |
+
filename = "{filename}.{s.subtype}".format(filename=safe, s=self)
|
228 |
filename = filename or self.default_filename
|
229 |
|
230 |
if filename_prefix:
|
231 |
+
filename = "{prefix}{filename}".format(
|
232 |
+
prefix=safe_filename(filename_prefix), filename=filename,
|
233 |
+
)
|
|
|
|
|
234 |
|
235 |
# file path
|
236 |
fp = os.path.join(output_path, filename)
|
237 |
bytes_remaining = self.filesize
|
238 |
logger.debug(
|
239 |
+
"downloading (%s total bytes) file to %s", self.filesize, fp,
|
|
|
240 |
)
|
241 |
|
242 |
+
with open(fp, "wb") as fh:
|
243 |
for chunk in request.get(self.url, streaming=True):
|
244 |
# reduce the (bytes) remainder by the length of the chunk.
|
245 |
bytes_remaining -= len(chunk)
|
|
|
256 |
buffer = io.BytesIO()
|
257 |
bytes_remaining = self.filesize
|
258 |
logger.debug(
|
259 |
+
"downloading (%s total bytes) file to BytesIO buffer", self.filesize,
|
|
|
260 |
)
|
261 |
|
262 |
for chunk in request.get(self.url, streaming=True):
|
|
|
289 |
"""
|
290 |
file_handler.write(chunk)
|
291 |
logger.debug(
|
292 |
+
"download progress\n%s",
|
293 |
pprint.pformat(
|
294 |
+
{"chunk_size": len(chunk), "bytes_remaining": bytes_remaining,},
|
295 |
+
indent=2,
|
|
|
|
|
296 |
),
|
297 |
)
|
298 |
+
on_progress = self._monostate["on_progress"]
|
299 |
if on_progress:
|
300 |
+
logger.debug("calling on_progress callback %s", on_progress)
|
301 |
on_progress(self, chunk, file_handler, bytes_remaining)
|
302 |
|
303 |
def on_complete(self, file_handle):
|
|
|
311 |
:rtype: None
|
312 |
|
313 |
"""
|
314 |
+
logger.debug("download finished")
|
315 |
+
on_complete = self._monostate["on_complete"]
|
316 |
if on_complete:
|
317 |
+
logger.debug("calling on_complete callback %s", on_complete)
|
318 |
on_complete(self, file_handle)
|
319 |
|
320 |
def __repr__(self):
|
|
|
329 |
if self.includes_video_track:
|
330 |
parts.extend(['res="{s.resolution}"', 'fps="{s.fps}fps"'])
|
331 |
if not self.is_adaptive:
|
332 |
+
parts.extend(
|
333 |
+
['vcodec="{s.video_codec}"', 'acodec="{s.audio_codec}"',]
|
334 |
+
)
|
|
|
335 |
else:
|
336 |
parts.extend(['vcodec="{s.video_codec}"'])
|
337 |
else:
|
338 |
parts.extend(['abr="{s.abr}"', 'acodec="{s.audio_codec}"'])
|
339 |
+
parts = " ".join(parts).format(s=self)
|
340 |
+
return "<Stream: {parts}>".format(parts=parts)
|