hbmartin commited on
Commit
40473f7
·
1 Parent(s): fcd83e1

black formatting

Browse files
.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": "5a2d404725db87789c428cc6fb3f2945c4232b4838e18c4ad95d5f07d002315a"
5
  },
6
  "pipfile-spec": 6,
7
  "requires": {
8
- "python_version": "3.6"
9
  },
10
  "sources": [
11
  {
@@ -19,24 +19,17 @@
19
  "develop": {
20
  "aspy.yaml": {
21
  "hashes": [
22
- "sha256:ae249074803e8b957c83fdd82a99160d0d6d26dff9ba81ba608b42eebd7d8cd3",
23
- "sha256:c7390d79f58eb9157406966201abf26da0d56c07e0ff0deadc39c8f4dbc13482"
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:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
37
- "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
38
  ],
39
- "version": "==19.1.0"
40
  },
41
  "bleach": {
42
  "hashes": [
@@ -55,17 +48,17 @@
55
  },
56
  "certifi": {
57
  "hashes": [
58
- "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
59
- "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
60
  ],
61
- "version": "==2019.3.9"
62
  },
63
  "cfgv": {
64
  "hashes": [
65
- "sha256:6e9f2feea5e84bc71e56abd703140d7a2c250fc5ba38b8702fd6a68ed4e3b2ef",
66
- "sha256:e7f186d4a36c099a9e20b04ac3108bd8bb9b9257e692ce18c8c3764d5cb12172"
67
  ],
68
- "version": "==1.6.0"
69
  },
70
  "chardet": {
71
  "hashes": [
@@ -76,47 +69,47 @@
76
  },
77
  "coverage": {
78
  "hashes": [
79
- "sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9",
80
- "sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74",
81
- "sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390",
82
- "sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8",
83
- "sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe",
84
- "sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf",
85
- "sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e",
86
- "sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741",
87
- "sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09",
88
- "sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd",
89
- "sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034",
90
- "sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420",
91
- "sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c",
92
- "sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab",
93
- "sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba",
94
- "sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e",
95
- "sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609",
96
- "sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2",
97
- "sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49",
98
- "sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b",
99
- "sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d",
100
- "sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce",
101
- "sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9",
102
- "sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4",
103
- "sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773",
104
- "sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723",
105
- "sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c",
106
- "sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f",
107
- "sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1",
108
- "sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260",
109
- "sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a"
110
- ],
111
- "version": "==4.5.3"
112
  },
113
  "coveralls": {
114
  "hashes": [
115
- "sha256:baa26648430d5c2225ab12d7e2067f75597a4b967034bba7e3d5ab7501d207a1",
116
- "sha256:ff9b7823b15070f26f654837bb02a201d006baaf2083e0514ffd3b34a3ffed81"
117
  ],
118
  "index": "pypi",
119
- "version": "==1.7.0"
120
  },
121
  "docopt": {
122
  "hashes": [
@@ -126,11 +119,10 @@
126
  },
127
  "docutils": {
128
  "hashes": [
129
- "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
130
- "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
131
- "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
132
  ],
133
- "version": "==0.14"
134
  },
135
  "entrypoints": {
136
  "hashes": [
@@ -151,18 +143,18 @@
151
  },
152
  "flake8": {
153
  "hashes": [
154
- "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661",
155
- "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8"
156
  ],
157
  "index": "pypi",
158
- "version": "==3.7.7"
159
  },
160
  "identify": {
161
  "hashes": [
162
- "sha256:443f419ca6160773cbaf22dbb302b1e436a386f23129dbb5482b68a147c2eca9",
163
- "sha256:bd7f15fe07112b713fb68fbdde3a34dd774d9062128f2c398104889f783f989d"
164
  ],
165
- "version": "==1.4.2"
166
  },
167
  "idna": {
168
  "hashes": [
@@ -173,18 +165,18 @@
173
  },
174
  "importlib-metadata": {
175
  "hashes": [
176
- "sha256:46fc60c34b6ed7547e2a723fc8de6dc2e3a1173f8423246b3ce497f064e9c3de",
177
- "sha256:bc136180e961875af88b1ab85b4009f4f1278f8396a60526c0009f503a1a96ca"
178
  ],
179
- "version": "==0.9"
 
180
  },
181
- "importlib-resources": {
182
  "hashes": [
183
- "sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b",
184
- "sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078"
185
  ],
186
- "markers": "python_version < '3.7'",
187
- "version": "==1.0.2"
188
  },
189
  "mccabe": {
190
  "hashes": [
@@ -195,11 +187,11 @@
195
  },
196
  "mock": {
197
  "hashes": [
198
- "sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1",
199
- "sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba"
200
  ],
201
  "index": "pypi",
202
- "version": "==2.0.0"
203
  },
204
  "more-itertools": {
205
  "hashes": [
@@ -212,24 +204,24 @@
212
  },
213
  "nodeenv": {
214
  "hashes": [
215
- "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a"
216
  ],
217
- "version": "==1.3.3"
218
  },
219
- "pathlib2": {
220
  "hashes": [
221
- "sha256:25199318e8cc3c25dcb45cbe084cc061051336d5a9ea2a12448d3d8cb748f742",
222
- "sha256:5887121d7f7df3603bca2f710e7219f3eca0eb69e0b7cc6e0a022e155ac931a7"
223
  ],
224
- "index": "pypi",
225
- "version": "==2.3.3"
226
  },
227
- "pbr": {
228
  "hashes": [
229
- "sha256:6901995b9b686cb90cceba67a0f6d4d14ae003cd59bc12beb61549bdfbe3bc89",
230
- "sha256:d950c64aeea5456bbd147468382a5bb77fe692c13c9f00f0219814ce5b642755"
231
  ],
232
- "version": "==5.2.0"
 
233
  },
234
  "pkginfo": {
235
  "hashes": [
@@ -240,25 +232,25 @@
240
  },
241
  "pluggy": {
242
  "hashes": [
243
- "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f",
244
- "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746"
245
  ],
246
- "version": "==0.9.0"
247
  },
248
  "pre-commit": {
249
  "hashes": [
250
- "sha256:2576a2776098f3902ef9540a84696e8e06bf18a337ce43a6a889e7fa5d26c4c5",
251
- "sha256:82f2f2d657d7f9280de9f927ae56886d60b9ef7f3714eae92d12713cd9cb9e11"
252
  ],
253
  "index": "pypi",
254
- "version": "==1.15.2"
255
  },
256
  "py": {
257
  "hashes": [
258
- "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
259
- "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
260
  ],
261
- "version": "==1.8.0"
262
  },
263
  "pycodestyle": {
264
  "hashes": [
@@ -276,50 +268,57 @@
276
  },
277
  "pygments": {
278
  "hashes": [
279
- "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a",
280
- "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"
281
  ],
282
- "version": "==2.3.1"
 
 
 
 
 
 
 
283
  },
284
  "pytest": {
285
  "hashes": [
286
- "sha256:3773f4c235918987d51daf1db66d51c99fac654c81d6f2f709a046ab446d5e5d",
287
- "sha256:b7802283b70ca24d7119b32915efa7c409982f59913c1a6c0640aacf118b95f5"
288
  ],
289
  "index": "pypi",
290
- "version": "==4.4.1"
291
  },
292
  "pytest-cov": {
293
  "hashes": [
294
- "sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33",
295
- "sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f"
296
  ],
297
  "index": "pypi",
298
- "version": "==2.6.1"
299
  },
300
  "pytest-mock": {
301
  "hashes": [
302
- "sha256:43ce4e9dd5074993e7c021bb1c22cbb5363e612a2b5a76bc6d956775b10758b7",
303
- "sha256:5bf5771b1db93beac965a7347dc81c675ec4090cb841e49d9d34637a25c30568"
304
  ],
305
  "index": "pypi",
306
- "version": "==1.10.4"
307
  },
308
  "pyyaml": {
309
  "hashes": [
310
- "sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c",
311
- "sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95",
312
- "sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2",
313
- "sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4",
314
- "sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad",
315
- "sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba",
316
- "sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1",
317
- "sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e",
318
- "sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673",
319
- "sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13",
320
- "sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19"
321
  ],
322
- "version": "==5.1"
323
  },
324
  "readme-renderer": {
325
  "hashes": [
@@ -330,10 +329,10 @@
330
  },
331
  "requests": {
332
  "hashes": [
333
- "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
334
- "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
335
  ],
336
- "version": "==2.21.0"
337
  },
338
  "requests-toolbelt": {
339
  "hashes": [
@@ -361,10 +360,10 @@
361
  },
362
  "six": {
363
  "hashes": [
364
- "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
365
- "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
366
  ],
367
- "version": "==1.12.0"
368
  },
369
  "toml": {
370
  "hashes": [
@@ -375,32 +374,39 @@
375
  },
376
  "tqdm": {
377
  "hashes": [
378
- "sha256:d385c95361699e5cf7622485d9b9eae2d4864b21cd5a2374a9c381ffed701021",
379
- "sha256:e22977e3ebe961f72362f6ddfb9197cc531c9737aaf5f607ef09740c849ecd05"
380
  ],
381
- "version": "==4.31.1"
382
  },
383
  "twine": {
384
  "hashes": [
385
- "sha256:0fb0bfa3df4f62076cab5def36b1a71a2e4acb4d1fa5c97475b048117b1a6446",
386
- "sha256:d6c29c933ecfc74e9b1d9fa13aa1f87c5d5770e119f5a4ce032092f0ff5b14dc"
387
  ],
388
  "index": "pypi",
389
- "version": "==1.13.0"
390
  },
391
  "urllib3": {
392
  "hashes": [
393
- "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0",
394
- "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3"
395
  ],
396
- "version": "==1.24.2"
397
  },
398
  "virtualenv": {
399
  "hashes": [
400
- "sha256:15ee248d13e4001a691d9583948ad3947bcb8a289775102e4c4aa98a8b7a6d73",
401
- "sha256:bfc98bb9b42a3029ee41b96dc00a34c2f254cbf7716bec824477b2c82741a5c4"
 
 
 
 
 
 
 
402
  ],
403
- "version": "==16.5.0"
404
  },
405
  "webencodings": {
406
  "hashes": [
@@ -411,10 +417,10 @@
411
  },
412
  "zipp": {
413
  "hashes": [
414
- "sha256:139391b239594fd8b91d856bc530fbd2df0892b17dd8d98a91f018715954185f",
415
- "sha256:8047e4575ce8d700370a3301bbfc972896a5845eb62dd535da395b86be95dfad"
416
  ],
417
- "version": "==0.4.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__ = '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,4 +19,4 @@ from pytube.contrib.playlist import Playlist
19
  from pytube.__main__ import YouTube
20
 
21
  logger = create_logger()
22
- logger.info('%s v%s', __title__, __version__)
 
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, 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
 
@@ -48,16 +53,16 @@ class YouTube(object):
48
  complete events.
49
 
50
  """
51
- self.js = None # js fetched by js_url
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 # content fetched by vid_info_url
58
  self.vid_info_url = None # the url to vid info, parsed from watch html
59
 
60
- self.watch_html = None # the html of /watch?v=<video_id>
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
- 'on_progress': on_progress_callback,
81
- 'on_complete': on_complete_callback,
82
  }
83
 
84
  if proxies:
@@ -107,34 +112,30 @@ class YouTube(object):
107
  :rtype: None
108
 
109
  """
110
- logger.info('init started')
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
- self.watch_html,
118
- )['args']
119
 
120
  # Fix for KeyError: 'title' issue #434
121
- if 'title' not in self.player_config_args:
122
- i_start = (
123
- self.watch_html
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(' - youtube')
130
  title = title[:index] if index > 0 else title
131
- self.player_config_args['title'] = title
132
 
133
  self.vid_descr = extract.get_vid_descr(self.watch_html)
134
  # https://github.com/nficano/pytube/issues/165
135
- stream_maps = ['url_encoded_fmt_stream_map']
136
- if 'adaptive_fmts' in self.player_config_args:
137
- stream_maps.append('adaptive_fmts')
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, 'player_response', json.loads)
159
 
160
  self.initialize_caption_objects()
161
- logger.info('init finished successfully')
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('This video is unavailable.')
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 'captions' not in self.player_config_args['player_response']:
223
  return
224
  # https://github.com/nficano/pytube/issues/167
225
  caption_tracks = (
226
- self.player_config_args
227
- .get('player_response', {})
228
- .get('captions', {})
229
- .get('playerCaptionsTracklistRenderer', {})
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['thumbnail_url']
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['title']
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('player_response', {})
288
- .get('videoDetails', {})
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['length_seconds']
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('player_response', {})
311
- .get('videoDetails', {})
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['on_progress'] = func
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['on_complete'] = func
 
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('baseUrl')
20
- self.name = caption_track['name']['simpleText']
21
- self.code = caption_track['languageCode']
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('%H:%M:%S,', time.gmtime(whole))
48
- ms = '{:.3f}'.format(frac).replace('0.', '')
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
- text
63
- .replace('\n', ' ')
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
- '{seq}\n{start} --> {end}\n{text}\n'.format(
72
- seq=sequence_number,
73
- start=self.float_to_srt_time_format(start),
74
- end=self.float_to_srt_time_format(end),
75
- text=caption,
76
- )
77
  )
78
  segments.append(line)
79
- return '\n'.join(segments).strip()
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'\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,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'%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,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'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,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(':', 1)
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
- ('{\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\];'
210
- '\w\[\w\%\w.length\]=\w}', swap,
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
- 'could not find python equivalent function for: ',
219
- js_func,
220
  )
221
 
222
 
@@ -238,8 +237,8 @@ def parse_function(js_func):
238
  ('AJ', 15)
239
 
240
  """
241
- logger.debug('parsing transform function')
242
- return regex_search(r'\w+\.(\w+)\(\w,(\d+)\)', js_func, groups=True)
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
- 'applied transform function\n%s', pprint.pformat(
 
270
  {
271
- 'output': ''.join(signature),
272
- 'js_function': name,
273
- 'argument': int(argument),
274
- 'function': tmap[name],
275
- }, indent=2,
 
276
  ),
277
  )
278
- return ''.join(signature)
 
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('url', help='The YouTube /watch url', nargs='?')
25
  parser.add_argument(
26
- '--version', action='version',
27
- version='%(prog)s ' + __version__,
28
  )
29
  parser.add_argument(
30
- '--itag', type=int, help=(
31
- 'The itag for the desired stream'
32
- ),
33
  )
34
  parser.add_argument(
35
- '-l', '--list', action='store_true', 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', '--verbose', action='count', default=0, dest='verbosity',
42
- help='Verbosity level',
 
 
 
 
43
  )
44
  parser.add_argument(
45
- '--build-playback-report', action='store_true', help=(
46
- 'Save the html and js to disk'
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, 'wb') as fh:
85
  fh.write(
86
- json.dumps({
87
- 'url': url,
88
- 'js': js,
89
- 'watch_html': watch_html,
90
- 'video_info': vid_info,
91
- })
92
- .encode('utf8'),
 
93
  )
94
 
95
 
96
  def get_terminal_size():
97
  """Return the terminal size in rows and columns."""
98
- rows, columns = os.popen('stty size', 'r').read().split()
99
  return int(rows), int(columns)
100
 
101
 
102
- def display_progress_bar(bytes_received, filesize, ch='', scale=0.55):
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 + ' ' * remaining
127
  percent = round(100.0 * bytes_received / float(filesize), 1)
128
- text = ' ↳ |{bar}| {percent}%\r'.format(bar=bar, percent=percent)
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('\n{fn} | {fs} bytes'.format(
165
- fn=stream.default_filename,
166
- fs=stream.filesize,
167
- ))
168
  try:
169
  stream.download()
170
- sys.stdout.write('\n')
171
  except KeyboardInterrupt:
172
  sys.exit()
173
 
@@ -184,5 +185,5 @@ def display_streams(url):
184
  print(stream)
185
 
186
 
187
- if __name__ == '__main__':
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 '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,12 +44,13 @@ class Playlist(object):
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=.*?)\"', req,
 
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('\n') if 'pl-video-title-link' in x]
66
- link_list = [x.split('href="', 1)[1].split('&', 1)[0] for x in content]
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): # there is an url found
72
- logger.debug('load more url: %s' % load_more_url)
73
  req = request.get(load_more_url)
74
  load_more = json.loads(req)
75
  videos = re.findall(
76
- r'href=\"(/watch\?v=[\w-]*)',
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 = 'https://www.youtube.com'
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('total videos found: %d', len(self.video_urls))
148
- logger.debug('starting download')
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('Exception suppressed')
161
  else:
162
  # TODO: this should not be hardcoded to a single user's
163
  # preference
164
- dl_stream = yt.streams.filter(
165
- progressive=True, subtype='mp4',
166
- ).order_by('resolution').desc().first()
167
-
168
- logger.debug('download path: %s', download_path)
 
 
 
169
  if prefix_number:
170
  prefix = next(prefix_gen)
171
- logger.debug('file prefix is: %s', prefix)
172
  dl_stream.download(download_path, filename_prefix=prefix)
173
  else:
174
  dl_stream.download(download_path)
175
- logger.debug('download complete')
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 = '<title>'
184
- end_tag = '</title>'
185
- matchresult = re.compile(open_tag + '(.+?)' + end_tag)
186
  matchresult = matchresult.search(req).group()
187
- matchresult = matchresult.replace(open_tag, '')
188
- matchresult = matchresult.replace(end_tag, '')
189
- matchresult = matchresult.replace('- YouTube', '')
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 = '{video_id}: {msg}'.format(video_id=video_id, msg=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 == '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,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'og:restrictions:age', watch_html, group=0)
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'(?:v=|\/)([0-9A-Za-z_-]{11}).*', url, group=1)
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 '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,
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
- ('video_id', video_id),
121
- ('eurl', eurl(video_id)),
122
- ('sts', sts),
123
- ])
124
  else:
125
- params = OrderedDict([
126
- ('video_id', video_id),
127
- ('el', '$el'),
128
- ('ps', 'default'),
129
- ('eurl', quote(watch_url)),
130
- ('hl', 'en_US'),
131
- ])
132
- return 'https://youtube.com/get_video_info?' + urlencode(params)
 
 
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['assets']['js']
149
- return 'https://youtube.com' + base_js
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'(\w+\/\w+)\;\scodecs=\"([a-zA-Z-0-9.,\s]*)\"'
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';ytplayer\.config\s*=\s*({.*?});'
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
- 'regex pattern ({pattern}) had zero matches'
40
- .format(pattern=p),
41
  )
42
  else:
43
  logger.debug(
44
- 'finished regex search: %s',
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
- 'regex pattern ({pattern}) had zero matches'
64
- .format(pattern=pattern),
65
  )
66
  else:
67
  logger.debug(
68
- 'finished regex search: %s',
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 = '|'.join(ntfs_chrs + chrs)
124
  regex = re.compile(pattern, re.UNICODE)
125
- filename = regex.sub('', s)
126
- return filename[:max_length].rsplit(' ', 0)[0]
 
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: ('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
-
38
  # DASH Video
39
- 133: ('240p', None),
40
- 134: ('360p', None),
41
- 135: ('480p', None),
42
- 136: ('720p', None),
43
- 137: ('1080p', None),
44
- 138: ('2160p', None),
45
- 160: ('144p', None),
46
- 167: ('360p', None),
47
- 168: ('480p', None),
48
- 169: ('720p', None),
49
- 170: ('1080p', None),
50
- 212: ('480p', None),
51
- 218: ('480p', None),
52
- 219: ('480p', None),
53
- 242: ('240p', None),
54
- 243: ('360p', None),
55
- 244: ('480p', None),
56
- 245: ('480p', 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),
66
- 299: ('1080p', None),
67
- 302: ('720p', None),
68
- 303: ('1080p', None),
69
- 308: ('1440p', None),
70
- 313: ('2160p', None),
71
- 315: ('2160p', None),
72
- 330: ('144p', None),
73
- 331: ('240p', None),
74
- 332: ('360p', None),
75
- 333: ('480p', None),
76
- 334: ('720p', None),
77
- 335: ('1080p', None),
78
- 336: ('1440p', None),
79
- 337: ('2160p', None),
80
-
81
  # DASH Audio
82
- 139: (None, '48kbps'),
83
- 140: (None, '128kbps'),
84
- 141: (None, '256kbps'),
85
- 171: (None, '128kbps'),
86
- 172: (None, '256kbps'),
87
- 249: (None, '50kbps'),
88
- 250: (None, '70kbps'),
89
- 251: (None, '160kbps'),
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
- 'resolution': res,
115
- 'abr': bitrate,
116
- 'is_live': itag in LIVE,
117
- 'is_3d': itag in _3D,
118
- 'is_hdr': itag in HDR,
119
- 'fps': 60 if itag in _60FPS else 30,
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 = '[%(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
 
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 = json.loads(config_args['player_response']).get(
35
- 'playabilityStatus', {},
36
- ).get('liveStreamability')
 
 
37
  for i, stream in enumerate(stream_manifest):
38
- if 'url' in stream:
39
- url = stream['url']
40
  elif live_stream:
41
- raise LiveStreamError('Video is currently being streamed live')
42
  # 403 Forbidden fix.
43
- if (
44
- 'signature' in url or (
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('signature found, skip decipher')
54
  continue
55
 
56
  if js is not None:
57
- signature = cipher.get_signature(js, stream['s'])
58
  else:
59
  # signature not present in url (line 33), need js to descramble
60
  # TypeError caught in __main__
61
- raise TypeError('JS is None')
62
 
63
  logger.debug(
64
- 'finished descrambling signature for itag=%s\n%s',
65
- stream['itag'], pprint.pformat(
66
- {
67
- 's': stream['s'],
68
- 'signature': signature,
69
- }, indent=2,
70
- ),
71
  )
72
  # 403 forbidden fix
73
- stream_manifest[i]['url'] = url + '&sig=' + signature
 
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 == 'url_encoded_fmt_stream_map' and not stream_data.get('url_encoded_fmt_stream_map'):
96
- formats = json.loads(stream_data['player_response'])[
97
- 'streamingData']['formats']
98
- formats.extend(json.loads(stream_data['player_response'])[
99
- 'streamingData']['adaptiveFormats'])
 
 
 
 
100
  try:
101
- stream_data[key] = [{u'url': format_item[u'url'],
102
- u'type': format_item[u'mimeType'],
103
- u'quality': format_item[u'quality'],
104
- u'itag': format_item[u'itag']} for format_item in formats]
 
 
 
 
 
105
  except KeyError:
106
- cipher_url = [parse_qs(formats[i]['cipher']) for i, data in enumerate(formats)]
107
- stream_data[key] = [{u'url': cipher_url[i][u'url'][0],
108
- u's': cipher_url[i][u's'][0],
109
- u'type': format_item[u'mimeType'],
110
- u'quality': format_item[u'quality'],
111
- u'itag': format_item[u'itag']} for i, format_item in enumerate(formats)]
 
 
 
 
 
 
 
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
- 'applying descrambler\n%s',
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
- self, fps=None, res=None, resolution=None, mime_type=None,
19
- type=None, subtype=None, file_extension=None, abr=None,
20
- bitrate=None, video_codec=None, audio_codec=None,
21
- only_audio=None, only_video=None,
22
- progressive=None, adaptive=None,
23
- custom_filter_functions=None,
 
 
 
 
 
 
 
 
 
 
 
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 = ''.join(x for x in attr if x.isdigit())
171
- integer_attr_repr[attr] = int(''.join(num)) if num else None
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={'User-Agent': 'Mozilla/5.0'})
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 # 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,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 == 'audio'
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 == 'video'
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['content-length'])
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 'title' in player_config_args:
179
- return player_config_args['title']
180
 
181
- details = self.player_config_args.get(
182
- 'player_response', {},
183
- ).get('videoDetails', {})
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,7 +197,7 @@ class Stream(object):
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,25 +224,22 @@ class Stream(object):
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}'\
232
- .format(
233
- prefix=safe_filename(filename_prefix),
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
- 'downloading (%s total bytes) file to %s',
242
- self.filesize, fp,
243
  )
244
 
245
- with open(fp, 'wb') as fh:
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
- 'downloading (%s total bytes) file to BytesIO buffer',
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
- 'download progress\n%s',
297
  pprint.pformat(
298
- {
299
- 'chunk_size': len(chunk),
300
- 'bytes_remaining': bytes_remaining,
301
- }, indent=2,
302
  ),
303
  )
304
- on_progress = self._monostate['on_progress']
305
  if on_progress:
306
- logger.debug('calling on_progress callback %s', on_progress)
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('download finished')
321
- on_complete = self._monostate['on_complete']
322
  if on_complete:
323
- logger.debug('calling on_complete callback %s', on_complete)
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
- 'acodec="{s.audio_codec}"',
341
- ])
342
  else:
343
  parts.extend(['vcodec="{s.video_codec}"'])
344
  else:
345
  parts.extend(['abr="{s.abr}"', 'acodec="{s.audio_codec}"'])
346
- parts = ' '.join(parts).format(s=self)
347
- return '<Stream: {parts}>'.format(parts=parts)
 
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)