]> jfr.im git - yt-dlp.git/blame - yt_dlp/utils.py
Improved progress reporting (See desc) (#1125)
[yt-dlp.git] / yt_dlp / utils.py
CommitLineData
cc52de43 1#!/usr/bin/env python3
dcdb292f 2# coding: utf-8
d77c3dfd 3
ecc0c5ee
PH
4from __future__ import unicode_literals
5
1e399778 6import base64
5bc880b9 7import binascii
912b38b4 8import calendar
676eb3f2 9import codecs
c380cc28 10import collections
62e609ab 11import contextlib
e3946f98 12import ctypes
c496ca96
PH
13import datetime
14import email.utils
0c265486 15import email.header
f45c185f 16import errno
be4a824d 17import functools
d77c3dfd 18import gzip
49fa4d9a
N
19import hashlib
20import hmac
f74980cb 21import imp
03f9daab 22import io
79a2e94e 23import itertools
f4bfd65f 24import json
d77c3dfd 25import locale
02dbf93f 26import math
347de493 27import operator
d77c3dfd 28import os
c496ca96 29import platform
773f291d 30import random
d77c3dfd 31import re
c496ca96 32import socket
79a2e94e 33import ssl
1c088fa8 34import subprocess
d77c3dfd 35import sys
181c8655 36import tempfile
c380cc28 37import time
01951dda 38import traceback
bcf89ce6 39import xml.etree.ElementTree
d77c3dfd 40import zlib
d77c3dfd 41
8c25f81b 42from .compat import (
b4a3d461 43 compat_HTMLParseError,
8bb56eee 44 compat_HTMLParser,
201c1459 45 compat_HTTPError,
8f9312c3 46 compat_basestring,
8c25f81b 47 compat_chr,
1bab3437 48 compat_cookiejar,
d7cd9a9e 49 compat_ctypes_WINFUNCTYPE,
36e6f62c 50 compat_etree_fromstring,
51098426 51 compat_expanduser,
8c25f81b 52 compat_html_entities,
55b2f099 53 compat_html_entities_html5,
be4a824d 54 compat_http_client,
42db58ec 55 compat_integer_types,
e29663c6 56 compat_numeric_types,
c86b6142 57 compat_kwargs,
efa97bdc 58 compat_os_name,
8c25f81b 59 compat_parse_qs,
702ccf2d 60 compat_shlex_quote,
8c25f81b 61 compat_str,
edaa23f8 62 compat_struct_pack,
d3f8e038 63 compat_struct_unpack,
8c25f81b
PH
64 compat_urllib_error,
65 compat_urllib_parse,
15707c7e 66 compat_urllib_parse_urlencode,
8c25f81b 67 compat_urllib_parse_urlparse,
732044af 68 compat_urllib_parse_urlunparse,
69 compat_urllib_parse_quote,
70 compat_urllib_parse_quote_plus,
7581bfc9 71 compat_urllib_parse_unquote_plus,
8c25f81b
PH
72 compat_urllib_request,
73 compat_urlparse,
810c10ba 74 compat_xpath,
8c25f81b 75)
4644ac55 76
71aff188
YCH
77from .socks import (
78 ProxyType,
79 sockssocket,
80)
81
4644ac55 82
51fb4995
YCH
83def register_socks_protocols():
84 # "Register" SOCKS protocols
d5ae6bb5
YCH
85 # In Python < 2.6.5, urlsplit() suffers from bug https://bugs.python.org/issue7904
86 # URLs with protocols not in urlparse.uses_netloc are not handled correctly
51fb4995
YCH
87 for scheme in ('socks', 'socks4', 'socks4a', 'socks5'):
88 if scheme not in compat_urlparse.uses_netloc:
89 compat_urlparse.uses_netloc.append(scheme)
90
91
468e2e92
FV
92# This is not clearly defined otherwise
93compiled_regex_type = type(re.compile(''))
94
f7a147e3
S
95
96def random_user_agent():
97 _USER_AGENT_TPL = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36'
98 _CHROME_VERSIONS = (
99 '74.0.3729.129',
100 '76.0.3780.3',
101 '76.0.3780.2',
102 '74.0.3729.128',
103 '76.0.3780.1',
104 '76.0.3780.0',
105 '75.0.3770.15',
106 '74.0.3729.127',
107 '74.0.3729.126',
108 '76.0.3779.1',
109 '76.0.3779.0',
110 '75.0.3770.14',
111 '74.0.3729.125',
112 '76.0.3778.1',
113 '76.0.3778.0',
114 '75.0.3770.13',
115 '74.0.3729.124',
116 '74.0.3729.123',
117 '73.0.3683.121',
118 '76.0.3777.1',
119 '76.0.3777.0',
120 '75.0.3770.12',
121 '74.0.3729.122',
122 '76.0.3776.4',
123 '75.0.3770.11',
124 '74.0.3729.121',
125 '76.0.3776.3',
126 '76.0.3776.2',
127 '73.0.3683.120',
128 '74.0.3729.120',
129 '74.0.3729.119',
130 '74.0.3729.118',
131 '76.0.3776.1',
132 '76.0.3776.0',
133 '76.0.3775.5',
134 '75.0.3770.10',
135 '74.0.3729.117',
136 '76.0.3775.4',
137 '76.0.3775.3',
138 '74.0.3729.116',
139 '75.0.3770.9',
140 '76.0.3775.2',
141 '76.0.3775.1',
142 '76.0.3775.0',
143 '75.0.3770.8',
144 '74.0.3729.115',
145 '74.0.3729.114',
146 '76.0.3774.1',
147 '76.0.3774.0',
148 '75.0.3770.7',
149 '74.0.3729.113',
150 '74.0.3729.112',
151 '74.0.3729.111',
152 '76.0.3773.1',
153 '76.0.3773.0',
154 '75.0.3770.6',
155 '74.0.3729.110',
156 '74.0.3729.109',
157 '76.0.3772.1',
158 '76.0.3772.0',
159 '75.0.3770.5',
160 '74.0.3729.108',
161 '74.0.3729.107',
162 '76.0.3771.1',
163 '76.0.3771.0',
164 '75.0.3770.4',
165 '74.0.3729.106',
166 '74.0.3729.105',
167 '75.0.3770.3',
168 '74.0.3729.104',
169 '74.0.3729.103',
170 '74.0.3729.102',
171 '75.0.3770.2',
172 '74.0.3729.101',
173 '75.0.3770.1',
174 '75.0.3770.0',
175 '74.0.3729.100',
176 '75.0.3769.5',
177 '75.0.3769.4',
178 '74.0.3729.99',
179 '75.0.3769.3',
180 '75.0.3769.2',
181 '75.0.3768.6',
182 '74.0.3729.98',
183 '75.0.3769.1',
184 '75.0.3769.0',
185 '74.0.3729.97',
186 '73.0.3683.119',
187 '73.0.3683.118',
188 '74.0.3729.96',
189 '75.0.3768.5',
190 '75.0.3768.4',
191 '75.0.3768.3',
192 '75.0.3768.2',
193 '74.0.3729.95',
194 '74.0.3729.94',
195 '75.0.3768.1',
196 '75.0.3768.0',
197 '74.0.3729.93',
198 '74.0.3729.92',
199 '73.0.3683.117',
200 '74.0.3729.91',
201 '75.0.3766.3',
202 '74.0.3729.90',
203 '75.0.3767.2',
204 '75.0.3767.1',
205 '75.0.3767.0',
206 '74.0.3729.89',
207 '73.0.3683.116',
208 '75.0.3766.2',
209 '74.0.3729.88',
210 '75.0.3766.1',
211 '75.0.3766.0',
212 '74.0.3729.87',
213 '73.0.3683.115',
214 '74.0.3729.86',
215 '75.0.3765.1',
216 '75.0.3765.0',
217 '74.0.3729.85',
218 '73.0.3683.114',
219 '74.0.3729.84',
220 '75.0.3764.1',
221 '75.0.3764.0',
222 '74.0.3729.83',
223 '73.0.3683.113',
224 '75.0.3763.2',
225 '75.0.3761.4',
226 '74.0.3729.82',
227 '75.0.3763.1',
228 '75.0.3763.0',
229 '74.0.3729.81',
230 '73.0.3683.112',
231 '75.0.3762.1',
232 '75.0.3762.0',
233 '74.0.3729.80',
234 '75.0.3761.3',
235 '74.0.3729.79',
236 '73.0.3683.111',
237 '75.0.3761.2',
238 '74.0.3729.78',
239 '74.0.3729.77',
240 '75.0.3761.1',
241 '75.0.3761.0',
242 '73.0.3683.110',
243 '74.0.3729.76',
244 '74.0.3729.75',
245 '75.0.3760.0',
246 '74.0.3729.74',
247 '75.0.3759.8',
248 '75.0.3759.7',
249 '75.0.3759.6',
250 '74.0.3729.73',
251 '75.0.3759.5',
252 '74.0.3729.72',
253 '73.0.3683.109',
254 '75.0.3759.4',
255 '75.0.3759.3',
256 '74.0.3729.71',
257 '75.0.3759.2',
258 '74.0.3729.70',
259 '73.0.3683.108',
260 '74.0.3729.69',
261 '75.0.3759.1',
262 '75.0.3759.0',
263 '74.0.3729.68',
264 '73.0.3683.107',
265 '74.0.3729.67',
266 '75.0.3758.1',
267 '75.0.3758.0',
268 '74.0.3729.66',
269 '73.0.3683.106',
270 '74.0.3729.65',
271 '75.0.3757.1',
272 '75.0.3757.0',
273 '74.0.3729.64',
274 '73.0.3683.105',
275 '74.0.3729.63',
276 '75.0.3756.1',
277 '75.0.3756.0',
278 '74.0.3729.62',
279 '73.0.3683.104',
280 '75.0.3755.3',
281 '75.0.3755.2',
282 '73.0.3683.103',
283 '75.0.3755.1',
284 '75.0.3755.0',
285 '74.0.3729.61',
286 '73.0.3683.102',
287 '74.0.3729.60',
288 '75.0.3754.2',
289 '74.0.3729.59',
290 '75.0.3753.4',
291 '74.0.3729.58',
292 '75.0.3754.1',
293 '75.0.3754.0',
294 '74.0.3729.57',
295 '73.0.3683.101',
296 '75.0.3753.3',
297 '75.0.3752.2',
298 '75.0.3753.2',
299 '74.0.3729.56',
300 '75.0.3753.1',
301 '75.0.3753.0',
302 '74.0.3729.55',
303 '73.0.3683.100',
304 '74.0.3729.54',
305 '75.0.3752.1',
306 '75.0.3752.0',
307 '74.0.3729.53',
308 '73.0.3683.99',
309 '74.0.3729.52',
310 '75.0.3751.1',
311 '75.0.3751.0',
312 '74.0.3729.51',
313 '73.0.3683.98',
314 '74.0.3729.50',
315 '75.0.3750.0',
316 '74.0.3729.49',
317 '74.0.3729.48',
318 '74.0.3729.47',
319 '75.0.3749.3',
320 '74.0.3729.46',
321 '73.0.3683.97',
322 '75.0.3749.2',
323 '74.0.3729.45',
324 '75.0.3749.1',
325 '75.0.3749.0',
326 '74.0.3729.44',
327 '73.0.3683.96',
328 '74.0.3729.43',
329 '74.0.3729.42',
330 '75.0.3748.1',
331 '75.0.3748.0',
332 '74.0.3729.41',
333 '75.0.3747.1',
334 '73.0.3683.95',
335 '75.0.3746.4',
336 '74.0.3729.40',
337 '74.0.3729.39',
338 '75.0.3747.0',
339 '75.0.3746.3',
340 '75.0.3746.2',
341 '74.0.3729.38',
342 '75.0.3746.1',
343 '75.0.3746.0',
344 '74.0.3729.37',
345 '73.0.3683.94',
346 '75.0.3745.5',
347 '75.0.3745.4',
348 '75.0.3745.3',
349 '75.0.3745.2',
350 '74.0.3729.36',
351 '75.0.3745.1',
352 '75.0.3745.0',
353 '75.0.3744.2',
354 '74.0.3729.35',
355 '73.0.3683.93',
356 '74.0.3729.34',
357 '75.0.3744.1',
358 '75.0.3744.0',
359 '74.0.3729.33',
360 '73.0.3683.92',
361 '74.0.3729.32',
362 '74.0.3729.31',
363 '73.0.3683.91',
364 '75.0.3741.2',
365 '75.0.3740.5',
366 '74.0.3729.30',
367 '75.0.3741.1',
368 '75.0.3741.0',
369 '74.0.3729.29',
370 '75.0.3740.4',
371 '73.0.3683.90',
372 '74.0.3729.28',
373 '75.0.3740.3',
374 '73.0.3683.89',
375 '75.0.3740.2',
376 '74.0.3729.27',
377 '75.0.3740.1',
378 '75.0.3740.0',
379 '74.0.3729.26',
380 '73.0.3683.88',
381 '73.0.3683.87',
382 '74.0.3729.25',
383 '75.0.3739.1',
384 '75.0.3739.0',
385 '73.0.3683.86',
386 '74.0.3729.24',
387 '73.0.3683.85',
388 '75.0.3738.4',
389 '75.0.3738.3',
390 '75.0.3738.2',
391 '75.0.3738.1',
392 '75.0.3738.0',
393 '74.0.3729.23',
394 '73.0.3683.84',
395 '74.0.3729.22',
396 '74.0.3729.21',
397 '75.0.3737.1',
398 '75.0.3737.0',
399 '74.0.3729.20',
400 '73.0.3683.83',
401 '74.0.3729.19',
402 '75.0.3736.1',
403 '75.0.3736.0',
404 '74.0.3729.18',
405 '73.0.3683.82',
406 '74.0.3729.17',
407 '75.0.3735.1',
408 '75.0.3735.0',
409 '74.0.3729.16',
410 '73.0.3683.81',
411 '75.0.3734.1',
412 '75.0.3734.0',
413 '74.0.3729.15',
414 '73.0.3683.80',
415 '74.0.3729.14',
416 '75.0.3733.1',
417 '75.0.3733.0',
418 '75.0.3732.1',
419 '74.0.3729.13',
420 '74.0.3729.12',
421 '73.0.3683.79',
422 '74.0.3729.11',
423 '75.0.3732.0',
424 '74.0.3729.10',
425 '73.0.3683.78',
426 '74.0.3729.9',
427 '74.0.3729.8',
428 '74.0.3729.7',
429 '75.0.3731.3',
430 '75.0.3731.2',
431 '75.0.3731.0',
432 '74.0.3729.6',
433 '73.0.3683.77',
434 '73.0.3683.76',
435 '75.0.3730.5',
436 '75.0.3730.4',
437 '73.0.3683.75',
438 '74.0.3729.5',
439 '73.0.3683.74',
440 '75.0.3730.3',
441 '75.0.3730.2',
442 '74.0.3729.4',
443 '73.0.3683.73',
444 '73.0.3683.72',
445 '75.0.3730.1',
446 '75.0.3730.0',
447 '74.0.3729.3',
448 '73.0.3683.71',
449 '74.0.3729.2',
450 '73.0.3683.70',
451 '74.0.3729.1',
452 '74.0.3729.0',
453 '74.0.3726.4',
454 '73.0.3683.69',
455 '74.0.3726.3',
456 '74.0.3728.0',
457 '74.0.3726.2',
458 '73.0.3683.68',
459 '74.0.3726.1',
460 '74.0.3726.0',
461 '74.0.3725.4',
462 '73.0.3683.67',
463 '73.0.3683.66',
464 '74.0.3725.3',
465 '74.0.3725.2',
466 '74.0.3725.1',
467 '74.0.3724.8',
468 '74.0.3725.0',
469 '73.0.3683.65',
470 '74.0.3724.7',
471 '74.0.3724.6',
472 '74.0.3724.5',
473 '74.0.3724.4',
474 '74.0.3724.3',
475 '74.0.3724.2',
476 '74.0.3724.1',
477 '74.0.3724.0',
478 '73.0.3683.64',
479 '74.0.3723.1',
480 '74.0.3723.0',
481 '73.0.3683.63',
482 '74.0.3722.1',
483 '74.0.3722.0',
484 '73.0.3683.62',
485 '74.0.3718.9',
486 '74.0.3702.3',
487 '74.0.3721.3',
488 '74.0.3721.2',
489 '74.0.3721.1',
490 '74.0.3721.0',
491 '74.0.3720.6',
492 '73.0.3683.61',
493 '72.0.3626.122',
494 '73.0.3683.60',
495 '74.0.3720.5',
496 '72.0.3626.121',
497 '74.0.3718.8',
498 '74.0.3720.4',
499 '74.0.3720.3',
500 '74.0.3718.7',
501 '74.0.3720.2',
502 '74.0.3720.1',
503 '74.0.3720.0',
504 '74.0.3718.6',
505 '74.0.3719.5',
506 '73.0.3683.59',
507 '74.0.3718.5',
508 '74.0.3718.4',
509 '74.0.3719.4',
510 '74.0.3719.3',
511 '74.0.3719.2',
512 '74.0.3719.1',
513 '73.0.3683.58',
514 '74.0.3719.0',
515 '73.0.3683.57',
516 '73.0.3683.56',
517 '74.0.3718.3',
518 '73.0.3683.55',
519 '74.0.3718.2',
520 '74.0.3718.1',
521 '74.0.3718.0',
522 '73.0.3683.54',
523 '74.0.3717.2',
524 '73.0.3683.53',
525 '74.0.3717.1',
526 '74.0.3717.0',
527 '73.0.3683.52',
528 '74.0.3716.1',
529 '74.0.3716.0',
530 '73.0.3683.51',
531 '74.0.3715.1',
532 '74.0.3715.0',
533 '73.0.3683.50',
534 '74.0.3711.2',
535 '74.0.3714.2',
536 '74.0.3713.3',
537 '74.0.3714.1',
538 '74.0.3714.0',
539 '73.0.3683.49',
540 '74.0.3713.1',
541 '74.0.3713.0',
542 '72.0.3626.120',
543 '73.0.3683.48',
544 '74.0.3712.2',
545 '74.0.3712.1',
546 '74.0.3712.0',
547 '73.0.3683.47',
548 '72.0.3626.119',
549 '73.0.3683.46',
550 '74.0.3710.2',
551 '72.0.3626.118',
552 '74.0.3711.1',
553 '74.0.3711.0',
554 '73.0.3683.45',
555 '72.0.3626.117',
556 '74.0.3710.1',
557 '74.0.3710.0',
558 '73.0.3683.44',
559 '72.0.3626.116',
560 '74.0.3709.1',
561 '74.0.3709.0',
562 '74.0.3704.9',
563 '73.0.3683.43',
564 '72.0.3626.115',
565 '74.0.3704.8',
566 '74.0.3704.7',
567 '74.0.3708.0',
568 '74.0.3706.7',
569 '74.0.3704.6',
570 '73.0.3683.42',
571 '72.0.3626.114',
572 '74.0.3706.6',
573 '72.0.3626.113',
574 '74.0.3704.5',
575 '74.0.3706.5',
576 '74.0.3706.4',
577 '74.0.3706.3',
578 '74.0.3706.2',
579 '74.0.3706.1',
580 '74.0.3706.0',
581 '73.0.3683.41',
582 '72.0.3626.112',
583 '74.0.3705.1',
584 '74.0.3705.0',
585 '73.0.3683.40',
586 '72.0.3626.111',
587 '73.0.3683.39',
588 '74.0.3704.4',
589 '73.0.3683.38',
590 '74.0.3704.3',
591 '74.0.3704.2',
592 '74.0.3704.1',
593 '74.0.3704.0',
594 '73.0.3683.37',
595 '72.0.3626.110',
596 '72.0.3626.109',
597 '74.0.3703.3',
598 '74.0.3703.2',
599 '73.0.3683.36',
600 '74.0.3703.1',
601 '74.0.3703.0',
602 '73.0.3683.35',
603 '72.0.3626.108',
604 '74.0.3702.2',
605 '74.0.3699.3',
606 '74.0.3702.1',
607 '74.0.3702.0',
608 '73.0.3683.34',
609 '72.0.3626.107',
610 '73.0.3683.33',
611 '74.0.3701.1',
612 '74.0.3701.0',
613 '73.0.3683.32',
614 '73.0.3683.31',
615 '72.0.3626.105',
616 '74.0.3700.1',
617 '74.0.3700.0',
618 '73.0.3683.29',
619 '72.0.3626.103',
620 '74.0.3699.2',
621 '74.0.3699.1',
622 '74.0.3699.0',
623 '73.0.3683.28',
624 '72.0.3626.102',
625 '73.0.3683.27',
626 '73.0.3683.26',
627 '74.0.3698.0',
628 '74.0.3696.2',
629 '72.0.3626.101',
630 '73.0.3683.25',
631 '74.0.3696.1',
632 '74.0.3696.0',
633 '74.0.3694.8',
634 '72.0.3626.100',
635 '74.0.3694.7',
636 '74.0.3694.6',
637 '74.0.3694.5',
638 '74.0.3694.4',
639 '72.0.3626.99',
640 '72.0.3626.98',
641 '74.0.3694.3',
642 '73.0.3683.24',
643 '72.0.3626.97',
644 '72.0.3626.96',
645 '72.0.3626.95',
646 '73.0.3683.23',
647 '72.0.3626.94',
648 '73.0.3683.22',
649 '73.0.3683.21',
650 '72.0.3626.93',
651 '74.0.3694.2',
652 '72.0.3626.92',
653 '74.0.3694.1',
654 '74.0.3694.0',
655 '74.0.3693.6',
656 '73.0.3683.20',
657 '72.0.3626.91',
658 '74.0.3693.5',
659 '74.0.3693.4',
660 '74.0.3693.3',
661 '74.0.3693.2',
662 '73.0.3683.19',
663 '74.0.3693.1',
664 '74.0.3693.0',
665 '73.0.3683.18',
666 '72.0.3626.90',
667 '74.0.3692.1',
668 '74.0.3692.0',
669 '73.0.3683.17',
670 '72.0.3626.89',
671 '74.0.3687.3',
672 '74.0.3691.1',
673 '74.0.3691.0',
674 '73.0.3683.16',
675 '72.0.3626.88',
676 '72.0.3626.87',
677 '73.0.3683.15',
678 '74.0.3690.1',
679 '74.0.3690.0',
680 '73.0.3683.14',
681 '72.0.3626.86',
682 '73.0.3683.13',
683 '73.0.3683.12',
684 '74.0.3689.1',
685 '74.0.3689.0',
686 '73.0.3683.11',
687 '72.0.3626.85',
688 '73.0.3683.10',
689 '72.0.3626.84',
690 '73.0.3683.9',
691 '74.0.3688.1',
692 '74.0.3688.0',
693 '73.0.3683.8',
694 '72.0.3626.83',
695 '74.0.3687.2',
696 '74.0.3687.1',
697 '74.0.3687.0',
698 '73.0.3683.7',
699 '72.0.3626.82',
700 '74.0.3686.4',
701 '72.0.3626.81',
702 '74.0.3686.3',
703 '74.0.3686.2',
704 '74.0.3686.1',
705 '74.0.3686.0',
706 '73.0.3683.6',
707 '72.0.3626.80',
708 '74.0.3685.1',
709 '74.0.3685.0',
710 '73.0.3683.5',
711 '72.0.3626.79',
712 '74.0.3684.1',
713 '74.0.3684.0',
714 '73.0.3683.4',
715 '72.0.3626.78',
716 '72.0.3626.77',
717 '73.0.3683.3',
718 '73.0.3683.2',
719 '72.0.3626.76',
720 '73.0.3683.1',
721 '73.0.3683.0',
722 '72.0.3626.75',
723 '71.0.3578.141',
724 '73.0.3682.1',
725 '73.0.3682.0',
726 '72.0.3626.74',
727 '71.0.3578.140',
728 '73.0.3681.4',
729 '73.0.3681.3',
730 '73.0.3681.2',
731 '73.0.3681.1',
732 '73.0.3681.0',
733 '72.0.3626.73',
734 '71.0.3578.139',
735 '72.0.3626.72',
736 '72.0.3626.71',
737 '73.0.3680.1',
738 '73.0.3680.0',
739 '72.0.3626.70',
740 '71.0.3578.138',
741 '73.0.3678.2',
742 '73.0.3679.1',
743 '73.0.3679.0',
744 '72.0.3626.69',
745 '71.0.3578.137',
746 '73.0.3678.1',
747 '73.0.3678.0',
748 '71.0.3578.136',
749 '73.0.3677.1',
750 '73.0.3677.0',
751 '72.0.3626.68',
752 '72.0.3626.67',
753 '71.0.3578.135',
754 '73.0.3676.1',
755 '73.0.3676.0',
756 '73.0.3674.2',
757 '72.0.3626.66',
758 '71.0.3578.134',
759 '73.0.3674.1',
760 '73.0.3674.0',
761 '72.0.3626.65',
762 '71.0.3578.133',
763 '73.0.3673.2',
764 '73.0.3673.1',
765 '73.0.3673.0',
766 '72.0.3626.64',
767 '71.0.3578.132',
768 '72.0.3626.63',
769 '72.0.3626.62',
770 '72.0.3626.61',
771 '72.0.3626.60',
772 '73.0.3672.1',
773 '73.0.3672.0',
774 '72.0.3626.59',
775 '71.0.3578.131',
776 '73.0.3671.3',
777 '73.0.3671.2',
778 '73.0.3671.1',
779 '73.0.3671.0',
780 '72.0.3626.58',
781 '71.0.3578.130',
782 '73.0.3670.1',
783 '73.0.3670.0',
784 '72.0.3626.57',
785 '71.0.3578.129',
786 '73.0.3669.1',
787 '73.0.3669.0',
788 '72.0.3626.56',
789 '71.0.3578.128',
790 '73.0.3668.2',
791 '73.0.3668.1',
792 '73.0.3668.0',
793 '72.0.3626.55',
794 '71.0.3578.127',
795 '73.0.3667.2',
796 '73.0.3667.1',
797 '73.0.3667.0',
798 '72.0.3626.54',
799 '71.0.3578.126',
800 '73.0.3666.1',
801 '73.0.3666.0',
802 '72.0.3626.53',
803 '71.0.3578.125',
804 '73.0.3665.4',
805 '73.0.3665.3',
806 '72.0.3626.52',
807 '73.0.3665.2',
808 '73.0.3664.4',
809 '73.0.3665.1',
810 '73.0.3665.0',
811 '72.0.3626.51',
812 '71.0.3578.124',
813 '72.0.3626.50',
814 '73.0.3664.3',
815 '73.0.3664.2',
816 '73.0.3664.1',
817 '73.0.3664.0',
818 '73.0.3663.2',
819 '72.0.3626.49',
820 '71.0.3578.123',
821 '73.0.3663.1',
822 '73.0.3663.0',
823 '72.0.3626.48',
824 '71.0.3578.122',
825 '73.0.3662.1',
826 '73.0.3662.0',
827 '72.0.3626.47',
828 '71.0.3578.121',
829 '73.0.3661.1',
830 '72.0.3626.46',
831 '73.0.3661.0',
832 '72.0.3626.45',
833 '71.0.3578.120',
834 '73.0.3660.2',
835 '73.0.3660.1',
836 '73.0.3660.0',
837 '72.0.3626.44',
838 '71.0.3578.119',
839 '73.0.3659.1',
840 '73.0.3659.0',
841 '72.0.3626.43',
842 '71.0.3578.118',
843 '73.0.3658.1',
844 '73.0.3658.0',
845 '72.0.3626.42',
846 '71.0.3578.117',
847 '73.0.3657.1',
848 '73.0.3657.0',
849 '72.0.3626.41',
850 '71.0.3578.116',
851 '73.0.3656.1',
852 '73.0.3656.0',
853 '72.0.3626.40',
854 '71.0.3578.115',
855 '73.0.3655.1',
856 '73.0.3655.0',
857 '72.0.3626.39',
858 '71.0.3578.114',
859 '73.0.3654.1',
860 '73.0.3654.0',
861 '72.0.3626.38',
862 '71.0.3578.113',
863 '73.0.3653.1',
864 '73.0.3653.0',
865 '72.0.3626.37',
866 '71.0.3578.112',
867 '73.0.3652.1',
868 '73.0.3652.0',
869 '72.0.3626.36',
870 '71.0.3578.111',
871 '73.0.3651.1',
872 '73.0.3651.0',
873 '72.0.3626.35',
874 '71.0.3578.110',
875 '73.0.3650.1',
876 '73.0.3650.0',
877 '72.0.3626.34',
878 '71.0.3578.109',
879 '73.0.3649.1',
880 '73.0.3649.0',
881 '72.0.3626.33',
882 '71.0.3578.108',
883 '73.0.3648.2',
884 '73.0.3648.1',
885 '73.0.3648.0',
886 '72.0.3626.32',
887 '71.0.3578.107',
888 '73.0.3647.2',
889 '73.0.3647.1',
890 '73.0.3647.0',
891 '72.0.3626.31',
892 '71.0.3578.106',
893 '73.0.3635.3',
894 '73.0.3646.2',
895 '73.0.3646.1',
896 '73.0.3646.0',
897 '72.0.3626.30',
898 '71.0.3578.105',
899 '72.0.3626.29',
900 '73.0.3645.2',
901 '73.0.3645.1',
902 '73.0.3645.0',
903 '72.0.3626.28',
904 '71.0.3578.104',
905 '72.0.3626.27',
906 '72.0.3626.26',
907 '72.0.3626.25',
908 '72.0.3626.24',
909 '73.0.3644.0',
910 '73.0.3643.2',
911 '72.0.3626.23',
912 '71.0.3578.103',
913 '73.0.3643.1',
914 '73.0.3643.0',
915 '72.0.3626.22',
916 '71.0.3578.102',
917 '73.0.3642.1',
918 '73.0.3642.0',
919 '72.0.3626.21',
920 '71.0.3578.101',
921 '73.0.3641.1',
922 '73.0.3641.0',
923 '72.0.3626.20',
924 '71.0.3578.100',
925 '72.0.3626.19',
926 '73.0.3640.1',
927 '73.0.3640.0',
928 '72.0.3626.18',
929 '73.0.3639.1',
930 '71.0.3578.99',
931 '73.0.3639.0',
932 '72.0.3626.17',
933 '73.0.3638.2',
934 '72.0.3626.16',
935 '73.0.3638.1',
936 '73.0.3638.0',
937 '72.0.3626.15',
938 '71.0.3578.98',
939 '73.0.3635.2',
940 '71.0.3578.97',
941 '73.0.3637.1',
942 '73.0.3637.0',
943 '72.0.3626.14',
944 '71.0.3578.96',
945 '71.0.3578.95',
946 '72.0.3626.13',
947 '71.0.3578.94',
948 '73.0.3636.2',
949 '71.0.3578.93',
950 '73.0.3636.1',
951 '73.0.3636.0',
952 '72.0.3626.12',
953 '71.0.3578.92',
954 '73.0.3635.1',
955 '73.0.3635.0',
956 '72.0.3626.11',
957 '71.0.3578.91',
958 '73.0.3634.2',
959 '73.0.3634.1',
960 '73.0.3634.0',
961 '72.0.3626.10',
962 '71.0.3578.90',
963 '71.0.3578.89',
964 '73.0.3633.2',
965 '73.0.3633.1',
966 '73.0.3633.0',
967 '72.0.3610.4',
968 '72.0.3626.9',
969 '71.0.3578.88',
970 '73.0.3632.5',
971 '73.0.3632.4',
972 '73.0.3632.3',
973 '73.0.3632.2',
974 '73.0.3632.1',
975 '73.0.3632.0',
976 '72.0.3626.8',
977 '71.0.3578.87',
978 '73.0.3631.2',
979 '73.0.3631.1',
980 '73.0.3631.0',
981 '72.0.3626.7',
982 '71.0.3578.86',
983 '72.0.3626.6',
984 '73.0.3630.1',
985 '73.0.3630.0',
986 '72.0.3626.5',
987 '71.0.3578.85',
988 '72.0.3626.4',
989 '73.0.3628.3',
990 '73.0.3628.2',
991 '73.0.3629.1',
992 '73.0.3629.0',
993 '72.0.3626.3',
994 '71.0.3578.84',
995 '73.0.3628.1',
996 '73.0.3628.0',
997 '71.0.3578.83',
998 '73.0.3627.1',
999 '73.0.3627.0',
1000 '72.0.3626.2',
1001 '71.0.3578.82',
1002 '71.0.3578.81',
1003 '71.0.3578.80',
1004 '72.0.3626.1',
1005 '72.0.3626.0',
1006 '71.0.3578.79',
1007 '70.0.3538.124',
1008 '71.0.3578.78',
1009 '72.0.3623.4',
1010 '72.0.3625.2',
1011 '72.0.3625.1',
1012 '72.0.3625.0',
1013 '71.0.3578.77',
1014 '70.0.3538.123',
1015 '72.0.3624.4',
1016 '72.0.3624.3',
1017 '72.0.3624.2',
1018 '71.0.3578.76',
1019 '72.0.3624.1',
1020 '72.0.3624.0',
1021 '72.0.3623.3',
1022 '71.0.3578.75',
1023 '70.0.3538.122',
1024 '71.0.3578.74',
1025 '72.0.3623.2',
1026 '72.0.3610.3',
1027 '72.0.3623.1',
1028 '72.0.3623.0',
1029 '72.0.3622.3',
1030 '72.0.3622.2',
1031 '71.0.3578.73',
1032 '70.0.3538.121',
1033 '72.0.3622.1',
1034 '72.0.3622.0',
1035 '71.0.3578.72',
1036 '70.0.3538.120',
1037 '72.0.3621.1',
1038 '72.0.3621.0',
1039 '71.0.3578.71',
1040 '70.0.3538.119',
1041 '72.0.3620.1',
1042 '72.0.3620.0',
1043 '71.0.3578.70',
1044 '70.0.3538.118',
1045 '71.0.3578.69',
1046 '72.0.3619.1',
1047 '72.0.3619.0',
1048 '71.0.3578.68',
1049 '70.0.3538.117',
1050 '71.0.3578.67',
1051 '72.0.3618.1',
1052 '72.0.3618.0',
1053 '71.0.3578.66',
1054 '70.0.3538.116',
1055 '72.0.3617.1',
1056 '72.0.3617.0',
1057 '71.0.3578.65',
1058 '70.0.3538.115',
1059 '72.0.3602.3',
1060 '71.0.3578.64',
1061 '72.0.3616.1',
1062 '72.0.3616.0',
1063 '71.0.3578.63',
1064 '70.0.3538.114',
1065 '71.0.3578.62',
1066 '72.0.3615.1',
1067 '72.0.3615.0',
1068 '71.0.3578.61',
1069 '70.0.3538.113',
1070 '72.0.3614.1',
1071 '72.0.3614.0',
1072 '71.0.3578.60',
1073 '70.0.3538.112',
1074 '72.0.3613.1',
1075 '72.0.3613.0',
1076 '71.0.3578.59',
1077 '70.0.3538.111',
1078 '72.0.3612.2',
1079 '72.0.3612.1',
1080 '72.0.3612.0',
1081 '70.0.3538.110',
1082 '71.0.3578.58',
1083 '70.0.3538.109',
1084 '72.0.3611.2',
1085 '72.0.3611.1',
1086 '72.0.3611.0',
1087 '71.0.3578.57',
1088 '70.0.3538.108',
1089 '72.0.3610.2',
1090 '71.0.3578.56',
1091 '71.0.3578.55',
1092 '72.0.3610.1',
1093 '72.0.3610.0',
1094 '71.0.3578.54',
1095 '70.0.3538.107',
1096 '71.0.3578.53',
1097 '72.0.3609.3',
1098 '71.0.3578.52',
1099 '72.0.3609.2',
1100 '71.0.3578.51',
1101 '72.0.3608.5',
1102 '72.0.3609.1',
1103 '72.0.3609.0',
1104 '71.0.3578.50',
1105 '70.0.3538.106',
1106 '72.0.3608.4',
1107 '72.0.3608.3',
1108 '72.0.3608.2',
1109 '71.0.3578.49',
1110 '72.0.3608.1',
1111 '72.0.3608.0',
1112 '70.0.3538.105',
1113 '71.0.3578.48',
1114 '72.0.3607.1',
1115 '72.0.3607.0',
1116 '71.0.3578.47',
1117 '70.0.3538.104',
1118 '72.0.3606.2',
1119 '72.0.3606.1',
1120 '72.0.3606.0',
1121 '71.0.3578.46',
1122 '70.0.3538.103',
1123 '70.0.3538.102',
1124 '72.0.3605.3',
1125 '72.0.3605.2',
1126 '72.0.3605.1',
1127 '72.0.3605.0',
1128 '71.0.3578.45',
1129 '70.0.3538.101',
1130 '71.0.3578.44',
1131 '71.0.3578.43',
1132 '70.0.3538.100',
1133 '70.0.3538.99',
1134 '71.0.3578.42',
1135 '72.0.3604.1',
1136 '72.0.3604.0',
1137 '71.0.3578.41',
1138 '70.0.3538.98',
1139 '71.0.3578.40',
1140 '72.0.3603.2',
1141 '72.0.3603.1',
1142 '72.0.3603.0',
1143 '71.0.3578.39',
1144 '70.0.3538.97',
1145 '72.0.3602.2',
1146 '71.0.3578.38',
1147 '71.0.3578.37',
1148 '72.0.3602.1',
1149 '72.0.3602.0',
1150 '71.0.3578.36',
1151 '70.0.3538.96',
1152 '72.0.3601.1',
1153 '72.0.3601.0',
1154 '71.0.3578.35',
1155 '70.0.3538.95',
1156 '72.0.3600.1',
1157 '72.0.3600.0',
1158 '71.0.3578.34',
1159 '70.0.3538.94',
1160 '72.0.3599.3',
1161 '72.0.3599.2',
1162 '72.0.3599.1',
1163 '72.0.3599.0',
1164 '71.0.3578.33',
1165 '70.0.3538.93',
1166 '72.0.3598.1',
1167 '72.0.3598.0',
1168 '71.0.3578.32',
1169 '70.0.3538.87',
1170 '72.0.3597.1',
1171 '72.0.3597.0',
1172 '72.0.3596.2',
1173 '71.0.3578.31',
1174 '70.0.3538.86',
1175 '71.0.3578.30',
1176 '71.0.3578.29',
1177 '72.0.3596.1',
1178 '72.0.3596.0',
1179 '71.0.3578.28',
1180 '70.0.3538.85',
1181 '72.0.3595.2',
1182 '72.0.3591.3',
1183 '72.0.3595.1',
1184 '72.0.3595.0',
1185 '71.0.3578.27',
1186 '70.0.3538.84',
1187 '72.0.3594.1',
1188 '72.0.3594.0',
1189 '71.0.3578.26',
1190 '70.0.3538.83',
1191 '72.0.3593.2',
1192 '72.0.3593.1',
1193 '72.0.3593.0',
1194 '71.0.3578.25',
1195 '70.0.3538.82',
1196 '72.0.3589.3',
1197 '72.0.3592.2',
1198 '72.0.3592.1',
1199 '72.0.3592.0',
1200 '71.0.3578.24',
1201 '72.0.3589.2',
1202 '70.0.3538.81',
1203 '70.0.3538.80',
1204 '72.0.3591.2',
1205 '72.0.3591.1',
1206 '72.0.3591.0',
1207 '71.0.3578.23',
1208 '70.0.3538.79',
1209 '71.0.3578.22',
1210 '72.0.3590.1',
1211 '72.0.3590.0',
1212 '71.0.3578.21',
1213 '70.0.3538.78',
1214 '70.0.3538.77',
1215 '72.0.3589.1',
1216 '72.0.3589.0',
1217 '71.0.3578.20',
1218 '70.0.3538.76',
1219 '71.0.3578.19',
1220 '70.0.3538.75',
1221 '72.0.3588.1',
1222 '72.0.3588.0',
1223 '71.0.3578.18',
1224 '70.0.3538.74',
1225 '72.0.3586.2',
1226 '72.0.3587.0',
1227 '71.0.3578.17',
1228 '70.0.3538.73',
1229 '72.0.3586.1',
1230 '72.0.3586.0',
1231 '71.0.3578.16',
1232 '70.0.3538.72',
1233 '72.0.3585.1',
1234 '72.0.3585.0',
1235 '71.0.3578.15',
1236 '70.0.3538.71',
1237 '71.0.3578.14',
1238 '72.0.3584.1',
1239 '72.0.3584.0',
1240 '71.0.3578.13',
1241 '70.0.3538.70',
1242 '72.0.3583.2',
1243 '71.0.3578.12',
1244 '72.0.3583.1',
1245 '72.0.3583.0',
1246 '71.0.3578.11',
1247 '70.0.3538.69',
1248 '71.0.3578.10',
1249 '72.0.3582.0',
1250 '72.0.3581.4',
1251 '71.0.3578.9',
1252 '70.0.3538.67',
1253 '72.0.3581.3',
1254 '72.0.3581.2',
1255 '72.0.3581.1',
1256 '72.0.3581.0',
1257 '71.0.3578.8',
1258 '70.0.3538.66',
1259 '72.0.3580.1',
1260 '72.0.3580.0',
1261 '71.0.3578.7',
1262 '70.0.3538.65',
1263 '71.0.3578.6',
1264 '72.0.3579.1',
1265 '72.0.3579.0',
1266 '71.0.3578.5',
1267 '70.0.3538.64',
1268 '71.0.3578.4',
1269 '71.0.3578.3',
1270 '71.0.3578.2',
1271 '71.0.3578.1',
1272 '71.0.3578.0',
1273 '70.0.3538.63',
1274 '69.0.3497.128',
1275 '70.0.3538.62',
1276 '70.0.3538.61',
1277 '70.0.3538.60',
1278 '70.0.3538.59',
1279 '71.0.3577.1',
1280 '71.0.3577.0',
1281 '70.0.3538.58',
1282 '69.0.3497.127',
1283 '71.0.3576.2',
1284 '71.0.3576.1',
1285 '71.0.3576.0',
1286 '70.0.3538.57',
1287 '70.0.3538.56',
1288 '71.0.3575.2',
1289 '70.0.3538.55',
1290 '69.0.3497.126',
1291 '70.0.3538.54',
1292 '71.0.3575.1',
1293 '71.0.3575.0',
1294 '71.0.3574.1',
1295 '71.0.3574.0',
1296 '70.0.3538.53',
1297 '69.0.3497.125',
1298 '70.0.3538.52',
1299 '71.0.3573.1',
1300 '71.0.3573.0',
1301 '70.0.3538.51',
1302 '69.0.3497.124',
1303 '71.0.3572.1',
1304 '71.0.3572.0',
1305 '70.0.3538.50',
1306 '69.0.3497.123',
1307 '71.0.3571.2',
1308 '70.0.3538.49',
1309 '69.0.3497.122',
1310 '71.0.3571.1',
1311 '71.0.3571.0',
1312 '70.0.3538.48',
1313 '69.0.3497.121',
1314 '71.0.3570.1',
1315 '71.0.3570.0',
1316 '70.0.3538.47',
1317 '69.0.3497.120',
1318 '71.0.3568.2',
1319 '71.0.3569.1',
1320 '71.0.3569.0',
1321 '70.0.3538.46',
1322 '69.0.3497.119',
1323 '70.0.3538.45',
1324 '71.0.3568.1',
1325 '71.0.3568.0',
1326 '70.0.3538.44',
1327 '69.0.3497.118',
1328 '70.0.3538.43',
1329 '70.0.3538.42',
1330 '71.0.3567.1',
1331 '71.0.3567.0',
1332 '70.0.3538.41',
1333 '69.0.3497.117',
1334 '71.0.3566.1',
1335 '71.0.3566.0',
1336 '70.0.3538.40',
1337 '69.0.3497.116',
1338 '71.0.3565.1',
1339 '71.0.3565.0',
1340 '70.0.3538.39',
1341 '69.0.3497.115',
1342 '71.0.3564.1',
1343 '71.0.3564.0',
1344 '70.0.3538.38',
1345 '69.0.3497.114',
1346 '71.0.3563.0',
1347 '71.0.3562.2',
1348 '70.0.3538.37',
1349 '69.0.3497.113',
1350 '70.0.3538.36',
1351 '70.0.3538.35',
1352 '71.0.3562.1',
1353 '71.0.3562.0',
1354 '70.0.3538.34',
1355 '69.0.3497.112',
1356 '70.0.3538.33',
1357 '71.0.3561.1',
1358 '71.0.3561.0',
1359 '70.0.3538.32',
1360 '69.0.3497.111',
1361 '71.0.3559.6',
1362 '71.0.3560.1',
1363 '71.0.3560.0',
1364 '71.0.3559.5',
1365 '71.0.3559.4',
1366 '70.0.3538.31',
1367 '69.0.3497.110',
1368 '71.0.3559.3',
1369 '70.0.3538.30',
1370 '69.0.3497.109',
1371 '71.0.3559.2',
1372 '71.0.3559.1',
1373 '71.0.3559.0',
1374 '70.0.3538.29',
1375 '69.0.3497.108',
1376 '71.0.3558.2',
1377 '71.0.3558.1',
1378 '71.0.3558.0',
1379 '70.0.3538.28',
1380 '69.0.3497.107',
1381 '71.0.3557.2',
1382 '71.0.3557.1',
1383 '71.0.3557.0',
1384 '70.0.3538.27',
1385 '69.0.3497.106',
1386 '71.0.3554.4',
1387 '70.0.3538.26',
1388 '71.0.3556.1',
1389 '71.0.3556.0',
1390 '70.0.3538.25',
1391 '71.0.3554.3',
1392 '69.0.3497.105',
1393 '71.0.3554.2',
1394 '70.0.3538.24',
1395 '69.0.3497.104',
1396 '71.0.3555.2',
1397 '70.0.3538.23',
1398 '71.0.3555.1',
1399 '71.0.3555.0',
1400 '70.0.3538.22',
1401 '69.0.3497.103',
1402 '71.0.3554.1',
1403 '71.0.3554.0',
1404 '70.0.3538.21',
1405 '69.0.3497.102',
1406 '71.0.3553.3',
1407 '70.0.3538.20',
1408 '69.0.3497.101',
1409 '71.0.3553.2',
1410 '69.0.3497.100',
1411 '71.0.3553.1',
1412 '71.0.3553.0',
1413 '70.0.3538.19',
1414 '69.0.3497.99',
1415 '69.0.3497.98',
1416 '69.0.3497.97',
1417 '71.0.3552.6',
1418 '71.0.3552.5',
1419 '71.0.3552.4',
1420 '71.0.3552.3',
1421 '71.0.3552.2',
1422 '71.0.3552.1',
1423 '71.0.3552.0',
1424 '70.0.3538.18',
1425 '69.0.3497.96',
1426 '71.0.3551.3',
1427 '71.0.3551.2',
1428 '71.0.3551.1',
1429 '71.0.3551.0',
1430 '70.0.3538.17',
1431 '69.0.3497.95',
1432 '71.0.3550.3',
1433 '71.0.3550.2',
1434 '71.0.3550.1',
1435 '71.0.3550.0',
1436 '70.0.3538.16',
1437 '69.0.3497.94',
1438 '71.0.3549.1',
1439 '71.0.3549.0',
1440 '70.0.3538.15',
1441 '69.0.3497.93',
1442 '69.0.3497.92',
1443 '71.0.3548.1',
1444 '71.0.3548.0',
1445 '70.0.3538.14',
1446 '69.0.3497.91',
1447 '71.0.3547.1',
1448 '71.0.3547.0',
1449 '70.0.3538.13',
1450 '69.0.3497.90',
1451 '71.0.3546.2',
1452 '69.0.3497.89',
1453 '71.0.3546.1',
1454 '71.0.3546.0',
1455 '70.0.3538.12',
1456 '69.0.3497.88',
1457 '71.0.3545.4',
1458 '71.0.3545.3',
1459 '71.0.3545.2',
1460 '71.0.3545.1',
1461 '71.0.3545.0',
1462 '70.0.3538.11',
1463 '69.0.3497.87',
1464 '71.0.3544.5',
1465 '71.0.3544.4',
1466 '71.0.3544.3',
1467 '71.0.3544.2',
1468 '71.0.3544.1',
1469 '71.0.3544.0',
1470 '69.0.3497.86',
1471 '70.0.3538.10',
1472 '69.0.3497.85',
1473 '70.0.3538.9',
1474 '69.0.3497.84',
1475 '71.0.3543.4',
1476 '70.0.3538.8',
1477 '71.0.3543.3',
1478 '71.0.3543.2',
1479 '71.0.3543.1',
1480 '71.0.3543.0',
1481 '70.0.3538.7',
1482 '69.0.3497.83',
1483 '71.0.3542.2',
1484 '71.0.3542.1',
1485 '71.0.3542.0',
1486 '70.0.3538.6',
1487 '69.0.3497.82',
1488 '69.0.3497.81',
1489 '71.0.3541.1',
1490 '71.0.3541.0',
1491 '70.0.3538.5',
1492 '69.0.3497.80',
1493 '71.0.3540.1',
1494 '71.0.3540.0',
1495 '70.0.3538.4',
1496 '69.0.3497.79',
1497 '70.0.3538.3',
1498 '71.0.3539.1',
1499 '71.0.3539.0',
1500 '69.0.3497.78',
1501 '68.0.3440.134',
1502 '69.0.3497.77',
1503 '70.0.3538.2',
1504 '70.0.3538.1',
1505 '70.0.3538.0',
1506 '69.0.3497.76',
1507 '68.0.3440.133',
1508 '69.0.3497.75',
1509 '70.0.3537.2',
1510 '70.0.3537.1',
1511 '70.0.3537.0',
1512 '69.0.3497.74',
1513 '68.0.3440.132',
1514 '70.0.3536.0',
1515 '70.0.3535.5',
1516 '70.0.3535.4',
1517 '70.0.3535.3',
1518 '69.0.3497.73',
1519 '68.0.3440.131',
1520 '70.0.3532.8',
1521 '70.0.3532.7',
1522 '69.0.3497.72',
1523 '69.0.3497.71',
1524 '70.0.3535.2',
1525 '70.0.3535.1',
1526 '70.0.3535.0',
1527 '69.0.3497.70',
1528 '68.0.3440.130',
1529 '69.0.3497.69',
1530 '68.0.3440.129',
1531 '70.0.3534.4',
1532 '70.0.3534.3',
1533 '70.0.3534.2',
1534 '70.0.3534.1',
1535 '70.0.3534.0',
1536 '69.0.3497.68',
1537 '68.0.3440.128',
1538 '70.0.3533.2',
1539 '70.0.3533.1',
1540 '70.0.3533.0',
1541 '69.0.3497.67',
1542 '68.0.3440.127',
1543 '70.0.3532.6',
1544 '70.0.3532.5',
1545 '70.0.3532.4',
1546 '69.0.3497.66',
1547 '68.0.3440.126',
1548 '70.0.3532.3',
1549 '70.0.3532.2',
1550 '70.0.3532.1',
1551 '69.0.3497.60',
1552 '69.0.3497.65',
1553 '69.0.3497.64',
1554 '70.0.3532.0',
1555 '70.0.3531.0',
1556 '70.0.3530.4',
1557 '70.0.3530.3',
1558 '70.0.3530.2',
1559 '69.0.3497.58',
1560 '68.0.3440.125',
1561 '69.0.3497.57',
1562 '69.0.3497.56',
1563 '69.0.3497.55',
1564 '69.0.3497.54',
1565 '70.0.3530.1',
1566 '70.0.3530.0',
1567 '69.0.3497.53',
1568 '68.0.3440.124',
1569 '69.0.3497.52',
1570 '70.0.3529.3',
1571 '70.0.3529.2',
1572 '70.0.3529.1',
1573 '70.0.3529.0',
1574 '69.0.3497.51',
1575 '70.0.3528.4',
1576 '68.0.3440.123',
1577 '70.0.3528.3',
1578 '70.0.3528.2',
1579 '70.0.3528.1',
1580 '70.0.3528.0',
1581 '69.0.3497.50',
1582 '68.0.3440.122',
1583 '70.0.3527.1',
1584 '70.0.3527.0',
1585 '69.0.3497.49',
1586 '68.0.3440.121',
1587 '70.0.3526.1',
1588 '70.0.3526.0',
1589 '68.0.3440.120',
1590 '69.0.3497.48',
1591 '69.0.3497.47',
1592 '68.0.3440.119',
1593 '68.0.3440.118',
1594 '70.0.3525.5',
1595 '70.0.3525.4',
1596 '70.0.3525.3',
1597 '68.0.3440.117',
1598 '69.0.3497.46',
1599 '70.0.3525.2',
1600 '70.0.3525.1',
1601 '70.0.3525.0',
1602 '69.0.3497.45',
1603 '68.0.3440.116',
1604 '70.0.3524.4',
1605 '70.0.3524.3',
1606 '69.0.3497.44',
1607 '70.0.3524.2',
1608 '70.0.3524.1',
1609 '70.0.3524.0',
1610 '70.0.3523.2',
1611 '69.0.3497.43',
1612 '68.0.3440.115',
1613 '70.0.3505.9',
1614 '69.0.3497.42',
1615 '70.0.3505.8',
1616 '70.0.3523.1',
1617 '70.0.3523.0',
1618 '69.0.3497.41',
1619 '68.0.3440.114',
1620 '70.0.3505.7',
1621 '69.0.3497.40',
1622 '70.0.3522.1',
1623 '70.0.3522.0',
1624 '70.0.3521.2',
1625 '69.0.3497.39',
1626 '68.0.3440.113',
1627 '70.0.3505.6',
1628 '70.0.3521.1',
1629 '70.0.3521.0',
1630 '69.0.3497.38',
1631 '68.0.3440.112',
1632 '70.0.3520.1',
1633 '70.0.3520.0',
1634 '69.0.3497.37',
1635 '68.0.3440.111',
1636 '70.0.3519.3',
1637 '70.0.3519.2',
1638 '70.0.3519.1',
1639 '70.0.3519.0',
1640 '69.0.3497.36',
1641 '68.0.3440.110',
1642 '70.0.3518.1',
1643 '70.0.3518.0',
1644 '69.0.3497.35',
1645 '69.0.3497.34',
1646 '68.0.3440.109',
1647 '70.0.3517.1',
1648 '70.0.3517.0',
1649 '69.0.3497.33',
1650 '68.0.3440.108',
1651 '69.0.3497.32',
1652 '70.0.3516.3',
1653 '70.0.3516.2',
1654 '70.0.3516.1',
1655 '70.0.3516.0',
1656 '69.0.3497.31',
1657 '68.0.3440.107',
1658 '70.0.3515.4',
1659 '68.0.3440.106',
1660 '70.0.3515.3',
1661 '70.0.3515.2',
1662 '70.0.3515.1',
1663 '70.0.3515.0',
1664 '69.0.3497.30',
1665 '68.0.3440.105',
1666 '68.0.3440.104',
1667 '70.0.3514.2',
1668 '70.0.3514.1',
1669 '70.0.3514.0',
1670 '69.0.3497.29',
1671 '68.0.3440.103',
1672 '70.0.3513.1',
1673 '70.0.3513.0',
1674 '69.0.3497.28',
1675 )
1676 return _USER_AGENT_TPL % random.choice(_CHROME_VERSIONS)
1677
1678
3e669f36 1679std_headers = {
f7a147e3 1680 'User-Agent': random_user_agent(),
59ae15a5
PH
1681 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
1682 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
1683 'Accept-Encoding': 'gzip, deflate',
1684 'Accept-Language': 'en-us,en;q=0.5',
3e669f36 1685}
f427df17 1686
5f6a1245 1687
fb37eb25
S
1688USER_AGENTS = {
1689 'Safari': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27',
1690}
1691
1692
bf42a990
S
1693NO_DEFAULT = object()
1694
7105440c
YCH
1695ENGLISH_MONTH_NAMES = [
1696 'January', 'February', 'March', 'April', 'May', 'June',
1697 'July', 'August', 'September', 'October', 'November', 'December']
1698
f6717dec
S
1699MONTH_NAMES = {
1700 'en': ENGLISH_MONTH_NAMES,
1701 'fr': [
3e4185c3
S
1702 'janvier', 'février', 'mars', 'avril', 'mai', 'juin',
1703 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'],
f6717dec 1704}
a942d6cb 1705
a7aaa398
S
1706KNOWN_EXTENSIONS = (
1707 'mp4', 'm4a', 'm4p', 'm4b', 'm4r', 'm4v', 'aac',
1708 'flv', 'f4v', 'f4a', 'f4b',
1709 'webm', 'ogg', 'ogv', 'oga', 'ogx', 'spx', 'opus',
1710 'mkv', 'mka', 'mk3d',
1711 'avi', 'divx',
1712 'mov',
1713 'asf', 'wmv', 'wma',
1714 '3gp', '3g2',
1715 'mp3',
1716 'flac',
1717 'ape',
1718 'wav',
1719 'f4f', 'f4m', 'm3u8', 'smil')
1720
c587cbb7 1721# needed for sanitizing filenames in restricted mode
c8827027 1722ACCENT_CHARS = dict(zip('ÂÃÄÀÁÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖŐØŒÙÚÛÜŰÝÞßàáâãäåæçèéêëìíîïðñòóôõöőøœùúûüűýþÿ',
fd35d8cd
JW
1723 itertools.chain('AAAAAA', ['AE'], 'CEEEEIIIIDNOOOOOOO', ['OE'], 'UUUUUY', ['TH', 'ss'],
1724 'aaaaaa', ['ae'], 'ceeeeiiiionooooooo', ['oe'], 'uuuuuy', ['th'], 'y')))
c587cbb7 1725
46f59e89
S
1726DATE_FORMATS = (
1727 '%d %B %Y',
1728 '%d %b %Y',
1729 '%B %d %Y',
cb655f34
S
1730 '%B %dst %Y',
1731 '%B %dnd %Y',
9d30c213 1732 '%B %drd %Y',
cb655f34 1733 '%B %dth %Y',
46f59e89 1734 '%b %d %Y',
cb655f34
S
1735 '%b %dst %Y',
1736 '%b %dnd %Y',
9d30c213 1737 '%b %drd %Y',
cb655f34 1738 '%b %dth %Y',
46f59e89
S
1739 '%b %dst %Y %I:%M',
1740 '%b %dnd %Y %I:%M',
9d30c213 1741 '%b %drd %Y %I:%M',
46f59e89
S
1742 '%b %dth %Y %I:%M',
1743 '%Y %m %d',
1744 '%Y-%m-%d',
bccdbd22 1745 '%Y.%m.%d.',
46f59e89 1746 '%Y/%m/%d',
81c13222 1747 '%Y/%m/%d %H:%M',
46f59e89 1748 '%Y/%m/%d %H:%M:%S',
1931a55e
THD
1749 '%Y%m%d%H%M',
1750 '%Y%m%d%H%M%S',
0c1c6f4b 1751 '%Y-%m-%d %H:%M',
46f59e89
S
1752 '%Y-%m-%d %H:%M:%S',
1753 '%Y-%m-%d %H:%M:%S.%f',
5014558a 1754 '%Y-%m-%d %H:%M:%S:%f',
46f59e89
S
1755 '%d.%m.%Y %H:%M',
1756 '%d.%m.%Y %H.%M',
1757 '%Y-%m-%dT%H:%M:%SZ',
1758 '%Y-%m-%dT%H:%M:%S.%fZ',
1759 '%Y-%m-%dT%H:%M:%S.%f0Z',
1760 '%Y-%m-%dT%H:%M:%S',
1761 '%Y-%m-%dT%H:%M:%S.%f',
1762 '%Y-%m-%dT%H:%M',
c6eed6b8
S
1763 '%b %d %Y at %H:%M',
1764 '%b %d %Y at %H:%M:%S',
b555ae9b
S
1765 '%B %d %Y at %H:%M',
1766 '%B %d %Y at %H:%M:%S',
a63d9bd0 1767 '%H:%M %d-%b-%Y',
46f59e89
S
1768)
1769
1770DATE_FORMATS_DAY_FIRST = list(DATE_FORMATS)
1771DATE_FORMATS_DAY_FIRST.extend([
1772 '%d-%m-%Y',
1773 '%d.%m.%Y',
1774 '%d.%m.%y',
1775 '%d/%m/%Y',
1776 '%d/%m/%y',
1777 '%d/%m/%Y %H:%M:%S',
1778])
1779
1780DATE_FORMATS_MONTH_FIRST = list(DATE_FORMATS)
1781DATE_FORMATS_MONTH_FIRST.extend([
1782 '%m-%d-%Y',
1783 '%m.%d.%Y',
1784 '%m/%d/%Y',
1785 '%m/%d/%y',
1786 '%m/%d/%Y %H:%M:%S',
1787])
1788
06b3fe29 1789PACKED_CODES_RE = r"}\('(.+)',(\d+),(\d+),'([^']+)'\.split\('\|'\)"
22f5f5c6 1790JSON_LD_RE = r'(?is)<script[^>]+type=(["\']?)application/ld\+json\1[^>]*>(?P<json_ld>.+?)</script>'
06b3fe29 1791
7105440c 1792
d77c3dfd 1793def preferredencoding():
59ae15a5 1794 """Get preferred encoding.
d77c3dfd 1795
59ae15a5
PH
1796 Returns the best encoding scheme for the system, based on
1797 locale.getpreferredencoding() and some further tweaks.
1798 """
1799 try:
1800 pref = locale.getpreferredencoding()
28e614de 1801 'TEST'.encode(pref)
70a1165b 1802 except Exception:
59ae15a5 1803 pref = 'UTF-8'
bae611f2 1804
59ae15a5 1805 return pref
d77c3dfd 1806
f4bfd65f 1807
181c8655 1808def write_json_file(obj, fn):
1394646a 1809 """ Encode obj as JSON and write it to fn, atomically if possible """
181c8655 1810
92120217 1811 fn = encodeFilename(fn)
61ee5aeb 1812 if sys.version_info < (3, 0) and sys.platform != 'win32':
ec5f6016
JMF
1813 encoding = get_filesystem_encoding()
1814 # os.path.basename returns a bytes object, but NamedTemporaryFile
1815 # will fail if the filename contains non ascii characters unless we
1816 # use a unicode object
1817 path_basename = lambda f: os.path.basename(fn).decode(encoding)
1818 # the same for os.path.dirname
1819 path_dirname = lambda f: os.path.dirname(fn).decode(encoding)
1820 else:
1821 path_basename = os.path.basename
1822 path_dirname = os.path.dirname
1823
73159f99
S
1824 args = {
1825 'suffix': '.tmp',
ec5f6016
JMF
1826 'prefix': path_basename(fn) + '.',
1827 'dir': path_dirname(fn),
73159f99
S
1828 'delete': False,
1829 }
1830
181c8655
PH
1831 # In Python 2.x, json.dump expects a bytestream.
1832 # In Python 3.x, it writes to a character stream
1833 if sys.version_info < (3, 0):
73159f99 1834 args['mode'] = 'wb'
181c8655 1835 else:
73159f99
S
1836 args.update({
1837 'mode': 'w',
1838 'encoding': 'utf-8',
1839 })
1840
c86b6142 1841 tf = tempfile.NamedTemporaryFile(**compat_kwargs(args))
181c8655
PH
1842
1843 try:
1844 with tf:
6e84b215 1845 json.dump(obj, tf)
1394646a
IK
1846 if sys.platform == 'win32':
1847 # Need to remove existing file on Windows, else os.rename raises
1848 # WindowsError or FileExistsError.
1849 try:
1850 os.unlink(fn)
1851 except OSError:
1852 pass
9cd5f54e
R
1853 try:
1854 mask = os.umask(0)
1855 os.umask(mask)
1856 os.chmod(tf.name, 0o666 & ~mask)
1857 except OSError:
1858 pass
181c8655 1859 os.rename(tf.name, fn)
70a1165b 1860 except Exception:
181c8655
PH
1861 try:
1862 os.remove(tf.name)
1863 except OSError:
1864 pass
1865 raise
1866
1867
1868if sys.version_info >= (2, 7):
ee114368 1869 def find_xpath_attr(node, xpath, key, val=None):
59ae56fa 1870 """ Find the xpath xpath[@key=val] """
5d2354f1 1871 assert re.match(r'^[a-zA-Z_-]+$', key)
ee114368 1872 expr = xpath + ('[@%s]' % key if val is None else "[@%s='%s']" % (key, val))
59ae56fa
PH
1873 return node.find(expr)
1874else:
ee114368 1875 def find_xpath_attr(node, xpath, key, val=None):
810c10ba 1876 for f in node.findall(compat_xpath(xpath)):
ee114368
S
1877 if key not in f.attrib:
1878 continue
1879 if val is None or f.attrib.get(key) == val:
59ae56fa
PH
1880 return f
1881 return None
1882
d7e66d39
JMF
1883# On python2.6 the xml.etree.ElementTree.Element methods don't support
1884# the namespace parameter
5f6a1245
JW
1885
1886
d7e66d39
JMF
1887def xpath_with_ns(path, ns_map):
1888 components = [c.split(':') for c in path.split('/')]
1889 replaced = []
1890 for c in components:
1891 if len(c) == 1:
1892 replaced.append(c[0])
1893 else:
1894 ns, tag = c
1895 replaced.append('{%s}%s' % (ns_map[ns], tag))
1896 return '/'.join(replaced)
1897
d77c3dfd 1898
a41fb80c 1899def xpath_element(node, xpath, name=None, fatal=False, default=NO_DEFAULT):
578c0745 1900 def _find_xpath(xpath):
810c10ba 1901 return node.find(compat_xpath(xpath))
578c0745
S
1902
1903 if isinstance(xpath, (str, compat_str)):
1904 n = _find_xpath(xpath)
1905 else:
1906 for xp in xpath:
1907 n = _find_xpath(xp)
1908 if n is not None:
1909 break
d74bebd5 1910
8e636da4 1911 if n is None:
bf42a990
S
1912 if default is not NO_DEFAULT:
1913 return default
1914 elif fatal:
bf0ff932
PH
1915 name = xpath if name is None else name
1916 raise ExtractorError('Could not find XML element %s' % name)
1917 else:
1918 return None
a41fb80c
S
1919 return n
1920
1921
1922def xpath_text(node, xpath, name=None, fatal=False, default=NO_DEFAULT):
8e636da4
S
1923 n = xpath_element(node, xpath, name, fatal=fatal, default=default)
1924 if n is None or n == default:
1925 return n
1926 if n.text is None:
1927 if default is not NO_DEFAULT:
1928 return default
1929 elif fatal:
1930 name = xpath if name is None else name
1931 raise ExtractorError('Could not find XML element\'s text %s' % name)
1932 else:
1933 return None
1934 return n.text
a41fb80c
S
1935
1936
1937def xpath_attr(node, xpath, key, name=None, fatal=False, default=NO_DEFAULT):
1938 n = find_xpath_attr(node, xpath, key)
1939 if n is None:
1940 if default is not NO_DEFAULT:
1941 return default
1942 elif fatal:
1943 name = '%s[@%s]' % (xpath, key) if name is None else name
1944 raise ExtractorError('Could not find XML attribute %s' % name)
1945 else:
1946 return None
1947 return n.attrib[key]
bf0ff932
PH
1948
1949
9e6dd238 1950def get_element_by_id(id, html):
43e8fafd 1951 """Return the content of the tag with the specified ID in the passed HTML document"""
611c1dd9 1952 return get_element_by_attribute('id', id, html)
43e8fafd 1953
12ea2f30 1954
84c237fb 1955def get_element_by_class(class_name, html):
2af12ad9
TC
1956 """Return the content of the first tag with the specified class in the passed HTML document"""
1957 retval = get_elements_by_class(class_name, html)
1958 return retval[0] if retval else None
1959
1960
1961def get_element_by_attribute(attribute, value, html, escape_value=True):
1962 retval = get_elements_by_attribute(attribute, value, html, escape_value)
1963 return retval[0] if retval else None
1964
1965
1966def get_elements_by_class(class_name, html):
1967 """Return the content of all tags with the specified class in the passed HTML document as a list"""
1968 return get_elements_by_attribute(
84c237fb
YCH
1969 'class', r'[^\'"]*\b%s\b[^\'"]*' % re.escape(class_name),
1970 html, escape_value=False)
1971
1972
2af12ad9 1973def get_elements_by_attribute(attribute, value, html, escape_value=True):
43e8fafd 1974 """Return the content of the tag with the specified attribute in the passed HTML document"""
9e6dd238 1975
84c237fb
YCH
1976 value = re.escape(value) if escape_value else value
1977
2af12ad9
TC
1978 retlist = []
1979 for m in re.finditer(r'''(?xs)
38285056 1980 <([a-zA-Z0-9:._-]+)
609ff8ca 1981 (?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]*|="[^"]*"|='[^']*'|))*?
38285056 1982 \s+%s=['"]?%s['"]?
609ff8ca 1983 (?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]*|="[^"]*"|='[^']*'|))*?
38285056
PH
1984 \s*>
1985 (?P<content>.*?)
1986 </\1>
2af12ad9
TC
1987 ''' % (re.escape(attribute), value), html):
1988 res = m.group('content')
38285056 1989
2af12ad9
TC
1990 if res.startswith('"') or res.startswith("'"):
1991 res = res[1:-1]
38285056 1992
2af12ad9 1993 retlist.append(unescapeHTML(res))
a921f407 1994
2af12ad9 1995 return retlist
a921f407 1996
c5229f39 1997
8bb56eee
BF
1998class HTMLAttributeParser(compat_HTMLParser):
1999 """Trivial HTML parser to gather the attributes for a single element"""
b6e0c7d2 2000
8bb56eee 2001 def __init__(self):
c5229f39 2002 self.attrs = {}
8bb56eee
BF
2003 compat_HTMLParser.__init__(self)
2004
2005 def handle_starttag(self, tag, attrs):
2006 self.attrs = dict(attrs)
2007
c5229f39 2008
8bb56eee
BF
2009def extract_attributes(html_element):
2010 """Given a string for an HTML element such as
2011 <el
2012 a="foo" B="bar" c="&98;az" d=boz
2013 empty= noval entity="&amp;"
2014 sq='"' dq="'"
2015 >
2016 Decode and return a dictionary of attributes.
2017 {
2018 'a': 'foo', 'b': 'bar', c: 'baz', d: 'boz',
2019 'empty': '', 'noval': None, 'entity': '&',
2020 'sq': '"', 'dq': '\''
2021 }.
2022 NB HTMLParser is stricter in Python 2.6 & 3.2 than in later versions,
2023 but the cases in the unit test will work for all of 2.6, 2.7, 3.2-3.5.
2024 """
2025 parser = HTMLAttributeParser()
b4a3d461
S
2026 try:
2027 parser.feed(html_element)
2028 parser.close()
2029 # Older Python may throw HTMLParseError in case of malformed HTML
2030 except compat_HTMLParseError:
2031 pass
8bb56eee 2032 return parser.attrs
9e6dd238 2033
c5229f39 2034
9e6dd238 2035def clean_html(html):
59ae15a5 2036 """Clean an HTML snippet into a readable string"""
dd622d7c
PH
2037
2038 if html is None: # Convenience for sanitizing descriptions etc.
2039 return html
2040
59ae15a5
PH
2041 # Newline vs <br />
2042 html = html.replace('\n', ' ')
edd9221c
TF
2043 html = re.sub(r'(?u)\s*<\s*br\s*/?\s*>\s*', '\n', html)
2044 html = re.sub(r'(?u)<\s*/\s*p\s*>\s*<\s*p[^>]*>', '\n', html)
59ae15a5
PH
2045 # Strip html tags
2046 html = re.sub('<.*?>', '', html)
2047 # Replace html entities
2048 html = unescapeHTML(html)
7decf895 2049 return html.strip()
9e6dd238
FV
2050
2051
d77c3dfd 2052def sanitize_open(filename, open_mode):
59ae15a5
PH
2053 """Try to open the given filename, and slightly tweak it if this fails.
2054
2055 Attempts to open the given filename. If this fails, it tries to change
2056 the filename slightly, step by step, until it's either able to open it
2057 or it fails and raises a final exception, like the standard open()
2058 function.
2059
2060 It returns the tuple (stream, definitive_file_name).
2061 """
2062 try:
28e614de 2063 if filename == '-':
59ae15a5
PH
2064 if sys.platform == 'win32':
2065 import msvcrt
2066 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
898280a0 2067 return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename)
59ae15a5
PH
2068 stream = open(encodeFilename(filename), open_mode)
2069 return (stream, filename)
2070 except (IOError, OSError) as err:
f45c185f
PH
2071 if err.errno in (errno.EACCES,):
2072 raise
59ae15a5 2073
f45c185f 2074 # In case of error, try to remove win32 forbidden chars
d55de57b 2075 alt_filename = sanitize_path(filename)
f45c185f
PH
2076 if alt_filename == filename:
2077 raise
2078 else:
2079 # An exception here should be caught in the caller
d55de57b 2080 stream = open(encodeFilename(alt_filename), open_mode)
f45c185f 2081 return (stream, alt_filename)
d77c3dfd
FV
2082
2083
2084def timeconvert(timestr):
59ae15a5
PH
2085 """Convert RFC 2822 defined time string into system timestamp"""
2086 timestamp = None
2087 timetuple = email.utils.parsedate_tz(timestr)
2088 if timetuple is not None:
2089 timestamp = email.utils.mktime_tz(timetuple)
2090 return timestamp
1c469a94 2091
5f6a1245 2092
796173d0 2093def sanitize_filename(s, restricted=False, is_id=False):
59ae15a5
PH
2094 """Sanitizes a string so it could be used as part of a filename.
2095 If restricted is set, use a stricter subset of allowed characters.
158af524
S
2096 Set is_id if this is not an arbitrary string, but an ID that should be kept
2097 if possible.
59ae15a5
PH
2098 """
2099 def replace_insane(char):
c587cbb7
AT
2100 if restricted and char in ACCENT_CHARS:
2101 return ACCENT_CHARS[char]
91dd88b9 2102 elif not restricted and char == '\n':
2103 return ' '
2104 elif char == '?' or ord(char) < 32 or ord(char) == 127:
59ae15a5
PH
2105 return ''
2106 elif char == '"':
2107 return '' if restricted else '\''
2108 elif char == ':':
2109 return '_-' if restricted else ' -'
2110 elif char in '\\/|*<>':
2111 return '_'
627dcfff 2112 if restricted and (char in '!&\'()[]{}$;`^,#' or char.isspace()):
59ae15a5
PH
2113 return '_'
2114 if restricted and ord(char) > 127:
2115 return '_'
2116 return char
2117
639f1cea 2118 if s == '':
2119 return ''
2aeb06d6
PH
2120 # Handle timestamps
2121 s = re.sub(r'[0-9]+(?::[0-9]+)+', lambda m: m.group(0).replace(':', '_'), s)
28e614de 2122 result = ''.join(map(replace_insane, s))
796173d0
PH
2123 if not is_id:
2124 while '__' in result:
2125 result = result.replace('__', '_')
2126 result = result.strip('_')
2127 # Common case of "Foreign band name - English song title"
2128 if restricted and result.startswith('-_'):
2129 result = result[2:]
5a42414b
PH
2130 if result.startswith('-'):
2131 result = '_' + result[len('-'):]
a7440261 2132 result = result.lstrip('.')
796173d0
PH
2133 if not result:
2134 result = '_'
59ae15a5 2135 return result
d77c3dfd 2136
5f6a1245 2137
c2934512 2138def sanitize_path(s, force=False):
a2aaf4db 2139 """Sanitizes and normalizes path on Windows"""
c2934512 2140 if sys.platform == 'win32':
c4218ac3 2141 force = False
c2934512 2142 drive_or_unc, _ = os.path.splitdrive(s)
2143 if sys.version_info < (2, 7) and not drive_or_unc:
2144 drive_or_unc, _ = os.path.splitunc(s)
2145 elif force:
2146 drive_or_unc = ''
2147 else:
a2aaf4db 2148 return s
c2934512 2149
be531ef1
S
2150 norm_path = os.path.normpath(remove_start(s, drive_or_unc)).split(os.path.sep)
2151 if drive_or_unc:
a2aaf4db
S
2152 norm_path.pop(0)
2153 sanitized_path = [
ec85ded8 2154 path_part if path_part in ['.', '..'] else re.sub(r'(?:[/<>:"\|\\?\*]|[\s.]$)', '#', path_part)
a2aaf4db 2155 for path_part in norm_path]
be531ef1
S
2156 if drive_or_unc:
2157 sanitized_path.insert(0, drive_or_unc + os.path.sep)
c4218ac3 2158 elif force and s[0] == os.path.sep:
2159 sanitized_path.insert(0, os.path.sep)
a2aaf4db
S
2160 return os.path.join(*sanitized_path)
2161
2162
17bcc626 2163def sanitize_url(url):
befa4708
S
2164 # Prepend protocol-less URLs with `http:` scheme in order to mitigate
2165 # the number of unwanted failures due to missing protocol
2166 if url.startswith('//'):
2167 return 'http:%s' % url
2168 # Fix some common typos seen so far
2169 COMMON_TYPOS = (
067aa17e 2170 # https://github.com/ytdl-org/youtube-dl/issues/15649
befa4708
S
2171 (r'^httpss://', r'https://'),
2172 # https://bx1.be/lives/direct-tv/
2173 (r'^rmtp([es]?)://', r'rtmp\1://'),
2174 )
2175 for mistake, fixup in COMMON_TYPOS:
2176 if re.match(mistake, url):
2177 return re.sub(mistake, fixup, url)
bc6b9bcd 2178 return url
17bcc626
S
2179
2180
5435dcf9
HH
2181def extract_basic_auth(url):
2182 parts = compat_urlparse.urlsplit(url)
2183 if parts.username is None:
2184 return url, None
2185 url = compat_urlparse.urlunsplit(parts._replace(netloc=(
2186 parts.hostname if parts.port is None
2187 else '%s:%d' % (parts.hostname, parts.port))))
2188 auth_payload = base64.b64encode(
2189 ('%s:%s' % (parts.username, parts.password or '')).encode('utf-8'))
2190 return url, 'Basic ' + auth_payload.decode('utf-8')
2191
2192
67dda517 2193def sanitized_Request(url, *args, **kwargs):
bc6b9bcd 2194 url, auth_header = extract_basic_auth(escape_url(sanitize_url(url)))
5435dcf9
HH
2195 if auth_header is not None:
2196 headers = args[1] if len(args) >= 2 else kwargs.setdefault('headers', {})
2197 headers['Authorization'] = auth_header
2198 return compat_urllib_request.Request(url, *args, **kwargs)
67dda517
S
2199
2200
51098426
S
2201def expand_path(s):
2202 """Expand shell variables and ~"""
2203 return os.path.expandvars(compat_expanduser(s))
2204
2205
d77c3dfd 2206def orderedSet(iterable):
59ae15a5
PH
2207 """ Remove all duplicates from the input iterable """
2208 res = []
2209 for el in iterable:
2210 if el not in res:
2211 res.append(el)
2212 return res
d77c3dfd 2213
912b38b4 2214
55b2f099 2215def _htmlentity_transform(entity_with_semicolon):
4e408e47 2216 """Transforms an HTML entity to a character."""
55b2f099
YCH
2217 entity = entity_with_semicolon[:-1]
2218
4e408e47
PH
2219 # Known non-numeric HTML entity
2220 if entity in compat_html_entities.name2codepoint:
2221 return compat_chr(compat_html_entities.name2codepoint[entity])
2222
55b2f099
YCH
2223 # TODO: HTML5 allows entities without a semicolon. For example,
2224 # '&Eacuteric' should be decoded as 'Éric'.
2225 if entity_with_semicolon in compat_html_entities_html5:
2226 return compat_html_entities_html5[entity_with_semicolon]
2227
91757b0f 2228 mobj = re.match(r'#(x[0-9a-fA-F]+|[0-9]+)', entity)
4e408e47
PH
2229 if mobj is not None:
2230 numstr = mobj.group(1)
28e614de 2231 if numstr.startswith('x'):
4e408e47 2232 base = 16
28e614de 2233 numstr = '0%s' % numstr
4e408e47
PH
2234 else:
2235 base = 10
067aa17e 2236 # See https://github.com/ytdl-org/youtube-dl/issues/7518
7aefc49c
S
2237 try:
2238 return compat_chr(int(numstr, base))
2239 except ValueError:
2240 pass
4e408e47
PH
2241
2242 # Unknown entity in name, return its literal representation
7a3f0c00 2243 return '&%s;' % entity
4e408e47
PH
2244
2245
d77c3dfd 2246def unescapeHTML(s):
912b38b4
PH
2247 if s is None:
2248 return None
2249 assert type(s) == compat_str
d77c3dfd 2250
4e408e47 2251 return re.sub(
95f3f7c2 2252 r'&([^&;]+;)', lambda m: _htmlentity_transform(m.group(1)), s)
d77c3dfd 2253
8bf48f23 2254
cdb19aa4 2255def escapeHTML(text):
2256 return (
2257 text
2258 .replace('&', '&amp;')
2259 .replace('<', '&lt;')
2260 .replace('>', '&gt;')
2261 .replace('"', '&quot;')
2262 .replace("'", '&#39;')
2263 )
2264
2265
f5b1bca9 2266def process_communicate_or_kill(p, *args, **kwargs):
2267 try:
2268 return p.communicate(*args, **kwargs)
2269 except BaseException: # Including KeyboardInterrupt
2270 p.kill()
2271 p.wait()
2272 raise
2273
2274
aa49acd1
S
2275def get_subprocess_encoding():
2276 if sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
2277 # For subprocess calls, encode with locale encoding
2278 # Refer to http://stackoverflow.com/a/9951851/35070
2279 encoding = preferredencoding()
2280 else:
2281 encoding = sys.getfilesystemencoding()
2282 if encoding is None:
2283 encoding = 'utf-8'
2284 return encoding
2285
2286
8bf48f23 2287def encodeFilename(s, for_subprocess=False):
59ae15a5
PH
2288 """
2289 @param s The name of the file
2290 """
d77c3dfd 2291
8bf48f23 2292 assert type(s) == compat_str
d77c3dfd 2293
59ae15a5
PH
2294 # Python 3 has a Unicode API
2295 if sys.version_info >= (3, 0):
2296 return s
0f00efed 2297
aa49acd1
S
2298 # Pass '' directly to use Unicode APIs on Windows 2000 and up
2299 # (Detecting Windows NT 4 is tricky because 'major >= 4' would
2300 # match Windows 9x series as well. Besides, NT 4 is obsolete.)
2301 if not for_subprocess and sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
2302 return s
2303
8ee239e9
YCH
2304 # Jython assumes filenames are Unicode strings though reported as Python 2.x compatible
2305 if sys.platform.startswith('java'):
2306 return s
2307
aa49acd1
S
2308 return s.encode(get_subprocess_encoding(), 'ignore')
2309
2310
2311def decodeFilename(b, for_subprocess=False):
2312
2313 if sys.version_info >= (3, 0):
2314 return b
2315
2316 if not isinstance(b, bytes):
2317 return b
2318
2319 return b.decode(get_subprocess_encoding(), 'ignore')
8bf48f23 2320
f07b74fc
PH
2321
2322def encodeArgument(s):
2323 if not isinstance(s, compat_str):
2324 # Legacy code that uses byte strings
2325 # Uncomment the following line after fixing all post processors
7af808a5 2326 # assert False, 'Internal error: %r should be of type %r, is %r' % (s, compat_str, type(s))
f07b74fc
PH
2327 s = s.decode('ascii')
2328 return encodeFilename(s, True)
2329
2330
aa49acd1
S
2331def decodeArgument(b):
2332 return decodeFilename(b, True)
2333
2334
8271226a
PH
2335def decodeOption(optval):
2336 if optval is None:
2337 return optval
2338 if isinstance(optval, bytes):
2339 optval = optval.decode(preferredencoding())
2340
2341 assert isinstance(optval, compat_str)
2342 return optval
1c256f70 2343
5f6a1245 2344
cdb19aa4 2345def formatSeconds(secs, delim=':', msec=False):
4539dd30 2346 if secs > 3600:
cdb19aa4 2347 ret = '%d%s%02d%s%02d' % (secs // 3600, delim, (secs % 3600) // 60, delim, secs % 60)
4539dd30 2348 elif secs > 60:
cdb19aa4 2349 ret = '%d%s%02d' % (secs // 60, delim, secs % 60)
4539dd30 2350 else:
cdb19aa4 2351 ret = '%d' % secs
2352 return '%s.%03d' % (ret, secs % 1) if msec else ret
4539dd30 2353
a0ddb8a2 2354
77562778 2355def _ssl_load_windows_store_certs(ssl_context, storename):
2356 # Code adapted from _load_windows_store_certs in https://github.com/python/cpython/blob/main/Lib/ssl.py
2357 try:
2358 certs = [cert for cert, encoding, trust in ssl.enum_certificates(storename)
2359 if encoding == 'x509_asn' and (
2360 trust is True or ssl.Purpose.SERVER_AUTH.oid in trust)]
2361 except PermissionError:
2362 return
2363 for cert in certs:
a2366922 2364 try:
77562778 2365 ssl_context.load_verify_locations(cadata=cert)
2366 except ssl.SSLError:
a2366922
PH
2367 pass
2368
77562778 2369
2370def make_HTTPS_handler(params, **kwargs):
2371 opts_check_certificate = not params.get('nocheckcertificate')
2372 context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
2373 context.check_hostname = opts_check_certificate
2374 context.verify_mode = ssl.CERT_REQUIRED if opts_check_certificate else ssl.CERT_NONE
2375 if opts_check_certificate:
4e3d1898 2376 try:
2377 context.load_default_certs()
2378 # Work around the issue in load_default_certs when there are bad certificates. See:
2379 # https://github.com/yt-dlp/yt-dlp/issues/1060,
2380 # https://bugs.python.org/issue35665, https://bugs.python.org/issue45312
2381 except ssl.SSLError:
2382 # enum_certificates is not present in mingw python. See https://github.com/yt-dlp/yt-dlp/issues/1151
2383 if sys.platform == 'win32' and hasattr(ssl, 'enum_certificates'):
2384 # Create a new context to discard any certificates that were already loaded
2385 context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
2386 context.check_hostname, context.verify_mode = True, ssl.CERT_REQUIRED
2387 for storename in ('CA', 'ROOT'):
2388 _ssl_load_windows_store_certs(context, storename)
2389 context.set_default_verify_paths()
77562778 2390 return YoutubeDLHTTPSHandler(params, context=context, **kwargs)
ea6d901e 2391
732ea2f0 2392
5873d4cc 2393def bug_reports_message(before=';'):
08f2a92c 2394 if ytdl_is_updateable():
7a5c1cfe 2395 update_cmd = 'type yt-dlp -U to update'
08f2a92c 2396 else:
7a5c1cfe 2397 update_cmd = 'see https://github.com/yt-dlp/yt-dlp on how to update'
5873d4cc 2398 msg = 'please report this issue on https://github.com/yt-dlp/yt-dlp .'
08f2a92c 2399 msg += ' Make sure you are using the latest version; %s.' % update_cmd
7a5c1cfe 2400 msg += ' Be sure to call yt-dlp with the --verbose flag and include its complete output.'
5873d4cc
F
2401
2402 before = before.rstrip()
2403 if not before or before.endswith(('.', '!', '?')):
2404 msg = msg[0].title() + msg[1:]
2405
2406 return (before + ' ' if before else '') + msg
08f2a92c
JMF
2407
2408
bf5b9d85
PM
2409class YoutubeDLError(Exception):
2410 """Base exception for YoutubeDL errors."""
2411 pass
2412
2413
3158150c 2414network_exceptions = [compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error]
2415if hasattr(ssl, 'CertificateError'):
2416 network_exceptions.append(ssl.CertificateError)
2417network_exceptions = tuple(network_exceptions)
2418
2419
bf5b9d85 2420class ExtractorError(YoutubeDLError):
1c256f70 2421 """Error during info extraction."""
5f6a1245 2422
1151c407 2423 def __init__(self, msg, tb=None, expected=False, cause=None, video_id=None, ie=None):
9a82b238 2424 """ tb, if given, is the original traceback (so that it can be printed out).
7a5c1cfe 2425 If expected is set, this is a normal error message and most likely not a bug in yt-dlp.
9a82b238 2426 """
3158150c 2427 if sys.exc_info()[0] in network_exceptions:
9a82b238 2428 expected = True
d5979c5d 2429
526d74ec 2430 self.msg = str(msg)
1c256f70 2431 self.traceback = tb
1151c407 2432 self.expected = expected
2eabb802 2433 self.cause = cause
d11271dd 2434 self.video_id = video_id
1151c407 2435 self.ie = ie
2436 self.exc_info = sys.exc_info() # preserve original exception
2437
2438 super(ExtractorError, self).__init__(''.join((
2439 format_field(ie, template='[%s] '),
2440 format_field(video_id, template='%s: '),
526d74ec 2441 self.msg,
1151c407 2442 format_field(cause, template=' (caused by %r)'),
2443 '' if expected else bug_reports_message())))
1c256f70 2444
01951dda
PH
2445 def format_traceback(self):
2446 if self.traceback is None:
2447 return None
28e614de 2448 return ''.join(traceback.format_tb(self.traceback))
01951dda 2449
1c256f70 2450
416c7fcb
PH
2451class UnsupportedError(ExtractorError):
2452 def __init__(self, url):
2453 super(UnsupportedError, self).__init__(
2454 'Unsupported URL: %s' % url, expected=True)
2455 self.url = url
2456
2457
55b3e45b
JMF
2458class RegexNotFoundError(ExtractorError):
2459 """Error when a regex didn't match"""
2460 pass
2461
2462
773f291d
S
2463class GeoRestrictedError(ExtractorError):
2464 """Geographic restriction Error exception.
2465
2466 This exception may be thrown when a video is not available from your
2467 geographic location due to geographic restrictions imposed by a website.
2468 """
b6e0c7d2 2469
773f291d
S
2470 def __init__(self, msg, countries=None):
2471 super(GeoRestrictedError, self).__init__(msg, expected=True)
2472 self.msg = msg
2473 self.countries = countries
2474
2475
bf5b9d85 2476class DownloadError(YoutubeDLError):
59ae15a5 2477 """Download Error exception.
d77c3dfd 2478
59ae15a5
PH
2479 This exception may be thrown by FileDownloader objects if they are not
2480 configured to continue on errors. They will contain the appropriate
2481 error message.
2482 """
5f6a1245 2483
8cc83b8d
FV
2484 def __init__(self, msg, exc_info=None):
2485 """ exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """
2486 super(DownloadError, self).__init__(msg)
2487 self.exc_info = exc_info
d77c3dfd
FV
2488
2489
498f5606 2490class EntryNotInPlaylist(YoutubeDLError):
2491 """Entry not in playlist exception.
2492
2493 This exception will be thrown by YoutubeDL when a requested entry
2494 is not found in the playlist info_dict
2495 """
2496 pass
2497
2498
bf5b9d85 2499class SameFileError(YoutubeDLError):
59ae15a5 2500 """Same File exception.
d77c3dfd 2501
59ae15a5
PH
2502 This exception will be thrown by FileDownloader objects if they detect
2503 multiple files would have to be downloaded to the same file on disk.
2504 """
2505 pass
d77c3dfd
FV
2506
2507
bf5b9d85 2508class PostProcessingError(YoutubeDLError):
59ae15a5 2509 """Post Processing exception.
d77c3dfd 2510
59ae15a5
PH
2511 This exception may be raised by PostProcessor's .run() method to
2512 indicate an error in the postprocessing task.
2513 """
5f6a1245 2514
7851b379 2515 def __init__(self, msg):
bf5b9d85 2516 super(PostProcessingError, self).__init__(msg)
7851b379 2517 self.msg = msg
d77c3dfd 2518
5f6a1245 2519
8b0d7497 2520class ExistingVideoReached(YoutubeDLError):
2521 """ --max-downloads limit has been reached. """
2522 pass
2523
2524
2525class RejectedVideoReached(YoutubeDLError):
2526 """ --max-downloads limit has been reached. """
2527 pass
2528
2529
51d9739f 2530class ThrottledDownload(YoutubeDLError):
2531 """ Download speed below --throttled-rate. """
2532 pass
2533
2534
bf5b9d85 2535class MaxDownloadsReached(YoutubeDLError):
59ae15a5
PH
2536 """ --max-downloads limit has been reached. """
2537 pass
d77c3dfd
FV
2538
2539
bf5b9d85 2540class UnavailableVideoError(YoutubeDLError):
59ae15a5 2541 """Unavailable Format exception.
d77c3dfd 2542
59ae15a5
PH
2543 This exception will be thrown when a video is requested
2544 in a format that is not available for that video.
2545 """
2546 pass
d77c3dfd
FV
2547
2548
bf5b9d85 2549class ContentTooShortError(YoutubeDLError):
59ae15a5 2550 """Content Too Short exception.
d77c3dfd 2551
59ae15a5
PH
2552 This exception may be raised by FileDownloader objects when a file they
2553 download is too small for what the server announced first, indicating
2554 the connection was probably interrupted.
2555 """
d77c3dfd 2556
59ae15a5 2557 def __init__(self, downloaded, expected):
bf5b9d85
PM
2558 super(ContentTooShortError, self).__init__(
2559 'Downloaded {0} bytes, expected {1} bytes'.format(downloaded, expected)
2560 )
2c7ed247 2561 # Both in bytes
59ae15a5
PH
2562 self.downloaded = downloaded
2563 self.expected = expected
d77c3dfd 2564
5f6a1245 2565
bf5b9d85 2566class XAttrMetadataError(YoutubeDLError):
efa97bdc
YCH
2567 def __init__(self, code=None, msg='Unknown error'):
2568 super(XAttrMetadataError, self).__init__(msg)
2569 self.code = code
bd264412 2570 self.msg = msg
efa97bdc
YCH
2571
2572 # Parsing code and msg
3089bc74 2573 if (self.code in (errno.ENOSPC, errno.EDQUOT)
a0566bbf 2574 or 'No space left' in self.msg or 'Disk quota exceeded' in self.msg):
efa97bdc
YCH
2575 self.reason = 'NO_SPACE'
2576 elif self.code == errno.E2BIG or 'Argument list too long' in self.msg:
2577 self.reason = 'VALUE_TOO_LONG'
2578 else:
2579 self.reason = 'NOT_SUPPORTED'
2580
2581
bf5b9d85 2582class XAttrUnavailableError(YoutubeDLError):
efa97bdc
YCH
2583 pass
2584
2585
c5a59d93 2586def _create_http_connection(ydl_handler, http_class, is_https, *args, **kwargs):
e5e78797
S
2587 # Working around python 2 bug (see http://bugs.python.org/issue17849) by limiting
2588 # expected HTTP responses to meet HTTP/1.0 or later (see also
067aa17e 2589 # https://github.com/ytdl-org/youtube-dl/issues/6727)
e5e78797 2590 if sys.version_info < (3, 0):
65220c3b
S
2591 kwargs['strict'] = True
2592 hc = http_class(*args, **compat_kwargs(kwargs))
be4a824d 2593 source_address = ydl_handler._params.get('source_address')
8959018a 2594
be4a824d 2595 if source_address is not None:
8959018a
AU
2596 # This is to workaround _create_connection() from socket where it will try all
2597 # address data from getaddrinfo() including IPv6. This filters the result from
2598 # getaddrinfo() based on the source_address value.
2599 # This is based on the cpython socket.create_connection() function.
2600 # https://github.com/python/cpython/blob/master/Lib/socket.py#L691
2601 def _create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, source_address=None):
2602 host, port = address
2603 err = None
2604 addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
9e21e6d9
S
2605 af = socket.AF_INET if '.' in source_address[0] else socket.AF_INET6
2606 ip_addrs = [addr for addr in addrs if addr[0] == af]
2607 if addrs and not ip_addrs:
2608 ip_version = 'v4' if af == socket.AF_INET else 'v6'
2609 raise socket.error(
2610 "No remote IP%s addresses available for connect, can't use '%s' as source address"
2611 % (ip_version, source_address[0]))
8959018a
AU
2612 for res in ip_addrs:
2613 af, socktype, proto, canonname, sa = res
2614 sock = None
2615 try:
2616 sock = socket.socket(af, socktype, proto)
2617 if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
2618 sock.settimeout(timeout)
2619 sock.bind(source_address)
2620 sock.connect(sa)
2621 err = None # Explicitly break reference cycle
2622 return sock
2623 except socket.error as _:
2624 err = _
2625 if sock is not None:
2626 sock.close()
2627 if err is not None:
2628 raise err
2629 else:
9e21e6d9
S
2630 raise socket.error('getaddrinfo returns an empty list')
2631 if hasattr(hc, '_create_connection'):
2632 hc._create_connection = _create_connection
be4a824d
PH
2633 sa = (source_address, 0)
2634 if hasattr(hc, 'source_address'): # Python 2.7+
2635 hc.source_address = sa
2636 else: # Python 2.6
2637 def _hc_connect(self, *args, **kwargs):
9e21e6d9 2638 sock = _create_connection(
be4a824d
PH
2639 (self.host, self.port), self.timeout, sa)
2640 if is_https:
d7932313
PH
2641 self.sock = ssl.wrap_socket(
2642 sock, self.key_file, self.cert_file,
2643 ssl_version=ssl.PROTOCOL_TLSv1)
be4a824d
PH
2644 else:
2645 self.sock = sock
2646 hc.connect = functools.partial(_hc_connect, hc)
2647
2648 return hc
2649
2650
87f0e62d 2651def handle_youtubedl_headers(headers):
992fc9d6
YCH
2652 filtered_headers = headers
2653
2654 if 'Youtubedl-no-compression' in filtered_headers:
2655 filtered_headers = dict((k, v) for k, v in filtered_headers.items() if k.lower() != 'accept-encoding')
87f0e62d 2656 del filtered_headers['Youtubedl-no-compression']
87f0e62d 2657
992fc9d6 2658 return filtered_headers
87f0e62d
YCH
2659
2660
acebc9cd 2661class YoutubeDLHandler(compat_urllib_request.HTTPHandler):
59ae15a5
PH
2662 """Handler for HTTP requests and responses.
2663
2664 This class, when installed with an OpenerDirector, automatically adds
2665 the standard headers to every HTTP request and handles gzipped and
2666 deflated responses from web servers. If compression is to be avoided in
2667 a particular request, the original request in the program code only has
0424ec30 2668 to include the HTTP header "Youtubedl-no-compression", which will be
59ae15a5
PH
2669 removed before making the real request.
2670
2671 Part of this code was copied from:
2672
2673 http://techknack.net/python-urllib2-handlers/
2674
2675 Andrew Rowls, the author of that code, agreed to release it to the
2676 public domain.
2677 """
2678
be4a824d
PH
2679 def __init__(self, params, *args, **kwargs):
2680 compat_urllib_request.HTTPHandler.__init__(self, *args, **kwargs)
2681 self._params = params
2682
2683 def http_open(self, req):
71aff188
YCH
2684 conn_class = compat_http_client.HTTPConnection
2685
2686 socks_proxy = req.headers.get('Ytdl-socks-proxy')
2687 if socks_proxy:
2688 conn_class = make_socks_conn_class(conn_class, socks_proxy)
2689 del req.headers['Ytdl-socks-proxy']
2690
be4a824d 2691 return self.do_open(functools.partial(
71aff188 2692 _create_http_connection, self, conn_class, False),
be4a824d
PH
2693 req)
2694
59ae15a5
PH
2695 @staticmethod
2696 def deflate(data):
fc2119f2 2697 if not data:
2698 return data
59ae15a5
PH
2699 try:
2700 return zlib.decompress(data, -zlib.MAX_WBITS)
2701 except zlib.error:
2702 return zlib.decompress(data)
2703
acebc9cd 2704 def http_request(self, req):
51f267d9
S
2705 # According to RFC 3986, URLs can not contain non-ASCII characters, however this is not
2706 # always respected by websites, some tend to give out URLs with non percent-encoded
2707 # non-ASCII characters (see telemb.py, ard.py [#3412])
2708 # urllib chokes on URLs with non-ASCII characters (see http://bugs.python.org/issue3991)
2709 # To work around aforementioned issue we will replace request's original URL with
2710 # percent-encoded one
2711 # Since redirects are also affected (e.g. http://www.southpark.de/alle-episoden/s18e09)
2712 # the code of this workaround has been moved here from YoutubeDL.urlopen()
2713 url = req.get_full_url()
2714 url_escaped = escape_url(url)
2715
2716 # Substitute URL if any change after escaping
2717 if url != url_escaped:
15d260eb 2718 req = update_Request(req, url=url_escaped)
51f267d9 2719
33ac271b 2720 for h, v in std_headers.items():
3d5f7a39
JK
2721 # Capitalize is needed because of Python bug 2275: http://bugs.python.org/issue2275
2722 # The dict keys are capitalized because of this bug by urllib
2723 if h.capitalize() not in req.headers:
33ac271b 2724 req.add_header(h, v)
87f0e62d
YCH
2725
2726 req.headers = handle_youtubedl_headers(req.headers)
989b4b2b
PH
2727
2728 if sys.version_info < (2, 7) and '#' in req.get_full_url():
2729 # Python 2.6 is brain-dead when it comes to fragments
2730 req._Request__original = req._Request__original.partition('#')[0]
2731 req._Request__r_type = req._Request__r_type.partition('#')[0]
2732
59ae15a5
PH
2733 return req
2734
acebc9cd 2735 def http_response(self, req, resp):
59ae15a5
PH
2736 old_resp = resp
2737 # gzip
2738 if resp.headers.get('Content-encoding', '') == 'gzip':
aa3e9507
PH
2739 content = resp.read()
2740 gz = gzip.GzipFile(fileobj=io.BytesIO(content), mode='rb')
2741 try:
2742 uncompressed = io.BytesIO(gz.read())
2743 except IOError as original_ioerror:
2744 # There may be junk add the end of the file
2745 # See http://stackoverflow.com/q/4928560/35070 for details
2746 for i in range(1, 1024):
2747 try:
2748 gz = gzip.GzipFile(fileobj=io.BytesIO(content[:-i]), mode='rb')
2749 uncompressed = io.BytesIO(gz.read())
2750 except IOError:
2751 continue
2752 break
2753 else:
2754 raise original_ioerror
b407d853 2755 resp = compat_urllib_request.addinfourl(uncompressed, old_resp.headers, old_resp.url, old_resp.code)
59ae15a5 2756 resp.msg = old_resp.msg
c047270c 2757 del resp.headers['Content-encoding']
59ae15a5
PH
2758 # deflate
2759 if resp.headers.get('Content-encoding', '') == 'deflate':
2760 gz = io.BytesIO(self.deflate(resp.read()))
b407d853 2761 resp = compat_urllib_request.addinfourl(gz, old_resp.headers, old_resp.url, old_resp.code)
59ae15a5 2762 resp.msg = old_resp.msg
c047270c 2763 del resp.headers['Content-encoding']
ad729172 2764 # Percent-encode redirect URL of Location HTTP header to satisfy RFC 3986 (see
067aa17e 2765 # https://github.com/ytdl-org/youtube-dl/issues/6457).
5a4d9ddb
S
2766 if 300 <= resp.code < 400:
2767 location = resp.headers.get('Location')
2768 if location:
2769 # As of RFC 2616 default charset is iso-8859-1 that is respected by python 3
2770 if sys.version_info >= (3, 0):
2771 location = location.encode('iso-8859-1').decode('utf-8')
0ea59007
YCH
2772 else:
2773 location = location.decode('utf-8')
5a4d9ddb
S
2774 location_escaped = escape_url(location)
2775 if location != location_escaped:
2776 del resp.headers['Location']
9a4aec8b
YCH
2777 if sys.version_info < (3, 0):
2778 location_escaped = location_escaped.encode('utf-8')
5a4d9ddb 2779 resp.headers['Location'] = location_escaped
59ae15a5 2780 return resp
0f8d03f8 2781
acebc9cd
PH
2782 https_request = http_request
2783 https_response = http_response
bf50b038 2784
5de90176 2785
71aff188
YCH
2786def make_socks_conn_class(base_class, socks_proxy):
2787 assert issubclass(base_class, (
2788 compat_http_client.HTTPConnection, compat_http_client.HTTPSConnection))
2789
2790 url_components = compat_urlparse.urlparse(socks_proxy)
2791 if url_components.scheme.lower() == 'socks5':
2792 socks_type = ProxyType.SOCKS5
2793 elif url_components.scheme.lower() in ('socks', 'socks4'):
2794 socks_type = ProxyType.SOCKS4
51fb4995
YCH
2795 elif url_components.scheme.lower() == 'socks4a':
2796 socks_type = ProxyType.SOCKS4A
71aff188 2797
cdd94c2e
YCH
2798 def unquote_if_non_empty(s):
2799 if not s:
2800 return s
2801 return compat_urllib_parse_unquote_plus(s)
2802
71aff188
YCH
2803 proxy_args = (
2804 socks_type,
2805 url_components.hostname, url_components.port or 1080,
2806 True, # Remote DNS
cdd94c2e
YCH
2807 unquote_if_non_empty(url_components.username),
2808 unquote_if_non_empty(url_components.password),
71aff188
YCH
2809 )
2810
2811 class SocksConnection(base_class):
2812 def connect(self):
2813 self.sock = sockssocket()
2814 self.sock.setproxy(*proxy_args)
2815 if type(self.timeout) in (int, float):
2816 self.sock.settimeout(self.timeout)
2817 self.sock.connect((self.host, self.port))
2818
2819 if isinstance(self, compat_http_client.HTTPSConnection):
2820 if hasattr(self, '_context'): # Python > 2.6
2821 self.sock = self._context.wrap_socket(
2822 self.sock, server_hostname=self.host)
2823 else:
2824 self.sock = ssl.wrap_socket(self.sock)
2825
2826 return SocksConnection
2827
2828
be4a824d
PH
2829class YoutubeDLHTTPSHandler(compat_urllib_request.HTTPSHandler):
2830 def __init__(self, params, https_conn_class=None, *args, **kwargs):
2831 compat_urllib_request.HTTPSHandler.__init__(self, *args, **kwargs)
2832 self._https_conn_class = https_conn_class or compat_http_client.HTTPSConnection
2833 self._params = params
2834
2835 def https_open(self, req):
4f264c02 2836 kwargs = {}
71aff188
YCH
2837 conn_class = self._https_conn_class
2838
4f264c02
JMF
2839 if hasattr(self, '_context'): # python > 2.6
2840 kwargs['context'] = self._context
2841 if hasattr(self, '_check_hostname'): # python 3.x
2842 kwargs['check_hostname'] = self._check_hostname
71aff188
YCH
2843
2844 socks_proxy = req.headers.get('Ytdl-socks-proxy')
2845 if socks_proxy:
2846 conn_class = make_socks_conn_class(conn_class, socks_proxy)
2847 del req.headers['Ytdl-socks-proxy']
2848
be4a824d 2849 return self.do_open(functools.partial(
71aff188 2850 _create_http_connection, self, conn_class, True),
4f264c02 2851 req, **kwargs)
be4a824d
PH
2852
2853
1bab3437 2854class YoutubeDLCookieJar(compat_cookiejar.MozillaCookieJar):
f1a8511f
S
2855 """
2856 See [1] for cookie file format.
2857
2858 1. https://curl.haxx.se/docs/http-cookies.html
2859 """
e7e62441 2860 _HTTPONLY_PREFIX = '#HttpOnly_'
c380cc28
S
2861 _ENTRY_LEN = 7
2862 _HEADER = '''# Netscape HTTP Cookie File
7a5c1cfe 2863# This file is generated by yt-dlp. Do not edit.
c380cc28
S
2864
2865'''
2866 _CookieFileEntry = collections.namedtuple(
2867 'CookieFileEntry',
2868 ('domain_name', 'include_subdomains', 'path', 'https_only', 'expires_at', 'name', 'value'))
e7e62441 2869
1bab3437 2870 def save(self, filename=None, ignore_discard=False, ignore_expires=False):
c380cc28
S
2871 """
2872 Save cookies to a file.
2873
2874 Most of the code is taken from CPython 3.8 and slightly adapted
2875 to support cookie files with UTF-8 in both python 2 and 3.
2876 """
2877 if filename is None:
2878 if self.filename is not None:
2879 filename = self.filename
2880 else:
2881 raise ValueError(compat_cookiejar.MISSING_FILENAME_TEXT)
2882
1bab3437
S
2883 # Store session cookies with `expires` set to 0 instead of an empty
2884 # string
2885 for cookie in self:
2886 if cookie.expires is None:
2887 cookie.expires = 0
c380cc28
S
2888
2889 with io.open(filename, 'w', encoding='utf-8') as f:
2890 f.write(self._HEADER)
2891 now = time.time()
2892 for cookie in self:
2893 if not ignore_discard and cookie.discard:
2894 continue
2895 if not ignore_expires and cookie.is_expired(now):
2896 continue
2897 if cookie.secure:
2898 secure = 'TRUE'
2899 else:
2900 secure = 'FALSE'
2901 if cookie.domain.startswith('.'):
2902 initial_dot = 'TRUE'
2903 else:
2904 initial_dot = 'FALSE'
2905 if cookie.expires is not None:
2906 expires = compat_str(cookie.expires)
2907 else:
2908 expires = ''
2909 if cookie.value is None:
2910 # cookies.txt regards 'Set-Cookie: foo' as a cookie
2911 # with no name, whereas http.cookiejar regards it as a
2912 # cookie with no value.
2913 name = ''
2914 value = cookie.name
2915 else:
2916 name = cookie.name
2917 value = cookie.value
2918 f.write(
2919 '\t'.join([cookie.domain, initial_dot, cookie.path,
2920 secure, expires, name, value]) + '\n')
1bab3437
S
2921
2922 def load(self, filename=None, ignore_discard=False, ignore_expires=False):
e7e62441 2923 """Load cookies from a file."""
2924 if filename is None:
2925 if self.filename is not None:
2926 filename = self.filename
2927 else:
2928 raise ValueError(compat_cookiejar.MISSING_FILENAME_TEXT)
2929
c380cc28
S
2930 def prepare_line(line):
2931 if line.startswith(self._HTTPONLY_PREFIX):
2932 line = line[len(self._HTTPONLY_PREFIX):]
2933 # comments and empty lines are fine
2934 if line.startswith('#') or not line.strip():
2935 return line
2936 cookie_list = line.split('\t')
2937 if len(cookie_list) != self._ENTRY_LEN:
2938 raise compat_cookiejar.LoadError('invalid length %d' % len(cookie_list))
2939 cookie = self._CookieFileEntry(*cookie_list)
2940 if cookie.expires_at and not cookie.expires_at.isdigit():
2941 raise compat_cookiejar.LoadError('invalid expires at %s' % cookie.expires_at)
2942 return line
2943
e7e62441 2944 cf = io.StringIO()
c380cc28 2945 with io.open(filename, encoding='utf-8') as f:
e7e62441 2946 for line in f:
c380cc28
S
2947 try:
2948 cf.write(prepare_line(line))
2949 except compat_cookiejar.LoadError as e:
2950 write_string(
2951 'WARNING: skipping cookie file entry due to %s: %r\n'
2952 % (e, line), sys.stderr)
2953 continue
e7e62441 2954 cf.seek(0)
2955 self._really_load(cf, filename, ignore_discard, ignore_expires)
1bab3437
S
2956 # Session cookies are denoted by either `expires` field set to
2957 # an empty string or 0. MozillaCookieJar only recognizes the former
2958 # (see [1]). So we need force the latter to be recognized as session
2959 # cookies on our own.
2960 # Session cookies may be important for cookies-based authentication,
2961 # e.g. usually, when user does not check 'Remember me' check box while
2962 # logging in on a site, some important cookies are stored as session
2963 # cookies so that not recognizing them will result in failed login.
2964 # 1. https://bugs.python.org/issue17164
2965 for cookie in self:
2966 # Treat `expires=0` cookies as session cookies
2967 if cookie.expires == 0:
2968 cookie.expires = None
2969 cookie.discard = True
2970
2971
a6420bf5
S
2972class YoutubeDLCookieProcessor(compat_urllib_request.HTTPCookieProcessor):
2973 def __init__(self, cookiejar=None):
2974 compat_urllib_request.HTTPCookieProcessor.__init__(self, cookiejar)
2975
2976 def http_response(self, request, response):
2977 # Python 2 will choke on next HTTP request in row if there are non-ASCII
2978 # characters in Set-Cookie HTTP header of last response (see
067aa17e 2979 # https://github.com/ytdl-org/youtube-dl/issues/6769).
a6420bf5
S
2980 # In order to at least prevent crashing we will percent encode Set-Cookie
2981 # header before HTTPCookieProcessor starts processing it.
e28034c5
S
2982 # if sys.version_info < (3, 0) and response.headers:
2983 # for set_cookie_header in ('Set-Cookie', 'Set-Cookie2'):
2984 # set_cookie = response.headers.get(set_cookie_header)
2985 # if set_cookie:
2986 # set_cookie_escaped = compat_urllib_parse.quote(set_cookie, b"%/;:@&=+$,!~*'()?#[] ")
2987 # if set_cookie != set_cookie_escaped:
2988 # del response.headers[set_cookie_header]
2989 # response.headers[set_cookie_header] = set_cookie_escaped
a6420bf5
S
2990 return compat_urllib_request.HTTPCookieProcessor.http_response(self, request, response)
2991
f5fa042c 2992 https_request = compat_urllib_request.HTTPCookieProcessor.http_request
a6420bf5
S
2993 https_response = http_response
2994
2995
fca6dba8 2996class YoutubeDLRedirectHandler(compat_urllib_request.HTTPRedirectHandler):
201c1459 2997 """YoutubeDL redirect handler
2998
2999 The code is based on HTTPRedirectHandler implementation from CPython [1].
3000
3001 This redirect handler solves two issues:
3002 - ensures redirect URL is always unicode under python 2
3003 - introduces support for experimental HTTP response status code
3004 308 Permanent Redirect [2] used by some sites [3]
3005
3006 1. https://github.com/python/cpython/blob/master/Lib/urllib/request.py
3007 2. https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308
3008 3. https://github.com/ytdl-org/youtube-dl/issues/28768
3009 """
3010
3011 http_error_301 = http_error_303 = http_error_307 = http_error_308 = compat_urllib_request.HTTPRedirectHandler.http_error_302
3012
3013 def redirect_request(self, req, fp, code, msg, headers, newurl):
3014 """Return a Request or None in response to a redirect.
3015
3016 This is called by the http_error_30x methods when a
3017 redirection response is received. If a redirection should
3018 take place, return a new Request to allow http_error_30x to
3019 perform the redirect. Otherwise, raise HTTPError if no-one
3020 else should try to handle this url. Return None if you can't
3021 but another Handler might.
3022 """
3023 m = req.get_method()
3024 if (not (code in (301, 302, 303, 307, 308) and m in ("GET", "HEAD")
3025 or code in (301, 302, 303) and m == "POST")):
3026 raise compat_HTTPError(req.full_url, code, msg, headers, fp)
3027 # Strictly (according to RFC 2616), 301 or 302 in response to
3028 # a POST MUST NOT cause a redirection without confirmation
3029 # from the user (of urllib.request, in this case). In practice,
3030 # essentially all clients do redirect in this case, so we do
3031 # the same.
3032
3033 # On python 2 urlh.geturl() may sometimes return redirect URL
3034 # as byte string instead of unicode. This workaround allows
3035 # to force it always return unicode.
3036 if sys.version_info[0] < 3:
3037 newurl = compat_str(newurl)
3038
3039 # Be conciliant with URIs containing a space. This is mainly
3040 # redundant with the more complete encoding done in http_error_302(),
3041 # but it is kept for compatibility with other callers.
3042 newurl = newurl.replace(' ', '%20')
3043
3044 CONTENT_HEADERS = ("content-length", "content-type")
3045 # NB: don't use dict comprehension for python 2.6 compatibility
3046 newheaders = dict((k, v) for k, v in req.headers.items()
3047 if k.lower() not in CONTENT_HEADERS)
3048 return compat_urllib_request.Request(
3049 newurl, headers=newheaders, origin_req_host=req.origin_req_host,
3050 unverifiable=True)
fca6dba8
S
3051
3052
46f59e89
S
3053def extract_timezone(date_str):
3054 m = re.search(
f137e4c2 3055 r'''(?x)
3056 ^.{8,}? # >=8 char non-TZ prefix, if present
3057 (?P<tz>Z| # just the UTC Z, or
3058 (?:(?<=.\b\d{4}|\b\d{2}:\d\d)| # preceded by 4 digits or hh:mm or
3059 (?<!.\b[a-zA-Z]{3}|[a-zA-Z]{4}|..\b\d\d)) # not preceded by 3 alpha word or >= 4 alpha or 2 digits
3060 [ ]? # optional space
3061 (?P<sign>\+|-) # +/-
3062 (?P<hours>[0-9]{2}):?(?P<minutes>[0-9]{2}) # hh[:]mm
3063 $)
3064 ''', date_str)
46f59e89
S
3065 if not m:
3066 timezone = datetime.timedelta()
3067 else:
3068 date_str = date_str[:-len(m.group('tz'))]
3069 if not m.group('sign'):
3070 timezone = datetime.timedelta()
3071 else:
3072 sign = 1 if m.group('sign') == '+' else -1
3073 timezone = datetime.timedelta(
3074 hours=sign * int(m.group('hours')),
3075 minutes=sign * int(m.group('minutes')))
3076 return timezone, date_str
3077
3078
08b38d54 3079def parse_iso8601(date_str, delimiter='T', timezone=None):
912b38b4
PH
3080 """ Return a UNIX timestamp from the given date """
3081
3082 if date_str is None:
3083 return None
3084
52c3a6e4
S
3085 date_str = re.sub(r'\.[0-9]+', '', date_str)
3086
08b38d54 3087 if timezone is None:
46f59e89
S
3088 timezone, date_str = extract_timezone(date_str)
3089
52c3a6e4
S
3090 try:
3091 date_format = '%Y-%m-%d{0}%H:%M:%S'.format(delimiter)
3092 dt = datetime.datetime.strptime(date_str, date_format) - timezone
3093 return calendar.timegm(dt.timetuple())
3094 except ValueError:
3095 pass
912b38b4
PH
3096
3097
46f59e89
S
3098def date_formats(day_first=True):
3099 return DATE_FORMATS_DAY_FIRST if day_first else DATE_FORMATS_MONTH_FIRST
3100
3101
42bdd9d0 3102def unified_strdate(date_str, day_first=True):
bf50b038 3103 """Return a string with the date in the format YYYYMMDD"""
64e7ad60
PH
3104
3105 if date_str is None:
3106 return None
bf50b038 3107 upload_date = None
5f6a1245 3108 # Replace commas
026fcc04 3109 date_str = date_str.replace(',', ' ')
42bdd9d0 3110 # Remove AM/PM + timezone
9bb8e0a3 3111 date_str = re.sub(r'(?i)\s*(?:AM|PM)(?:\s+[A-Z]+)?', '', date_str)
46f59e89 3112 _, date_str = extract_timezone(date_str)
42bdd9d0 3113
46f59e89 3114 for expression in date_formats(day_first):
bf50b038
JMF
3115 try:
3116 upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
5de90176 3117 except ValueError:
bf50b038 3118 pass
42393ce2
PH
3119 if upload_date is None:
3120 timetuple = email.utils.parsedate_tz(date_str)
3121 if timetuple:
c6b9cf05
S
3122 try:
3123 upload_date = datetime.datetime(*timetuple[:6]).strftime('%Y%m%d')
3124 except ValueError:
3125 pass
6a750402
JMF
3126 if upload_date is not None:
3127 return compat_str(upload_date)
bf50b038 3128
5f6a1245 3129
46f59e89
S
3130def unified_timestamp(date_str, day_first=True):
3131 if date_str is None:
3132 return None
3133
2ae2ffda 3134 date_str = re.sub(r'[,|]', '', date_str)
46f59e89 3135
7dc2a74e 3136 pm_delta = 12 if re.search(r'(?i)PM', date_str) else 0
46f59e89
S
3137 timezone, date_str = extract_timezone(date_str)
3138
3139 # Remove AM/PM + timezone
3140 date_str = re.sub(r'(?i)\s*(?:AM|PM)(?:\s+[A-Z]+)?', '', date_str)
3141
deef3195
S
3142 # Remove unrecognized timezones from ISO 8601 alike timestamps
3143 m = re.search(r'\d{1,2}:\d{1,2}(?:\.\d+)?(?P<tz>\s*[A-Z]+)$', date_str)
3144 if m:
3145 date_str = date_str[:-len(m.group('tz'))]
3146
f226880c
PH
3147 # Python only supports microseconds, so remove nanoseconds
3148 m = re.search(r'^([0-9]{4,}-[0-9]{1,2}-[0-9]{1,2}T[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}\.[0-9]{6})[0-9]+$', date_str)
3149 if m:
3150 date_str = m.group(1)
3151
46f59e89
S
3152 for expression in date_formats(day_first):
3153 try:
7dc2a74e 3154 dt = datetime.datetime.strptime(date_str, expression) - timezone + datetime.timedelta(hours=pm_delta)
46f59e89
S
3155 return calendar.timegm(dt.timetuple())
3156 except ValueError:
3157 pass
3158 timetuple = email.utils.parsedate_tz(date_str)
3159 if timetuple:
7dc2a74e 3160 return calendar.timegm(timetuple) + pm_delta * 3600
46f59e89
S
3161
3162
28e614de 3163def determine_ext(url, default_ext='unknown_video'):
85750f89 3164 if url is None or '.' not in url:
f4776371 3165 return default_ext
9cb9a5df 3166 guess = url.partition('?')[0].rpartition('.')[2]
73e79f2a
PH
3167 if re.match(r'^[A-Za-z0-9]+$', guess):
3168 return guess
a7aaa398
S
3169 # Try extract ext from URLs like http://example.com/foo/bar.mp4/?download
3170 elif guess.rstrip('/') in KNOWN_EXTENSIONS:
9cb9a5df 3171 return guess.rstrip('/')
73e79f2a 3172 else:
cbdbb766 3173 return default_ext
73e79f2a 3174
5f6a1245 3175
824fa511
S
3176def subtitles_filename(filename, sub_lang, sub_format, expected_real_ext=None):
3177 return replace_extension(filename, sub_lang + '.' + sub_format, expected_real_ext)
d4051a8e 3178
5f6a1245 3179
9e62f283 3180def datetime_from_str(date_str, precision='auto', format='%Y%m%d'):
37254abc
JMF
3181 """
3182 Return a datetime object from a string in the format YYYYMMDD or
9e62f283 3183 (now|today|date)[+-][0-9](microsecond|second|minute|hour|day|week|month|year)(s)?
3184
3185 format: string date format used to return datetime object from
3186 precision: round the time portion of a datetime object.
3187 auto|microsecond|second|minute|hour|day.
3188 auto: round to the unit provided in date_str (if applicable).
3189 """
3190 auto_precision = False
3191 if precision == 'auto':
3192 auto_precision = True
3193 precision = 'microsecond'
3194 today = datetime_round(datetime.datetime.now(), precision)
f8795e10 3195 if date_str in ('now', 'today'):
37254abc 3196 return today
f8795e10
PH
3197 if date_str == 'yesterday':
3198 return today - datetime.timedelta(days=1)
9e62f283 3199 match = re.match(
3200 r'(?P<start>.+)(?P<sign>[+-])(?P<time>\d+)(?P<unit>microsecond|second|minute|hour|day|week|month|year)(s)?',
3201 date_str)
37254abc 3202 if match is not None:
9e62f283 3203 start_time = datetime_from_str(match.group('start'), precision, format)
3204 time = int(match.group('time')) * (-1 if match.group('sign') == '-' else 1)
37254abc 3205 unit = match.group('unit')
9e62f283 3206 if unit == 'month' or unit == 'year':
3207 new_date = datetime_add_months(start_time, time * 12 if unit == 'year' else time)
37254abc 3208 unit = 'day'
9e62f283 3209 else:
3210 if unit == 'week':
3211 unit = 'day'
3212 time *= 7
3213 delta = datetime.timedelta(**{unit + 's': time})
3214 new_date = start_time + delta
3215 if auto_precision:
3216 return datetime_round(new_date, unit)
3217 return new_date
3218
3219 return datetime_round(datetime.datetime.strptime(date_str, format), precision)
3220
3221
3222def date_from_str(date_str, format='%Y%m%d'):
3223 """
3224 Return a datetime object from a string in the format YYYYMMDD or
3225 (now|today|date)[+-][0-9](microsecond|second|minute|hour|day|week|month|year)(s)?
3226
3227 format: string date format used to return datetime object from
3228 """
3229 return datetime_from_str(date_str, precision='microsecond', format=format).date()
3230
3231
3232def datetime_add_months(dt, months):
3233 """Increment/Decrement a datetime object by months."""
3234 month = dt.month + months - 1
3235 year = dt.year + month // 12
3236 month = month % 12 + 1
3237 day = min(dt.day, calendar.monthrange(year, month)[1])
3238 return dt.replace(year, month, day)
3239
3240
3241def datetime_round(dt, precision='day'):
3242 """
3243 Round a datetime object's time to a specific precision
3244 """
3245 if precision == 'microsecond':
3246 return dt
3247
3248 unit_seconds = {
3249 'day': 86400,
3250 'hour': 3600,
3251 'minute': 60,
3252 'second': 1,
3253 }
3254 roundto = lambda x, n: ((x + n / 2) // n) * n
3255 timestamp = calendar.timegm(dt.timetuple())
3256 return datetime.datetime.utcfromtimestamp(roundto(timestamp, unit_seconds[precision]))
5f6a1245
JW
3257
3258
e63fc1be 3259def hyphenate_date(date_str):
3260 """
3261 Convert a date in 'YYYYMMDD' format to 'YYYY-MM-DD' format"""
3262 match = re.match(r'^(\d\d\d\d)(\d\d)(\d\d)$', date_str)
3263 if match is not None:
3264 return '-'.join(match.groups())
3265 else:
3266 return date_str
3267
5f6a1245 3268
bd558525
JMF
3269class DateRange(object):
3270 """Represents a time interval between two dates"""
5f6a1245 3271
bd558525
JMF
3272 def __init__(self, start=None, end=None):
3273 """start and end must be strings in the format accepted by date"""
3274 if start is not None:
3275 self.start = date_from_str(start)
3276 else:
3277 self.start = datetime.datetime.min.date()
3278 if end is not None:
3279 self.end = date_from_str(end)
3280 else:
3281 self.end = datetime.datetime.max.date()
37254abc 3282 if self.start > self.end:
bd558525 3283 raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
5f6a1245 3284
bd558525
JMF
3285 @classmethod
3286 def day(cls, day):
3287 """Returns a range that only contains the given day"""
5f6a1245
JW
3288 return cls(day, day)
3289
bd558525
JMF
3290 def __contains__(self, date):
3291 """Check if the date is in the range"""
37254abc
JMF
3292 if not isinstance(date, datetime.date):
3293 date = date_from_str(date)
3294 return self.start <= date <= self.end
5f6a1245 3295
bd558525 3296 def __str__(self):
5f6a1245 3297 return '%s - %s' % (self.start.isoformat(), self.end.isoformat())
c496ca96
PH
3298
3299
3300def platform_name():
3301 """ Returns the platform name as a compat_str """
3302 res = platform.platform()
3303 if isinstance(res, bytes):
3304 res = res.decode(preferredencoding())
3305
3306 assert isinstance(res, compat_str)
3307 return res
c257baff
PH
3308
3309
49fa4d9a
N
3310def get_windows_version():
3311 ''' Get Windows version. None if it's not running on Windows '''
3312 if compat_os_name == 'nt':
3313 return version_tuple(platform.win32_ver()[1])
3314 else:
3315 return None
3316
3317
b58ddb32
PH
3318def _windows_write_string(s, out):
3319 """ Returns True if the string was written using special methods,
3320 False if it has yet to be written out."""
3321 # Adapted from http://stackoverflow.com/a/3259271/35070
3322
3323 import ctypes
3324 import ctypes.wintypes
3325
3326 WIN_OUTPUT_IDS = {
3327 1: -11,
3328 2: -12,
3329 }
3330
a383a98a
PH
3331 try:
3332 fileno = out.fileno()
3333 except AttributeError:
3334 # If the output stream doesn't have a fileno, it's virtual
3335 return False
aa42e873
PH
3336 except io.UnsupportedOperation:
3337 # Some strange Windows pseudo files?
3338 return False
b58ddb32
PH
3339 if fileno not in WIN_OUTPUT_IDS:
3340 return False
3341
d7cd9a9e 3342 GetStdHandle = compat_ctypes_WINFUNCTYPE(
b58ddb32 3343 ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD)(
d7cd9a9e 3344 ('GetStdHandle', ctypes.windll.kernel32))
b58ddb32
PH
3345 h = GetStdHandle(WIN_OUTPUT_IDS[fileno])
3346
d7cd9a9e 3347 WriteConsoleW = compat_ctypes_WINFUNCTYPE(
b58ddb32
PH
3348 ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE, ctypes.wintypes.LPWSTR,
3349 ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD),
d7cd9a9e 3350 ctypes.wintypes.LPVOID)(('WriteConsoleW', ctypes.windll.kernel32))
b58ddb32
PH
3351 written = ctypes.wintypes.DWORD(0)
3352
d7cd9a9e 3353 GetFileType = compat_ctypes_WINFUNCTYPE(ctypes.wintypes.DWORD, ctypes.wintypes.DWORD)(('GetFileType', ctypes.windll.kernel32))
b58ddb32
PH
3354 FILE_TYPE_CHAR = 0x0002
3355 FILE_TYPE_REMOTE = 0x8000
d7cd9a9e 3356 GetConsoleMode = compat_ctypes_WINFUNCTYPE(
b58ddb32
PH
3357 ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE,
3358 ctypes.POINTER(ctypes.wintypes.DWORD))(
d7cd9a9e 3359 ('GetConsoleMode', ctypes.windll.kernel32))
b58ddb32
PH
3360 INVALID_HANDLE_VALUE = ctypes.wintypes.DWORD(-1).value
3361
3362 def not_a_console(handle):
3363 if handle == INVALID_HANDLE_VALUE or handle is None:
3364 return True
3089bc74
S
3365 return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR
3366 or GetConsoleMode(handle, ctypes.byref(ctypes.wintypes.DWORD())) == 0)
b58ddb32
PH
3367
3368 if not_a_console(h):
3369 return False
3370
d1b9c912
PH
3371 def next_nonbmp_pos(s):
3372 try:
3373 return next(i for i, c in enumerate(s) if ord(c) > 0xffff)
3374 except StopIteration:
3375 return len(s)
3376
3377 while s:
3378 count = min(next_nonbmp_pos(s), 1024)
3379
b58ddb32 3380 ret = WriteConsoleW(
d1b9c912 3381 h, s, count if count else 2, ctypes.byref(written), None)
b58ddb32
PH
3382 if ret == 0:
3383 raise OSError('Failed to write string')
d1b9c912
PH
3384 if not count: # We just wrote a non-BMP character
3385 assert written.value == 2
3386 s = s[1:]
3387 else:
3388 assert written.value > 0
3389 s = s[written.value:]
b58ddb32
PH
3390 return True
3391
3392
734f90bb 3393def write_string(s, out=None, encoding=None):
7459e3a2
PH
3394 if out is None:
3395 out = sys.stderr
8bf48f23 3396 assert type(s) == compat_str
7459e3a2 3397
b58ddb32
PH
3398 if sys.platform == 'win32' and encoding is None and hasattr(out, 'fileno'):
3399 if _windows_write_string(s, out):
3400 return
3401
3089bc74
S
3402 if ('b' in getattr(out, 'mode', '')
3403 or sys.version_info[0] < 3): # Python 2 lies about mode of sys.stderr
104aa738
PH
3404 byt = s.encode(encoding or preferredencoding(), 'ignore')
3405 out.write(byt)
3406 elif hasattr(out, 'buffer'):
3407 enc = encoding or getattr(out, 'encoding', None) or preferredencoding()
3408 byt = s.encode(enc, 'ignore')
3409 out.buffer.write(byt)
3410 else:
8bf48f23 3411 out.write(s)
7459e3a2
PH
3412 out.flush()
3413
3414
48ea9cea
PH
3415def bytes_to_intlist(bs):
3416 if not bs:
3417 return []
3418 if isinstance(bs[0], int): # Python 3
3419 return list(bs)
3420 else:
3421 return [ord(c) for c in bs]
3422
c257baff 3423
cba892fa 3424def intlist_to_bytes(xs):
3425 if not xs:
3426 return b''
edaa23f8 3427 return compat_struct_pack('%dB' % len(xs), *xs)
c38b1e77
PH
3428
3429
c1c9a79c
PH
3430# Cross-platform file locking
3431if sys.platform == 'win32':
3432 import ctypes.wintypes
3433 import msvcrt
3434
3435 class OVERLAPPED(ctypes.Structure):
3436 _fields_ = [
3437 ('Internal', ctypes.wintypes.LPVOID),
3438 ('InternalHigh', ctypes.wintypes.LPVOID),
3439 ('Offset', ctypes.wintypes.DWORD),
3440 ('OffsetHigh', ctypes.wintypes.DWORD),
3441 ('hEvent', ctypes.wintypes.HANDLE),
3442 ]
3443
3444 kernel32 = ctypes.windll.kernel32
3445 LockFileEx = kernel32.LockFileEx
3446 LockFileEx.argtypes = [
3447 ctypes.wintypes.HANDLE, # hFile
3448 ctypes.wintypes.DWORD, # dwFlags
3449 ctypes.wintypes.DWORD, # dwReserved
3450 ctypes.wintypes.DWORD, # nNumberOfBytesToLockLow
3451 ctypes.wintypes.DWORD, # nNumberOfBytesToLockHigh
3452 ctypes.POINTER(OVERLAPPED) # Overlapped
3453 ]
3454 LockFileEx.restype = ctypes.wintypes.BOOL
3455 UnlockFileEx = kernel32.UnlockFileEx
3456 UnlockFileEx.argtypes = [
3457 ctypes.wintypes.HANDLE, # hFile
3458 ctypes.wintypes.DWORD, # dwReserved
3459 ctypes.wintypes.DWORD, # nNumberOfBytesToLockLow
3460 ctypes.wintypes.DWORD, # nNumberOfBytesToLockHigh
3461 ctypes.POINTER(OVERLAPPED) # Overlapped
3462 ]
3463 UnlockFileEx.restype = ctypes.wintypes.BOOL
3464 whole_low = 0xffffffff
3465 whole_high = 0x7fffffff
3466
3467 def _lock_file(f, exclusive):
3468 overlapped = OVERLAPPED()
3469 overlapped.Offset = 0
3470 overlapped.OffsetHigh = 0
3471 overlapped.hEvent = 0
3472 f._lock_file_overlapped_p = ctypes.pointer(overlapped)
3473 handle = msvcrt.get_osfhandle(f.fileno())
3474 if not LockFileEx(handle, 0x2 if exclusive else 0x0, 0,
3475 whole_low, whole_high, f._lock_file_overlapped_p):
3476 raise OSError('Locking file failed: %r' % ctypes.FormatError())
3477
3478 def _unlock_file(f):
3479 assert f._lock_file_overlapped_p
3480 handle = msvcrt.get_osfhandle(f.fileno())
3481 if not UnlockFileEx(handle, 0,
3482 whole_low, whole_high, f._lock_file_overlapped_p):
3483 raise OSError('Unlocking file failed: %r' % ctypes.FormatError())
3484
3485else:
399a76e6
YCH
3486 # Some platforms, such as Jython, is missing fcntl
3487 try:
3488 import fcntl
c1c9a79c 3489
399a76e6
YCH
3490 def _lock_file(f, exclusive):
3491 fcntl.flock(f, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
c1c9a79c 3492
399a76e6
YCH
3493 def _unlock_file(f):
3494 fcntl.flock(f, fcntl.LOCK_UN)
3495 except ImportError:
3496 UNSUPPORTED_MSG = 'file locking is not supported on this platform'
3497
3498 def _lock_file(f, exclusive):
3499 raise IOError(UNSUPPORTED_MSG)
3500
3501 def _unlock_file(f):
3502 raise IOError(UNSUPPORTED_MSG)
c1c9a79c
PH
3503
3504
3505class locked_file(object):
3506 def __init__(self, filename, mode, encoding=None):
3507 assert mode in ['r', 'a', 'w']
3508 self.f = io.open(filename, mode, encoding=encoding)
3509 self.mode = mode
3510
3511 def __enter__(self):
3512 exclusive = self.mode != 'r'
3513 try:
3514 _lock_file(self.f, exclusive)
3515 except IOError:
3516 self.f.close()
3517 raise
3518 return self
3519
3520 def __exit__(self, etype, value, traceback):
3521 try:
3522 _unlock_file(self.f)
3523 finally:
3524 self.f.close()
3525
3526 def __iter__(self):
3527 return iter(self.f)
3528
3529 def write(self, *args):
3530 return self.f.write(*args)
3531
3532 def read(self, *args):
3533 return self.f.read(*args)
4eb7f1d1
JMF
3534
3535
4644ac55
S
3536def get_filesystem_encoding():
3537 encoding = sys.getfilesystemencoding()
3538 return encoding if encoding is not None else 'utf-8'
3539
3540
4eb7f1d1 3541def shell_quote(args):
a6a173c2 3542 quoted_args = []
4644ac55 3543 encoding = get_filesystem_encoding()
a6a173c2
JMF
3544 for a in args:
3545 if isinstance(a, bytes):
3546 # We may get a filename encoded with 'encodeFilename'
3547 a = a.decode(encoding)
aefce8e6 3548 quoted_args.append(compat_shlex_quote(a))
28e614de 3549 return ' '.join(quoted_args)
9d4660ca
PH
3550
3551
3552def smuggle_url(url, data):
3553 """ Pass additional data in a URL for internal use. """
3554
81953d1a
RA
3555 url, idata = unsmuggle_url(url, {})
3556 data.update(idata)
15707c7e 3557 sdata = compat_urllib_parse_urlencode(
28e614de
PH
3558 {'__youtubedl_smuggle': json.dumps(data)})
3559 return url + '#' + sdata
9d4660ca
PH
3560
3561
79f82953 3562def unsmuggle_url(smug_url, default=None):
83e865a3 3563 if '#__youtubedl_smuggle' not in smug_url:
79f82953 3564 return smug_url, default
28e614de
PH
3565 url, _, sdata = smug_url.rpartition('#')
3566 jsond = compat_parse_qs(sdata)['__youtubedl_smuggle'][0]
9d4660ca
PH
3567 data = json.loads(jsond)
3568 return url, data
02dbf93f
PH
3569
3570
02dbf93f
PH
3571def format_bytes(bytes):
3572 if bytes is None:
28e614de 3573 return 'N/A'
02dbf93f
PH
3574 if type(bytes) is str:
3575 bytes = float(bytes)
3576 if bytes == 0.0:
3577 exponent = 0
3578 else:
3579 exponent = int(math.log(bytes, 1024.0))
28e614de 3580 suffix = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'][exponent]
02dbf93f 3581 converted = float(bytes) / float(1024 ** exponent)
28e614de 3582 return '%.2f%s' % (converted, suffix)
f53c966a 3583
1c088fa8 3584
fb47597b
S
3585def lookup_unit_table(unit_table, s):
3586 units_re = '|'.join(re.escape(u) for u in unit_table)
3587 m = re.match(
782b1b5b 3588 r'(?P<num>[0-9]+(?:[,.][0-9]*)?)\s*(?P<unit>%s)\b' % units_re, s)
fb47597b
S
3589 if not m:
3590 return None
3591 num_str = m.group('num').replace(',', '.')
3592 mult = unit_table[m.group('unit')]
3593 return int(float(num_str) * mult)
3594
3595
be64b5b0
PH
3596def parse_filesize(s):
3597 if s is None:
3598 return None
3599
dfb1b146 3600 # The lower-case forms are of course incorrect and unofficial,
be64b5b0
PH
3601 # but we support those too
3602 _UNIT_TABLE = {
3603 'B': 1,
3604 'b': 1,
70852b47 3605 'bytes': 1,
be64b5b0
PH
3606 'KiB': 1024,
3607 'KB': 1000,
3608 'kB': 1024,
3609 'Kb': 1000,
13585d76 3610 'kb': 1000,
70852b47
YCH
3611 'kilobytes': 1000,
3612 'kibibytes': 1024,
be64b5b0
PH
3613 'MiB': 1024 ** 2,
3614 'MB': 1000 ** 2,
3615 'mB': 1024 ** 2,
3616 'Mb': 1000 ** 2,
13585d76 3617 'mb': 1000 ** 2,
70852b47
YCH
3618 'megabytes': 1000 ** 2,
3619 'mebibytes': 1024 ** 2,
be64b5b0
PH
3620 'GiB': 1024 ** 3,
3621 'GB': 1000 ** 3,
3622 'gB': 1024 ** 3,
3623 'Gb': 1000 ** 3,
13585d76 3624 'gb': 1000 ** 3,
70852b47
YCH
3625 'gigabytes': 1000 ** 3,
3626 'gibibytes': 1024 ** 3,
be64b5b0
PH
3627 'TiB': 1024 ** 4,
3628 'TB': 1000 ** 4,
3629 'tB': 1024 ** 4,
3630 'Tb': 1000 ** 4,
13585d76 3631 'tb': 1000 ** 4,
70852b47
YCH
3632 'terabytes': 1000 ** 4,
3633 'tebibytes': 1024 ** 4,
be64b5b0
PH
3634 'PiB': 1024 ** 5,
3635 'PB': 1000 ** 5,
3636 'pB': 1024 ** 5,
3637 'Pb': 1000 ** 5,
13585d76 3638 'pb': 1000 ** 5,
70852b47
YCH
3639 'petabytes': 1000 ** 5,
3640 'pebibytes': 1024 ** 5,
be64b5b0
PH
3641 'EiB': 1024 ** 6,
3642 'EB': 1000 ** 6,
3643 'eB': 1024 ** 6,
3644 'Eb': 1000 ** 6,
13585d76 3645 'eb': 1000 ** 6,
70852b47
YCH
3646 'exabytes': 1000 ** 6,
3647 'exbibytes': 1024 ** 6,
be64b5b0
PH
3648 'ZiB': 1024 ** 7,
3649 'ZB': 1000 ** 7,
3650 'zB': 1024 ** 7,
3651 'Zb': 1000 ** 7,
13585d76 3652 'zb': 1000 ** 7,
70852b47
YCH
3653 'zettabytes': 1000 ** 7,
3654 'zebibytes': 1024 ** 7,
be64b5b0
PH
3655 'YiB': 1024 ** 8,
3656 'YB': 1000 ** 8,
3657 'yB': 1024 ** 8,
3658 'Yb': 1000 ** 8,
13585d76 3659 'yb': 1000 ** 8,
70852b47
YCH
3660 'yottabytes': 1000 ** 8,
3661 'yobibytes': 1024 ** 8,
be64b5b0
PH
3662 }
3663
fb47597b
S
3664 return lookup_unit_table(_UNIT_TABLE, s)
3665
3666
3667def parse_count(s):
3668 if s is None:
be64b5b0
PH
3669 return None
3670
fb47597b
S
3671 s = s.strip()
3672
3673 if re.match(r'^[\d,.]+$', s):
3674 return str_to_int(s)
3675
3676 _UNIT_TABLE = {
3677 'k': 1000,
3678 'K': 1000,
3679 'm': 1000 ** 2,
3680 'M': 1000 ** 2,
3681 'kk': 1000 ** 2,
3682 'KK': 1000 ** 2,
3683 }
be64b5b0 3684
fb47597b 3685 return lookup_unit_table(_UNIT_TABLE, s)
be64b5b0 3686
2f7ae819 3687
b871d7e9
S
3688def parse_resolution(s):
3689 if s is None:
3690 return {}
3691
3692 mobj = re.search(r'\b(?P<w>\d+)\s*[xX×]\s*(?P<h>\d+)\b', s)
3693 if mobj:
3694 return {
3695 'width': int(mobj.group('w')),
3696 'height': int(mobj.group('h')),
3697 }
3698
3699 mobj = re.search(r'\b(\d+)[pPiI]\b', s)
3700 if mobj:
3701 return {'height': int(mobj.group(1))}
3702
3703 mobj = re.search(r'\b([48])[kK]\b', s)
3704 if mobj:
3705 return {'height': int(mobj.group(1)) * 540}
3706
3707 return {}
3708
3709
0dc41787
S
3710def parse_bitrate(s):
3711 if not isinstance(s, compat_str):
3712 return
3713 mobj = re.search(r'\b(\d+)\s*kbps', s)
3714 if mobj:
3715 return int(mobj.group(1))
3716
3717
a942d6cb 3718def month_by_name(name, lang='en'):
caefb1de
PH
3719 """ Return the number of a month by (locale-independently) English name """
3720
f6717dec 3721 month_names = MONTH_NAMES.get(lang, MONTH_NAMES['en'])
a942d6cb 3722
caefb1de 3723 try:
f6717dec 3724 return month_names.index(name) + 1
7105440c
YCH
3725 except ValueError:
3726 return None
3727
3728
3729def month_by_abbreviation(abbrev):
3730 """ Return the number of a month by (locale-independently) English
3731 abbreviations """
3732
3733 try:
3734 return [s[:3] for s in ENGLISH_MONTH_NAMES].index(abbrev) + 1
caefb1de
PH
3735 except ValueError:
3736 return None
18258362
JMF
3737
3738
5aafe895 3739def fix_xml_ampersands(xml_str):
18258362 3740 """Replace all the '&' by '&amp;' in XML"""
5aafe895
PH
3741 return re.sub(
3742 r'&(?!amp;|lt;|gt;|apos;|quot;|#x[0-9a-fA-F]{,4};|#[0-9]{,4};)',
28e614de 3743 '&amp;',
5aafe895 3744 xml_str)
e3946f98
PH
3745
3746
3747def setproctitle(title):
8bf48f23 3748 assert isinstance(title, compat_str)
c1c05c67
YCH
3749
3750 # ctypes in Jython is not complete
3751 # http://bugs.jython.org/issue2148
3752 if sys.platform.startswith('java'):
3753 return
3754
e3946f98 3755 try:
611c1dd9 3756 libc = ctypes.cdll.LoadLibrary('libc.so.6')
e3946f98
PH
3757 except OSError:
3758 return
2f49bcd6
RC
3759 except TypeError:
3760 # LoadLibrary in Windows Python 2.7.13 only expects
3761 # a bytestring, but since unicode_literals turns
3762 # every string into a unicode string, it fails.
3763 return
6eefe533
PH
3764 title_bytes = title.encode('utf-8')
3765 buf = ctypes.create_string_buffer(len(title_bytes))
3766 buf.value = title_bytes
e3946f98 3767 try:
6eefe533 3768 libc.prctl(15, buf, 0, 0, 0)
e3946f98
PH
3769 except AttributeError:
3770 return # Strange libc, just skip this
d7dda168
PH
3771
3772
3773def remove_start(s, start):
46bc9b7d 3774 return s[len(start):] if s is not None and s.startswith(start) else s
29eb5174
PH
3775
3776
2b9faf55 3777def remove_end(s, end):
46bc9b7d 3778 return s[:-len(end)] if s is not None and s.endswith(end) else s
2b9faf55
PH
3779
3780
31b2051e
S
3781def remove_quotes(s):
3782 if s is None or len(s) < 2:
3783 return s
3784 for quote in ('"', "'", ):
3785 if s[0] == quote and s[-1] == quote:
3786 return s[1:-1]
3787 return s
3788
3789
b6e0c7d2
U
3790def get_domain(url):
3791 domain = re.match(r'(?:https?:\/\/)?(?:www\.)?(?P<domain>[^\n\/]+\.[^\n\/]+)(?:\/(.*))?', url)
3792 return domain.group('domain') if domain else None
3793
3794
29eb5174 3795def url_basename(url):
9b8aaeed 3796 path = compat_urlparse.urlparse(url).path
28e614de 3797 return path.strip('/').split('/')[-1]
aa94a6d3
PH
3798
3799
02dc0a36
S
3800def base_url(url):
3801 return re.match(r'https?://[^?#&]+/', url).group()
3802
3803
e34c3361 3804def urljoin(base, path):
4b5de77b
S
3805 if isinstance(path, bytes):
3806 path = path.decode('utf-8')
e34c3361
S
3807 if not isinstance(path, compat_str) or not path:
3808 return None
fad4ceb5 3809 if re.match(r'^(?:[a-zA-Z][a-zA-Z0-9+-.]*:)?//', path):
e34c3361 3810 return path
4b5de77b
S
3811 if isinstance(base, bytes):
3812 base = base.decode('utf-8')
3813 if not isinstance(base, compat_str) or not re.match(
3814 r'^(?:https?:)?//', base):
e34c3361
S
3815 return None
3816 return compat_urlparse.urljoin(base, path)
3817
3818
aa94a6d3
PH
3819class HEADRequest(compat_urllib_request.Request):
3820 def get_method(self):
611c1dd9 3821 return 'HEAD'
7217e148
PH
3822
3823
95cf60e8
S
3824class PUTRequest(compat_urllib_request.Request):
3825 def get_method(self):
3826 return 'PUT'
3827
3828
9732d77e 3829def int_or_none(v, scale=1, default=None, get_attr=None, invscale=1):
28746fbd
PH
3830 if get_attr:
3831 if v is not None:
3832 v = getattr(v, get_attr, None)
9572013d
PH
3833 if v == '':
3834 v = None
1812afb7
S
3835 if v is None:
3836 return default
3837 try:
3838 return int(v) * invscale // scale
5e1271c5 3839 except (ValueError, TypeError):
af98f8ff 3840 return default
9732d77e 3841
9572013d 3842
40a90862
JMF
3843def str_or_none(v, default=None):
3844 return default if v is None else compat_str(v)
3845
9732d77e
PH
3846
3847def str_to_int(int_str):
48d4681e 3848 """ A more relaxed version of int_or_none """
42db58ec 3849 if isinstance(int_str, compat_integer_types):
348c6bf1 3850 return int_str
42db58ec
S
3851 elif isinstance(int_str, compat_str):
3852 int_str = re.sub(r'[,\.\+]', '', int_str)
3853 return int_or_none(int_str)
608d11f5
PH
3854
3855
9732d77e 3856def float_or_none(v, scale=1, invscale=1, default=None):
caf80631
S
3857 if v is None:
3858 return default
3859 try:
3860 return float(v) * invscale / scale
5e1271c5 3861 except (ValueError, TypeError):
caf80631 3862 return default
43f775e4
PH
3863
3864
c7e327c4
S
3865def bool_or_none(v, default=None):
3866 return v if isinstance(v, bool) else default
3867
3868
53cd37ba
S
3869def strip_or_none(v, default=None):
3870 return v.strip() if isinstance(v, compat_str) else default
b72b4431
S
3871
3872
af03000a
S
3873def url_or_none(url):
3874 if not url or not isinstance(url, compat_str):
3875 return None
3876 url = url.strip()
29f7c58a 3877 return url if re.match(r'^(?:(?:https?|rt(?:m(?:pt?[es]?|fp)|sp[su]?)|mms|ftps?):)?//', url) else None
af03000a
S
3878
3879
e29663c6 3880def strftime_or_none(timestamp, date_format, default=None):
3881 datetime_object = None
3882 try:
3883 if isinstance(timestamp, compat_numeric_types): # unix timestamp
3884 datetime_object = datetime.datetime.utcfromtimestamp(timestamp)
3885 elif isinstance(timestamp, compat_str): # assume YYYYMMDD
3886 datetime_object = datetime.datetime.strptime(timestamp, '%Y%m%d')
3887 return datetime_object.strftime(date_format)
3888 except (ValueError, TypeError, AttributeError):
3889 return default
3890
3891
608d11f5 3892def parse_duration(s):
8f9312c3 3893 if not isinstance(s, compat_basestring):
608d11f5
PH
3894 return None
3895
ca7b3246
S
3896 s = s.strip()
3897
acaff495 3898 days, hours, mins, secs, ms = [None] * 5
15846398 3899 m = re.match(r'(?:(?:(?:(?P<days>[0-9]+):)?(?P<hours>[0-9]+):)?(?P<mins>[0-9]+):)?(?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?Z?$', s)
acaff495 3900 if m:
3901 days, hours, mins, secs, ms = m.groups()
3902 else:
3903 m = re.match(
056653bb
S
3904 r'''(?ix)(?:P?
3905 (?:
3906 [0-9]+\s*y(?:ears?)?\s*
3907 )?
3908 (?:
3909 [0-9]+\s*m(?:onths?)?\s*
3910 )?
3911 (?:
3912 [0-9]+\s*w(?:eeks?)?\s*
3913 )?
8f4b58d7 3914 (?:
acaff495 3915 (?P<days>[0-9]+)\s*d(?:ays?)?\s*
8f4b58d7 3916 )?
056653bb 3917 T)?
acaff495 3918 (?:
3919 (?P<hours>[0-9]+)\s*h(?:ours?)?\s*
3920 )?
3921 (?:
3922 (?P<mins>[0-9]+)\s*m(?:in(?:ute)?s?)?\s*
3923 )?
3924 (?:
3925 (?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?\s*s(?:ec(?:ond)?s?)?\s*
15846398 3926 )?Z?$''', s)
acaff495 3927 if m:
3928 days, hours, mins, secs, ms = m.groups()
3929 else:
15846398 3930 m = re.match(r'(?i)(?:(?P<hours>[0-9.]+)\s*(?:hours?)|(?P<mins>[0-9.]+)\s*(?:mins?\.?|minutes?)\s*)Z?$', s)
acaff495 3931 if m:
3932 hours, mins = m.groups()
3933 else:
3934 return None
3935
3936 duration = 0
3937 if secs:
3938 duration += float(secs)
3939 if mins:
3940 duration += float(mins) * 60
3941 if hours:
3942 duration += float(hours) * 60 * 60
3943 if days:
3944 duration += float(days) * 24 * 60 * 60
3945 if ms:
3946 duration += float(ms)
3947 return duration
91d7d0b3
JMF
3948
3949
e65e4c88 3950def prepend_extension(filename, ext, expected_real_ext=None):
5f6a1245 3951 name, real_ext = os.path.splitext(filename)
e65e4c88
S
3952 return (
3953 '{0}.{1}{2}'.format(name, ext, real_ext)
3954 if not expected_real_ext or real_ext[1:] == expected_real_ext
3955 else '{0}.{1}'.format(filename, ext))
d70ad093
PH
3956
3957
b3ed15b7
S
3958def replace_extension(filename, ext, expected_real_ext=None):
3959 name, real_ext = os.path.splitext(filename)
3960 return '{0}.{1}'.format(
3961 name if not expected_real_ext or real_ext[1:] == expected_real_ext else filename,
3962 ext)
3963
3964
d70ad093
PH
3965def check_executable(exe, args=[]):
3966 """ Checks if the given binary is installed somewhere in PATH, and returns its name.
3967 args can be a list of arguments for a short output (like -version) """
3968 try:
f5b1bca9 3969 process_communicate_or_kill(subprocess.Popen(
3970 [exe] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE))
d70ad093
PH
3971 except OSError:
3972 return False
3973 return exe
b7ab0590
PH
3974
3975
95807118 3976def get_exe_version(exe, args=['--version'],
cae97f65 3977 version_re=None, unrecognized='present'):
95807118
PH
3978 """ Returns the version of the specified executable,
3979 or False if the executable is not present """
3980 try:
b64d04c1 3981 # STDIN should be redirected too. On UNIX-like systems, ffmpeg triggers
7a5c1cfe 3982 # SIGTTOU if yt-dlp is run in the background.
067aa17e 3983 # See https://github.com/ytdl-org/youtube-dl/issues/955#issuecomment-209789656
f5b1bca9 3984 out, _ = process_communicate_or_kill(subprocess.Popen(
54116803 3985 [encodeArgument(exe)] + args,
00ca7552 3986 stdin=subprocess.PIPE,
f5b1bca9 3987 stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
95807118
PH
3988 except OSError:
3989 return False
cae97f65
PH
3990 if isinstance(out, bytes): # Python 2.x
3991 out = out.decode('ascii', 'ignore')
3992 return detect_exe_version(out, version_re, unrecognized)
3993
3994
3995def detect_exe_version(output, version_re=None, unrecognized='present'):
3996 assert isinstance(output, compat_str)
3997 if version_re is None:
3998 version_re = r'version\s+([-0-9._a-zA-Z]+)'
3999 m = re.search(version_re, output)
95807118
PH
4000 if m:
4001 return m.group(1)
4002 else:
4003 return unrecognized
4004
4005
cb89cfc1 4006class LazyList(collections.abc.Sequence):
483336e7 4007 ''' Lazy immutable list from an iterable
4008 Note that slices of a LazyList are lists and not LazyList'''
4009
8e5fecc8 4010 class IndexError(IndexError):
4011 pass
4012
483336e7 4013 def __init__(self, iterable):
4014 self.__iterable = iter(iterable)
4015 self.__cache = []
28419ca2 4016 self.__reversed = False
483336e7 4017
4018 def __iter__(self):
28419ca2 4019 if self.__reversed:
4020 # We need to consume the entire iterable to iterate in reverse
981052c9 4021 yield from self.exhaust()
28419ca2 4022 return
4023 yield from self.__cache
483336e7 4024 for item in self.__iterable:
4025 self.__cache.append(item)
4026 yield item
4027
981052c9 4028 def __exhaust(self):
483336e7 4029 self.__cache.extend(self.__iterable)
28419ca2 4030 return self.__cache
4031
981052c9 4032 def exhaust(self):
4033 ''' Evaluate the entire iterable '''
4034 return self.__exhaust()[::-1 if self.__reversed else 1]
4035
28419ca2 4036 @staticmethod
981052c9 4037 def __reverse_index(x):
e0f2b4b4 4038 return None if x is None else -(x + 1)
483336e7 4039
4040 def __getitem__(self, idx):
4041 if isinstance(idx, slice):
28419ca2 4042 if self.__reversed:
e0f2b4b4 4043 idx = slice(self.__reverse_index(idx.start), self.__reverse_index(idx.stop), -(idx.step or 1))
4044 start, stop, step = idx.start, idx.stop, idx.step or 1
483336e7 4045 elif isinstance(idx, int):
28419ca2 4046 if self.__reversed:
981052c9 4047 idx = self.__reverse_index(idx)
e0f2b4b4 4048 start, stop, step = idx, idx, 0
483336e7 4049 else:
4050 raise TypeError('indices must be integers or slices')
e0f2b4b4 4051 if ((start or 0) < 0 or (stop or 0) < 0
4052 or (start is None and step < 0)
4053 or (stop is None and step > 0)):
483336e7 4054 # We need to consume the entire iterable to be able to slice from the end
4055 # Obviously, never use this with infinite iterables
8e5fecc8 4056 self.__exhaust()
4057 try:
4058 return self.__cache[idx]
4059 except IndexError as e:
4060 raise self.IndexError(e) from e
e0f2b4b4 4061 n = max(start or 0, stop or 0) - len(self.__cache) + 1
28419ca2 4062 if n > 0:
4063 self.__cache.extend(itertools.islice(self.__iterable, n))
8e5fecc8 4064 try:
4065 return self.__cache[idx]
4066 except IndexError as e:
4067 raise self.IndexError(e) from e
483336e7 4068
4069 def __bool__(self):
4070 try:
28419ca2 4071 self[-1] if self.__reversed else self[0]
8e5fecc8 4072 except self.IndexError:
483336e7 4073 return False
4074 return True
4075
4076 def __len__(self):
8e5fecc8 4077 self.__exhaust()
483336e7 4078 return len(self.__cache)
4079
981052c9 4080 def reverse(self):
28419ca2 4081 self.__reversed = not self.__reversed
4082 return self
4083
4084 def __repr__(self):
4085 # repr and str should mimic a list. So we exhaust the iterable
4086 return repr(self.exhaust())
4087
4088 def __str__(self):
4089 return repr(self.exhaust())
4090
483336e7 4091
7be9ccff 4092class PagedList:
dd26ced1
PH
4093 def __len__(self):
4094 # This is only useful for tests
4095 return len(self.getslice())
4096
7be9ccff 4097 def __init__(self, pagefunc, pagesize, use_cache=True):
4098 self._pagefunc = pagefunc
4099 self._pagesize = pagesize
4100 self._use_cache = use_cache
4101 self._cache = {}
4102
4103 def getpage(self, pagenum):
4104 page_results = self._cache.get(pagenum) or list(self._pagefunc(pagenum))
4105 if self._use_cache:
4106 self._cache[pagenum] = page_results
4107 return page_results
4108
4109 def getslice(self, start=0, end=None):
4110 return list(self._getslice(start, end))
4111
4112 def _getslice(self, start, end):
55575225 4113 raise NotImplementedError('This method must be implemented by subclasses')
4114
4115 def __getitem__(self, idx):
7be9ccff 4116 # NOTE: cache must be enabled if this is used
55575225 4117 if not isinstance(idx, int) or idx < 0:
4118 raise TypeError('indices must be non-negative integers')
4119 entries = self.getslice(idx, idx + 1)
4120 return entries[0] if entries else None
4121
9c44d242
PH
4122
4123class OnDemandPagedList(PagedList):
7be9ccff 4124 def _getslice(self, start, end):
b7ab0590
PH
4125 for pagenum in itertools.count(start // self._pagesize):
4126 firstid = pagenum * self._pagesize
4127 nextfirstid = pagenum * self._pagesize + self._pagesize
4128 if start >= nextfirstid:
4129 continue
4130
b7ab0590
PH
4131 startv = (
4132 start % self._pagesize
4133 if firstid <= start < nextfirstid
4134 else 0)
b7ab0590
PH
4135 endv = (
4136 ((end - 1) % self._pagesize) + 1
4137 if (end is not None and firstid <= end <= nextfirstid)
4138 else None)
4139
7be9ccff 4140 page_results = self.getpage(pagenum)
b7ab0590
PH
4141 if startv != 0 or endv is not None:
4142 page_results = page_results[startv:endv]
7be9ccff 4143 yield from page_results
b7ab0590
PH
4144
4145 # A little optimization - if current page is not "full", ie. does
4146 # not contain page_size videos then we can assume that this page
4147 # is the last one - there are no more ids on further pages -
4148 # i.e. no need to query again.
4149 if len(page_results) + startv < self._pagesize:
4150 break
4151
4152 # If we got the whole page, but the next page is not interesting,
4153 # break out early as well
4154 if end == nextfirstid:
4155 break
81c2f20b
PH
4156
4157
9c44d242
PH
4158class InAdvancePagedList(PagedList):
4159 def __init__(self, pagefunc, pagecount, pagesize):
9c44d242 4160 self._pagecount = pagecount
7be9ccff 4161 PagedList.__init__(self, pagefunc, pagesize, True)
9c44d242 4162
7be9ccff 4163 def _getslice(self, start, end):
9c44d242
PH
4164 start_page = start // self._pagesize
4165 end_page = (
4166 self._pagecount if end is None else (end // self._pagesize + 1))
4167 skip_elems = start - start_page * self._pagesize
4168 only_more = None if end is None else end - start
4169 for pagenum in range(start_page, end_page):
7be9ccff 4170 page_results = self.getpage(pagenum)
9c44d242 4171 if skip_elems:
7be9ccff 4172 page_results = page_results[skip_elems:]
9c44d242
PH
4173 skip_elems = None
4174 if only_more is not None:
7be9ccff 4175 if len(page_results) < only_more:
4176 only_more -= len(page_results)
9c44d242 4177 else:
7be9ccff 4178 yield from page_results[:only_more]
9c44d242 4179 break
7be9ccff 4180 yield from page_results
9c44d242
PH
4181
4182
81c2f20b 4183def uppercase_escape(s):
676eb3f2 4184 unicode_escape = codecs.getdecoder('unicode_escape')
81c2f20b 4185 return re.sub(
a612753d 4186 r'\\U[0-9a-fA-F]{8}',
676eb3f2
PH
4187 lambda m: unicode_escape(m.group(0))[0],
4188 s)
0fe2ff78
YCH
4189
4190
4191def lowercase_escape(s):
4192 unicode_escape = codecs.getdecoder('unicode_escape')
4193 return re.sub(
4194 r'\\u[0-9a-fA-F]{4}',
4195 lambda m: unicode_escape(m.group(0))[0],
4196 s)
b53466e1 4197
d05cfe06
S
4198
4199def escape_rfc3986(s):
4200 """Escape non-ASCII characters as suggested by RFC 3986"""
8f9312c3 4201 if sys.version_info < (3, 0) and isinstance(s, compat_str):
d05cfe06 4202 s = s.encode('utf-8')
ecc0c5ee 4203 return compat_urllib_parse.quote(s, b"%/;:@&=+$,!~*'()?#[]")
d05cfe06
S
4204
4205
4206def escape_url(url):
4207 """Escape URL as suggested by RFC 3986"""
4208 url_parsed = compat_urllib_parse_urlparse(url)
4209 return url_parsed._replace(
efbed08d 4210 netloc=url_parsed.netloc.encode('idna').decode('ascii'),
d05cfe06
S
4211 path=escape_rfc3986(url_parsed.path),
4212 params=escape_rfc3986(url_parsed.params),
4213 query=escape_rfc3986(url_parsed.query),
4214 fragment=escape_rfc3986(url_parsed.fragment)
4215 ).geturl()
4216
62e609ab 4217
4dfbf869 4218def parse_qs(url):
4219 return compat_parse_qs(compat_urllib_parse_urlparse(url).query)
4220
4221
62e609ab
PH
4222def read_batch_urls(batch_fd):
4223 def fixup(url):
4224 if not isinstance(url, compat_str):
4225 url = url.decode('utf-8', 'replace')
8c04f0be 4226 BOM_UTF8 = ('\xef\xbb\xbf', '\ufeff')
4227 for bom in BOM_UTF8:
4228 if url.startswith(bom):
4229 url = url[len(bom):]
4230 url = url.lstrip()
4231 if not url or url.startswith(('#', ';', ']')):
62e609ab 4232 return False
8c04f0be 4233 # "#" cannot be stripped out since it is part of the URI
4234 # However, it can be safely stipped out if follwing a whitespace
4235 return re.split(r'\s#', url, 1)[0].rstrip()
62e609ab
PH
4236
4237 with contextlib.closing(batch_fd) as fd:
4238 return [url for url in map(fixup, fd) if url]
b74fa8cd
JMF
4239
4240
4241def urlencode_postdata(*args, **kargs):
15707c7e 4242 return compat_urllib_parse_urlencode(*args, **kargs).encode('ascii')
bcf89ce6
PH
4243
4244
38f9ef31 4245def update_url_query(url, query):
cacd9966
YCH
4246 if not query:
4247 return url
38f9ef31 4248 parsed_url = compat_urlparse.urlparse(url)
4249 qs = compat_parse_qs(parsed_url.query)
4250 qs.update(query)
4251 return compat_urlparse.urlunparse(parsed_url._replace(
15707c7e 4252 query=compat_urllib_parse_urlencode(qs, True)))
16392824 4253
8e60dc75 4254
ed0291d1
S
4255def update_Request(req, url=None, data=None, headers={}, query={}):
4256 req_headers = req.headers.copy()
4257 req_headers.update(headers)
4258 req_data = data or req.data
4259 req_url = update_url_query(url or req.get_full_url(), query)
95cf60e8
S
4260 req_get_method = req.get_method()
4261 if req_get_method == 'HEAD':
4262 req_type = HEADRequest
4263 elif req_get_method == 'PUT':
4264 req_type = PUTRequest
4265 else:
4266 req_type = compat_urllib_request.Request
ed0291d1
S
4267 new_req = req_type(
4268 req_url, data=req_data, headers=req_headers,
4269 origin_req_host=req.origin_req_host, unverifiable=req.unverifiable)
4270 if hasattr(req, 'timeout'):
4271 new_req.timeout = req.timeout
4272 return new_req
4273
4274
10c87c15 4275def _multipart_encode_impl(data, boundary):
0c265486
YCH
4276 content_type = 'multipart/form-data; boundary=%s' % boundary
4277
4278 out = b''
4279 for k, v in data.items():
4280 out += b'--' + boundary.encode('ascii') + b'\r\n'
4281 if isinstance(k, compat_str):
4282 k = k.encode('utf-8')
4283 if isinstance(v, compat_str):
4284 v = v.encode('utf-8')
4285 # RFC 2047 requires non-ASCII field names to be encoded, while RFC 7578
4286 # suggests sending UTF-8 directly. Firefox sends UTF-8, too
b2ad479d 4287 content = b'Content-Disposition: form-data; name="' + k + b'"\r\n\r\n' + v + b'\r\n'
0c265486
YCH
4288 if boundary.encode('ascii') in content:
4289 raise ValueError('Boundary overlaps with data')
4290 out += content
4291
4292 out += b'--' + boundary.encode('ascii') + b'--\r\n'
4293
4294 return out, content_type
4295
4296
4297def multipart_encode(data, boundary=None):
4298 '''
4299 Encode a dict to RFC 7578-compliant form-data
4300
4301 data:
4302 A dict where keys and values can be either Unicode or bytes-like
4303 objects.
4304 boundary:
4305 If specified a Unicode object, it's used as the boundary. Otherwise
4306 a random boundary is generated.
4307
4308 Reference: https://tools.ietf.org/html/rfc7578
4309 '''
4310 has_specified_boundary = boundary is not None
4311
4312 while True:
4313 if boundary is None:
4314 boundary = '---------------' + str(random.randrange(0x0fffffff, 0xffffffff))
4315
4316 try:
10c87c15 4317 out, content_type = _multipart_encode_impl(data, boundary)
0c265486
YCH
4318 break
4319 except ValueError:
4320 if has_specified_boundary:
4321 raise
4322 boundary = None
4323
4324 return out, content_type
4325
4326
86296ad2 4327def dict_get(d, key_or_keys, default=None, skip_false_values=True):
cbecc9b9
S
4328 if isinstance(key_or_keys, (list, tuple)):
4329 for key in key_or_keys:
86296ad2
S
4330 if key not in d or d[key] is None or skip_false_values and not d[key]:
4331 continue
4332 return d[key]
cbecc9b9
S
4333 return default
4334 return d.get(key_or_keys, default)
4335
4336
329ca3be 4337def try_get(src, getter, expected_type=None):
6606817a 4338 for get in variadic(getter):
a32a9a7e
S
4339 try:
4340 v = get(src)
4341 except (AttributeError, KeyError, TypeError, IndexError):
4342 pass
4343 else:
4344 if expected_type is None or isinstance(v, expected_type):
4345 return v
329ca3be
S
4346
4347
6cc62232
S
4348def merge_dicts(*dicts):
4349 merged = {}
4350 for a_dict in dicts:
4351 for k, v in a_dict.items():
4352 if v is None:
4353 continue
3089bc74
S
4354 if (k not in merged
4355 or (isinstance(v, compat_str) and v
4356 and isinstance(merged[k], compat_str)
4357 and not merged[k])):
6cc62232
S
4358 merged[k] = v
4359 return merged
4360
4361
8e60dc75
S
4362def encode_compat_str(string, encoding=preferredencoding(), errors='strict'):
4363 return string if isinstance(string, compat_str) else compat_str(string, encoding, errors)
4364
16392824 4365
a1a530b0
PH
4366US_RATINGS = {
4367 'G': 0,
4368 'PG': 10,
4369 'PG-13': 13,
4370 'R': 16,
4371 'NC': 18,
4372}
fac55558
PH
4373
4374
a8795327 4375TV_PARENTAL_GUIDELINES = {
5a16c9d9
RA
4376 'TV-Y': 0,
4377 'TV-Y7': 7,
4378 'TV-G': 0,
4379 'TV-PG': 0,
4380 'TV-14': 14,
4381 'TV-MA': 17,
a8795327
S
4382}
4383
4384
146c80e2 4385def parse_age_limit(s):
a8795327
S
4386 if type(s) == int:
4387 return s if 0 <= s <= 21 else None
4388 if not isinstance(s, compat_basestring):
d838b1bd 4389 return None
146c80e2 4390 m = re.match(r'^(?P<age>\d{1,2})\+?$', s)
a8795327
S
4391 if m:
4392 return int(m.group('age'))
5c5fae6d 4393 s = s.upper()
a8795327
S
4394 if s in US_RATINGS:
4395 return US_RATINGS[s]
5a16c9d9 4396 m = re.match(r'^TV[_-]?(%s)$' % '|'.join(k[3:] for k in TV_PARENTAL_GUIDELINES), s)
b8361187 4397 if m:
5a16c9d9 4398 return TV_PARENTAL_GUIDELINES['TV-' + m.group(1)]
b8361187 4399 return None
146c80e2
S
4400
4401
fac55558 4402def strip_jsonp(code):
609a61e3 4403 return re.sub(
5552c9eb 4404 r'''(?sx)^
e9c671d5 4405 (?:window\.)?(?P<func_name>[a-zA-Z0-9_.$]*)
5552c9eb
YCH
4406 (?:\s*&&\s*(?P=func_name))?
4407 \s*\(\s*(?P<callback_data>.*)\);?
4408 \s*?(?://[^\n]*)*$''',
4409 r'\g<callback_data>', code)
478c2c61
PH
4410
4411
5c610515 4412def js_to_json(code, vars={}):
4413 # vars is a dict of var, val pairs to substitute
c843e685 4414 COMMENT_RE = r'/\*(?:(?!\*/).)*?\*/|//[^\n]*\n'
4195096e
S
4415 SKIP_RE = r'\s*(?:{comment})?\s*'.format(comment=COMMENT_RE)
4416 INTEGER_TABLE = (
4417 (r'(?s)^(0[xX][0-9a-fA-F]+){skip}:?$'.format(skip=SKIP_RE), 16),
4418 (r'(?s)^(0+[0-7]+){skip}:?$'.format(skip=SKIP_RE), 8),
4419 )
4420
e05f6939 4421 def fix_kv(m):
e7b6d122
PH
4422 v = m.group(0)
4423 if v in ('true', 'false', 'null'):
4424 return v
421ddcb8
C
4425 elif v in ('undefined', 'void 0'):
4426 return 'null'
8bdd16b4 4427 elif v.startswith('/*') or v.startswith('//') or v.startswith('!') or v == ',':
bd1e4844 4428 return ""
4429
4430 if v[0] in ("'", '"'):
4431 v = re.sub(r'(?s)\\.|"', lambda m: {
e7b6d122 4432 '"': '\\"',
bd1e4844 4433 "\\'": "'",
4434 '\\\n': '',
4435 '\\x': '\\u00',
4436 }.get(m.group(0), m.group(0)), v[1:-1])
8bdd16b4 4437 else:
4438 for regex, base in INTEGER_TABLE:
4439 im = re.match(regex, v)
4440 if im:
4441 i = int(im.group(1), base)
4442 return '"%d":' % i if v.endswith(':') else '%d' % i
89ac4a19 4443
5c610515 4444 if v in vars:
4445 return vars[v]
4446
e7b6d122 4447 return '"%s"' % v
e05f6939 4448
bd1e4844 4449 return re.sub(r'''(?sx)
4450 "(?:[^"\\]*(?:\\\\|\\['"nurtbfx/\n]))*[^"\\]*"|
4451 '(?:[^'\\]*(?:\\\\|\\['"nurtbfx/\n]))*[^'\\]*'|
4195096e 4452 {comment}|,(?={skip}[\]}}])|
421ddcb8 4453 void\s0|(?:(?<![0-9])[eE]|[a-df-zA-DF-Z_$])[.a-zA-Z_$0-9]*|
4195096e 4454 \b(?:0[xX][0-9a-fA-F]+|0+[0-7]+)(?:{skip}:)?|
8bdd16b4 4455 [0-9]+(?={skip}:)|
4456 !+
4195096e 4457 '''.format(comment=COMMENT_RE, skip=SKIP_RE), fix_kv, code)
e05f6939
PH
4458
4459
478c2c61
PH
4460def qualities(quality_ids):
4461 """ Get a numeric quality value out of a list of possible values """
4462 def q(qid):
4463 try:
4464 return quality_ids.index(qid)
4465 except ValueError:
4466 return -1
4467 return q
4468
acd69589 4469
de6000d9 4470DEFAULT_OUTTMPL = {
4471 'default': '%(title)s [%(id)s].%(ext)s',
72755351 4472 'chapter': '%(title)s - %(section_number)03d %(section_title)s [%(id)s].%(ext)s',
de6000d9 4473}
4474OUTTMPL_TYPES = {
72755351 4475 'chapter': None,
de6000d9 4476 'subtitle': None,
4477 'thumbnail': None,
4478 'description': 'description',
4479 'annotation': 'annotations.xml',
4480 'infojson': 'info.json',
5112f26a 4481 'pl_thumbnail': None,
de6000d9 4482 'pl_description': 'description',
4483 'pl_infojson': 'info.json',
4484}
0a871f68 4485
143db31d 4486# As of [1] format syntax is:
4487# %[mapping_key][conversion_flags][minimum_width][.precision][length_modifier]type
4488# 1. https://docs.python.org/2/library/stdtypes.html#string-formatting
901130bb 4489STR_FORMAT_RE_TMPL = r'''(?x)
4490 (?<!%)(?P<prefix>(?:%%)*)
143db31d 4491 %
524e2e4f 4492 (?P<has_key>\((?P<key>{0})\))?
752cda38 4493 (?P<format>
524e2e4f 4494 (?P<conversion>[#0\-+ ]+)?
4495 (?P<min_width>\d+)?
4496 (?P<precision>\.\d+)?
4497 (?P<len_mod>[hlL])? # unused in python
901130bb 4498 {1} # conversion type
752cda38 4499 )
143db31d 4500'''
4501
7d1eb38a 4502
901130bb 4503STR_FORMAT_TYPES = 'diouxXeEfFgGcrs'
a020a0dc 4504
7d1eb38a 4505
a020a0dc
PH
4506def limit_length(s, length):
4507 """ Add ellipses to overly long strings """
4508 if s is None:
4509 return None
4510 ELLIPSES = '...'
4511 if len(s) > length:
4512 return s[:length - len(ELLIPSES)] + ELLIPSES
4513 return s
48844745
PH
4514
4515
4516def version_tuple(v):
5f9b8394 4517 return tuple(int(e) for e in re.split(r'[-.]', v))
48844745
PH
4518
4519
4520def is_outdated_version(version, limit, assume_new=True):
4521 if not version:
4522 return not assume_new
4523 try:
4524 return version_tuple(version) < version_tuple(limit)
4525 except ValueError:
4526 return not assume_new
732ea2f0
PH
4527
4528
4529def ytdl_is_updateable():
7a5c1cfe 4530 """ Returns if yt-dlp can be updated with -U """
735d865e 4531
5d535b4a 4532 from .update import is_non_updateable
732ea2f0 4533
5d535b4a 4534 return not is_non_updateable()
7d4111ed
PH
4535
4536
4537def args_to_str(args):
4538 # Get a short string representation for a subprocess command
702ccf2d 4539 return ' '.join(compat_shlex_quote(a) for a in args)
2ccd1b10
PH
4540
4541
9b9c5355 4542def error_to_compat_str(err):
fdae2358
S
4543 err_str = str(err)
4544 # On python 2 error byte string must be decoded with proper
4545 # encoding rather than ascii
4546 if sys.version_info[0] < 3:
4547 err_str = err_str.decode(preferredencoding())
4548 return err_str
4549
4550
c460bdd5 4551def mimetype2ext(mt):
eb9ee194
S
4552 if mt is None:
4553 return None
4554
9359f3d4
F
4555 mt, _, params = mt.partition(';')
4556 mt = mt.strip()
4557
4558 FULL_MAP = {
765ac263 4559 'audio/mp4': 'm4a',
6c33d24b
YCH
4560 # Per RFC 3003, audio/mpeg can be .mp1, .mp2 or .mp3. Here use .mp3 as
4561 # it's the most popular one
4562 'audio/mpeg': 'mp3',
ba39289d 4563 'audio/x-wav': 'wav',
9359f3d4
F
4564 'audio/wav': 'wav',
4565 'audio/wave': 'wav',
4566 }
4567
4568 ext = FULL_MAP.get(mt)
765ac263
JMF
4569 if ext is not None:
4570 return ext
4571
9359f3d4 4572 SUBTYPE_MAP = {
f6861ec9 4573 '3gpp': '3gp',
cafcf657 4574 'smptett+xml': 'tt',
cafcf657 4575 'ttaf+xml': 'dfxp',
a0d8d704 4576 'ttml+xml': 'ttml',
f6861ec9 4577 'x-flv': 'flv',
a0d8d704 4578 'x-mp4-fragmented': 'mp4',
d4f05d47 4579 'x-ms-sami': 'sami',
a0d8d704 4580 'x-ms-wmv': 'wmv',
b4173f15
RA
4581 'mpegurl': 'm3u8',
4582 'x-mpegurl': 'm3u8',
4583 'vnd.apple.mpegurl': 'm3u8',
4584 'dash+xml': 'mpd',
b4173f15 4585 'f4m+xml': 'f4m',
f164b971 4586 'hds+xml': 'f4m',
e910fe2f 4587 'vnd.ms-sstr+xml': 'ism',
c2b2c7e1 4588 'quicktime': 'mov',
98ce1a3f 4589 'mp2t': 'ts',
39e7107d 4590 'x-wav': 'wav',
9359f3d4
F
4591 'filmstrip+json': 'fs',
4592 'svg+xml': 'svg',
4593 }
4594
4595 _, _, subtype = mt.rpartition('/')
4596 ext = SUBTYPE_MAP.get(subtype.lower())
4597 if ext is not None:
4598 return ext
4599
4600 SUFFIX_MAP = {
4601 'json': 'json',
4602 'xml': 'xml',
4603 'zip': 'zip',
4604 'gzip': 'gz',
4605 }
4606
4607 _, _, suffix = subtype.partition('+')
4608 ext = SUFFIX_MAP.get(suffix)
4609 if ext is not None:
4610 return ext
4611
4612 return subtype.replace('+', '.')
c460bdd5
PH
4613
4614
4f3c5e06 4615def parse_codecs(codecs_str):
4616 # http://tools.ietf.org/html/rfc6381
4617 if not codecs_str:
4618 return {}
a0566bbf 4619 split_codecs = list(filter(None, map(
dbf5416a 4620 str.strip, codecs_str.strip().strip(',').split(','))))
4f3c5e06 4621 vcodec, acodec = None, None
a0566bbf 4622 for full_codec in split_codecs:
4f3c5e06 4623 codec = full_codec.split('.')[0]
28cc2241 4624 if codec in ('avc1', 'avc2', 'avc3', 'avc4', 'vp9', 'vp8', 'hev1', 'hev2', 'h263', 'h264', 'mp4v', 'hvc1', 'av01', 'theora'):
4f3c5e06 4625 if not vcodec:
4626 vcodec = full_codec
60f5c9fb 4627 elif codec in ('mp4a', 'opus', 'vorbis', 'mp3', 'aac', 'ac-3', 'ec-3', 'eac3', 'dtsc', 'dtse', 'dtsh', 'dtsl'):
4f3c5e06 4628 if not acodec:
4629 acodec = full_codec
4630 else:
60f5c9fb 4631 write_string('WARNING: Unknown codec %s\n' % full_codec, sys.stderr)
4f3c5e06 4632 if not vcodec and not acodec:
a0566bbf 4633 if len(split_codecs) == 2:
4f3c5e06 4634 return {
a0566bbf 4635 'vcodec': split_codecs[0],
4636 'acodec': split_codecs[1],
4f3c5e06 4637 }
4638 else:
4639 return {
4640 'vcodec': vcodec or 'none',
4641 'acodec': acodec or 'none',
4642 }
4643 return {}
4644
4645
2ccd1b10 4646def urlhandle_detect_ext(url_handle):
79298173 4647 getheader = url_handle.headers.get
2ccd1b10 4648
b55ee18f
PH
4649 cd = getheader('Content-Disposition')
4650 if cd:
4651 m = re.match(r'attachment;\s*filename="(?P<filename>[^"]+)"', cd)
4652 if m:
4653 e = determine_ext(m.group('filename'), default_ext=None)
4654 if e:
4655 return e
4656
c460bdd5 4657 return mimetype2ext(getheader('Content-Type'))
05900629
PH
4658
4659
1e399778
YCH
4660def encode_data_uri(data, mime_type):
4661 return 'data:%s;base64,%s' % (mime_type, base64.b64encode(data).decode('ascii'))
4662
4663
05900629 4664def age_restricted(content_limit, age_limit):
6ec6cb4e 4665 """ Returns True iff the content should be blocked """
05900629
PH
4666
4667 if age_limit is None: # No limit set
4668 return False
4669 if content_limit is None:
4670 return False # Content available for everyone
4671 return age_limit < content_limit
61ca9a80
PH
4672
4673
4674def is_html(first_bytes):
4675 """ Detect whether a file contains HTML by examining its first bytes. """
4676
4677 BOMS = [
4678 (b'\xef\xbb\xbf', 'utf-8'),
4679 (b'\x00\x00\xfe\xff', 'utf-32-be'),
4680 (b'\xff\xfe\x00\x00', 'utf-32-le'),
4681 (b'\xff\xfe', 'utf-16-le'),
4682 (b'\xfe\xff', 'utf-16-be'),
4683 ]
4684 for bom, enc in BOMS:
4685 if first_bytes.startswith(bom):
4686 s = first_bytes[len(bom):].decode(enc, 'replace')
4687 break
4688 else:
4689 s = first_bytes.decode('utf-8', 'replace')
4690
4691 return re.match(r'^\s*<', s)
a055469f
PH
4692
4693
4694def determine_protocol(info_dict):
4695 protocol = info_dict.get('protocol')
4696 if protocol is not None:
4697 return protocol
4698
4699 url = info_dict['url']
4700 if url.startswith('rtmp'):
4701 return 'rtmp'
4702 elif url.startswith('mms'):
4703 return 'mms'
4704 elif url.startswith('rtsp'):
4705 return 'rtsp'
4706
4707 ext = determine_ext(url)
4708 if ext == 'm3u8':
4709 return 'm3u8'
4710 elif ext == 'f4m':
4711 return 'f4m'
4712
4713 return compat_urllib_parse_urlparse(url).scheme
cfb56d1a
PH
4714
4715
76d321f6 4716def render_table(header_row, data, delim=False, extraGap=0, hideEmpty=False):
cfb56d1a 4717 """ Render a list of rows, each as a list of values """
76d321f6 4718
4719 def get_max_lens(table):
4720 return [max(len(compat_str(v)) for v in col) for col in zip(*table)]
4721
4722 def filter_using_list(row, filterArray):
4723 return [col for (take, col) in zip(filterArray, row) if take]
4724
4725 if hideEmpty:
4726 max_lens = get_max_lens(data)
4727 header_row = filter_using_list(header_row, max_lens)
4728 data = [filter_using_list(row, max_lens) for row in data]
4729
cfb56d1a 4730 table = [header_row] + data
76d321f6 4731 max_lens = get_max_lens(table)
4732 if delim:
4733 table = [header_row] + [['-' * ml for ml in max_lens]] + data
4734 format_str = ' '.join('%-' + compat_str(ml + extraGap) + 's' for ml in max_lens[:-1]) + ' %s'
cfb56d1a 4735 return '\n'.join(format_str % tuple(row) for row in table)
347de493
PH
4736
4737
8f18aca8 4738def _match_one(filter_part, dct, incomplete):
77b87f05 4739 # TODO: Generalize code with YoutubeDL._build_format_filter
a047eeb6 4740 STRING_OPERATORS = {
4741 '*=': operator.contains,
4742 '^=': lambda attr, value: attr.startswith(value),
4743 '$=': lambda attr, value: attr.endswith(value),
4744 '~=': lambda attr, value: re.search(value, attr),
4745 }
347de493 4746 COMPARISON_OPERATORS = {
a047eeb6 4747 **STRING_OPERATORS,
4748 '<=': operator.le, # "<=" must be defined above "<"
347de493 4749 '<': operator.lt,
347de493 4750 '>=': operator.ge,
a047eeb6 4751 '>': operator.gt,
347de493 4752 '=': operator.eq,
347de493 4753 }
a047eeb6 4754
347de493
PH
4755 operator_rex = re.compile(r'''(?x)\s*
4756 (?P<key>[a-z_]+)
77b87f05 4757 \s*(?P<negation>!\s*)?(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
347de493
PH
4758 (?:
4759 (?P<intval>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)|
a047eeb6 4760 (?P<quote>["\'])(?P<quotedstrval>.+?)(?P=quote)|
4761 (?P<strval>.+?)
347de493
PH
4762 )
4763 \s*$
4764 ''' % '|'.join(map(re.escape, COMPARISON_OPERATORS.keys())))
4765 m = operator_rex.search(filter_part)
4766 if m:
77b87f05
MT
4767 unnegated_op = COMPARISON_OPERATORS[m.group('op')]
4768 if m.group('negation'):
4769 op = lambda attr, value: not unnegated_op(attr, value)
4770 else:
4771 op = unnegated_op
e5a088dc 4772 actual_value = dct.get(m.group('key'))
3089bc74
S
4773 if (m.group('quotedstrval') is not None
4774 or m.group('strval') is not None
e5a088dc
S
4775 # If the original field is a string and matching comparisonvalue is
4776 # a number we should respect the origin of the original field
4777 # and process comparison value as a string (see
067aa17e 4778 # https://github.com/ytdl-org/youtube-dl/issues/11082).
3089bc74
S
4779 or actual_value is not None and m.group('intval') is not None
4780 and isinstance(actual_value, compat_str)):
db13c16e
S
4781 comparison_value = m.group('quotedstrval') or m.group('strval') or m.group('intval')
4782 quote = m.group('quote')
4783 if quote is not None:
4784 comparison_value = comparison_value.replace(r'\%s' % quote, quote)
347de493 4785 else:
a047eeb6 4786 if m.group('op') in STRING_OPERATORS:
4787 raise ValueError('Operator %s only supports string values!' % m.group('op'))
347de493
PH
4788 try:
4789 comparison_value = int(m.group('intval'))
4790 except ValueError:
4791 comparison_value = parse_filesize(m.group('intval'))
4792 if comparison_value is None:
4793 comparison_value = parse_filesize(m.group('intval') + 'B')
4794 if comparison_value is None:
4795 raise ValueError(
4796 'Invalid integer value %r in filter part %r' % (
4797 m.group('intval'), filter_part))
347de493 4798 if actual_value is None:
8f18aca8 4799 return incomplete or m.group('none_inclusive')
347de493
PH
4800 return op(actual_value, comparison_value)
4801
4802 UNARY_OPERATORS = {
1cc47c66
S
4803 '': lambda v: (v is True) if isinstance(v, bool) else (v is not None),
4804 '!': lambda v: (v is False) if isinstance(v, bool) else (v is None),
347de493
PH
4805 }
4806 operator_rex = re.compile(r'''(?x)\s*
4807 (?P<op>%s)\s*(?P<key>[a-z_]+)
4808 \s*$
4809 ''' % '|'.join(map(re.escape, UNARY_OPERATORS.keys())))
4810 m = operator_rex.search(filter_part)
4811 if m:
4812 op = UNARY_OPERATORS[m.group('op')]
4813 actual_value = dct.get(m.group('key'))
8f18aca8 4814 if incomplete and actual_value is None:
4815 return True
347de493
PH
4816 return op(actual_value)
4817
4818 raise ValueError('Invalid filter part %r' % filter_part)
4819
4820
8f18aca8 4821def match_str(filter_str, dct, incomplete=False):
4822 """ Filter a dictionary with a simple string syntax. Returns True (=passes filter) or false
4823 When incomplete, all conditions passes on missing fields
4824 """
347de493 4825 return all(
8f18aca8 4826 _match_one(filter_part.replace(r'\&', '&'), dct, incomplete)
a047eeb6 4827 for filter_part in re.split(r'(?<!\\)&', filter_str))
347de493
PH
4828
4829
4830def match_filter_func(filter_str):
8f18aca8 4831 def _match_func(info_dict, *args, **kwargs):
4832 if match_str(filter_str, info_dict, *args, **kwargs):
347de493
PH
4833 return None
4834 else:
4835 video_title = info_dict.get('title', info_dict.get('id', 'video'))
4836 return '%s does not pass filter %s, skipping ..' % (video_title, filter_str)
4837 return _match_func
91410c9b
PH
4838
4839
bf6427d2
YCH
4840def parse_dfxp_time_expr(time_expr):
4841 if not time_expr:
d631d5f9 4842 return
bf6427d2
YCH
4843
4844 mobj = re.match(r'^(?P<time_offset>\d+(?:\.\d+)?)s?$', time_expr)
4845 if mobj:
4846 return float(mobj.group('time_offset'))
4847
db2fe38b 4848 mobj = re.match(r'^(\d+):(\d\d):(\d\d(?:(?:\.|:)\d+)?)$', time_expr)
bf6427d2 4849 if mobj:
db2fe38b 4850 return 3600 * int(mobj.group(1)) + 60 * int(mobj.group(2)) + float(mobj.group(3).replace(':', '.'))
bf6427d2
YCH
4851
4852
c1c924ab
YCH
4853def srt_subtitles_timecode(seconds):
4854 return '%02d:%02d:%02d,%03d' % (seconds / 3600, (seconds % 3600) / 60, seconds % 60, (seconds % 1) * 1000)
bf6427d2
YCH
4855
4856
4857def dfxp2srt(dfxp_data):
3869028f
YCH
4858 '''
4859 @param dfxp_data A bytes-like object containing DFXP data
4860 @returns A unicode object containing converted SRT data
4861 '''
5b995f71 4862 LEGACY_NAMESPACES = (
3869028f
YCH
4863 (b'http://www.w3.org/ns/ttml', [
4864 b'http://www.w3.org/2004/11/ttaf1',
4865 b'http://www.w3.org/2006/04/ttaf1',
4866 b'http://www.w3.org/2006/10/ttaf1',
5b995f71 4867 ]),
3869028f
YCH
4868 (b'http://www.w3.org/ns/ttml#styling', [
4869 b'http://www.w3.org/ns/ttml#style',
5b995f71
RA
4870 ]),
4871 )
4872
4873 SUPPORTED_STYLING = [
4874 'color',
4875 'fontFamily',
4876 'fontSize',
4877 'fontStyle',
4878 'fontWeight',
4879 'textDecoration'
4880 ]
4881
4e335771 4882 _x = functools.partial(xpath_with_ns, ns_map={
261f4730 4883 'xml': 'http://www.w3.org/XML/1998/namespace',
4e335771 4884 'ttml': 'http://www.w3.org/ns/ttml',
5b995f71 4885 'tts': 'http://www.w3.org/ns/ttml#styling',
4e335771 4886 })
bf6427d2 4887
5b995f71
RA
4888 styles = {}
4889 default_style = {}
4890
87de7069 4891 class TTMLPElementParser(object):
5b995f71
RA
4892 _out = ''
4893 _unclosed_elements = []
4894 _applied_styles = []
bf6427d2 4895
2b14cb56 4896 def start(self, tag, attrib):
5b995f71
RA
4897 if tag in (_x('ttml:br'), 'br'):
4898 self._out += '\n'
4899 else:
4900 unclosed_elements = []
4901 style = {}
4902 element_style_id = attrib.get('style')
4903 if default_style:
4904 style.update(default_style)
4905 if element_style_id:
4906 style.update(styles.get(element_style_id, {}))
4907 for prop in SUPPORTED_STYLING:
4908 prop_val = attrib.get(_x('tts:' + prop))
4909 if prop_val:
4910 style[prop] = prop_val
4911 if style:
4912 font = ''
4913 for k, v in sorted(style.items()):
4914 if self._applied_styles and self._applied_styles[-1].get(k) == v:
4915 continue
4916 if k == 'color':
4917 font += ' color="%s"' % v
4918 elif k == 'fontSize':
4919 font += ' size="%s"' % v
4920 elif k == 'fontFamily':
4921 font += ' face="%s"' % v
4922 elif k == 'fontWeight' and v == 'bold':
4923 self._out += '<b>'
4924 unclosed_elements.append('b')
4925 elif k == 'fontStyle' and v == 'italic':
4926 self._out += '<i>'
4927 unclosed_elements.append('i')
4928 elif k == 'textDecoration' and v == 'underline':
4929 self._out += '<u>'
4930 unclosed_elements.append('u')
4931 if font:
4932 self._out += '<font' + font + '>'
4933 unclosed_elements.append('font')
4934 applied_style = {}
4935 if self._applied_styles:
4936 applied_style.update(self._applied_styles[-1])
4937 applied_style.update(style)
4938 self._applied_styles.append(applied_style)
4939 self._unclosed_elements.append(unclosed_elements)
bf6427d2 4940
2b14cb56 4941 def end(self, tag):
5b995f71
RA
4942 if tag not in (_x('ttml:br'), 'br'):
4943 unclosed_elements = self._unclosed_elements.pop()
4944 for element in reversed(unclosed_elements):
4945 self._out += '</%s>' % element
4946 if unclosed_elements and self._applied_styles:
4947 self._applied_styles.pop()
bf6427d2 4948
2b14cb56 4949 def data(self, data):
5b995f71 4950 self._out += data
2b14cb56 4951
4952 def close(self):
5b995f71 4953 return self._out.strip()
2b14cb56 4954
4955 def parse_node(node):
4956 target = TTMLPElementParser()
4957 parser = xml.etree.ElementTree.XMLParser(target=target)
4958 parser.feed(xml.etree.ElementTree.tostring(node))
4959 return parser.close()
bf6427d2 4960
5b995f71
RA
4961 for k, v in LEGACY_NAMESPACES:
4962 for ns in v:
4963 dfxp_data = dfxp_data.replace(ns, k)
4964
3869028f 4965 dfxp = compat_etree_fromstring(dfxp_data)
bf6427d2 4966 out = []
5b995f71 4967 paras = dfxp.findall(_x('.//ttml:p')) or dfxp.findall('.//p')
1b0427e6
YCH
4968
4969 if not paras:
4970 raise ValueError('Invalid dfxp/TTML subtitle')
bf6427d2 4971
5b995f71
RA
4972 repeat = False
4973 while True:
4974 for style in dfxp.findall(_x('.//ttml:style')):
261f4730
RA
4975 style_id = style.get('id') or style.get(_x('xml:id'))
4976 if not style_id:
4977 continue
5b995f71
RA
4978 parent_style_id = style.get('style')
4979 if parent_style_id:
4980 if parent_style_id not in styles:
4981 repeat = True
4982 continue
4983 styles[style_id] = styles[parent_style_id].copy()
4984 for prop in SUPPORTED_STYLING:
4985 prop_val = style.get(_x('tts:' + prop))
4986 if prop_val:
4987 styles.setdefault(style_id, {})[prop] = prop_val
4988 if repeat:
4989 repeat = False
4990 else:
4991 break
4992
4993 for p in ('body', 'div'):
4994 ele = xpath_element(dfxp, [_x('.//ttml:' + p), './/' + p])
4995 if ele is None:
4996 continue
4997 style = styles.get(ele.get('style'))
4998 if not style:
4999 continue
5000 default_style.update(style)
5001
bf6427d2 5002 for para, index in zip(paras, itertools.count(1)):
d631d5f9 5003 begin_time = parse_dfxp_time_expr(para.attrib.get('begin'))
7dff0363 5004 end_time = parse_dfxp_time_expr(para.attrib.get('end'))
d631d5f9
YCH
5005 dur = parse_dfxp_time_expr(para.attrib.get('dur'))
5006 if begin_time is None:
5007 continue
7dff0363 5008 if not end_time:
d631d5f9
YCH
5009 if not dur:
5010 continue
5011 end_time = begin_time + dur
bf6427d2
YCH
5012 out.append('%d\n%s --> %s\n%s\n\n' % (
5013 index,
c1c924ab
YCH
5014 srt_subtitles_timecode(begin_time),
5015 srt_subtitles_timecode(end_time),
bf6427d2
YCH
5016 parse_node(para)))
5017
5018 return ''.join(out)
5019
5020
66e289ba
S
5021def cli_option(params, command_option, param):
5022 param = params.get(param)
98e698f1
RA
5023 if param:
5024 param = compat_str(param)
66e289ba
S
5025 return [command_option, param] if param is not None else []
5026
5027
5028def cli_bool_option(params, command_option, param, true_value='true', false_value='false', separator=None):
5029 param = params.get(param)
5b232f46
S
5030 if param is None:
5031 return []
66e289ba
S
5032 assert isinstance(param, bool)
5033 if separator:
5034 return [command_option + separator + (true_value if param else false_value)]
5035 return [command_option, true_value if param else false_value]
5036
5037
5038def cli_valueless_option(params, command_option, param, expected_value=True):
5039 param = params.get(param)
5040 return [command_option] if param == expected_value else []
5041
5042
e92caff5 5043def cli_configuration_args(argdict, keys, default=[], use_compat=True):
eab9b2bc 5044 if isinstance(argdict, (list, tuple)): # for backward compatibility
e92caff5 5045 if use_compat:
5b1ecbb3 5046 return argdict
5047 else:
5048 argdict = None
eab9b2bc 5049 if argdict is None:
5b1ecbb3 5050 return default
eab9b2bc 5051 assert isinstance(argdict, dict)
5052
e92caff5 5053 assert isinstance(keys, (list, tuple))
5054 for key_list in keys:
e92caff5 5055 arg_list = list(filter(
5056 lambda x: x is not None,
6606817a 5057 [argdict.get(key.lower()) for key in variadic(key_list)]))
e92caff5 5058 if arg_list:
5059 return [arg for args in arg_list for arg in args]
5060 return default
66e289ba 5061
6251555f 5062
330690a2 5063def _configuration_args(main_key, argdict, exe, keys=None, default=[], use_compat=True):
5064 main_key, exe = main_key.lower(), exe.lower()
5065 root_key = exe if main_key == exe else f'{main_key}+{exe}'
5066 keys = [f'{root_key}{k}' for k in (keys or [''])]
5067 if root_key in keys:
5068 if main_key != exe:
5069 keys.append((main_key, exe))
5070 keys.append('default')
5071 else:
5072 use_compat = False
5073 return cli_configuration_args(argdict, keys, default, use_compat)
5074
66e289ba 5075
39672624
YCH
5076class ISO639Utils(object):
5077 # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
5078 _lang_map = {
5079 'aa': 'aar',
5080 'ab': 'abk',
5081 'ae': 'ave',
5082 'af': 'afr',
5083 'ak': 'aka',
5084 'am': 'amh',
5085 'an': 'arg',
5086 'ar': 'ara',
5087 'as': 'asm',
5088 'av': 'ava',
5089 'ay': 'aym',
5090 'az': 'aze',
5091 'ba': 'bak',
5092 'be': 'bel',
5093 'bg': 'bul',
5094 'bh': 'bih',
5095 'bi': 'bis',
5096 'bm': 'bam',
5097 'bn': 'ben',
5098 'bo': 'bod',
5099 'br': 'bre',
5100 'bs': 'bos',
5101 'ca': 'cat',
5102 'ce': 'che',
5103 'ch': 'cha',
5104 'co': 'cos',
5105 'cr': 'cre',
5106 'cs': 'ces',
5107 'cu': 'chu',
5108 'cv': 'chv',
5109 'cy': 'cym',
5110 'da': 'dan',
5111 'de': 'deu',
5112 'dv': 'div',
5113 'dz': 'dzo',
5114 'ee': 'ewe',
5115 'el': 'ell',
5116 'en': 'eng',
5117 'eo': 'epo',
5118 'es': 'spa',
5119 'et': 'est',
5120 'eu': 'eus',
5121 'fa': 'fas',
5122 'ff': 'ful',
5123 'fi': 'fin',
5124 'fj': 'fij',
5125 'fo': 'fao',
5126 'fr': 'fra',
5127 'fy': 'fry',
5128 'ga': 'gle',
5129 'gd': 'gla',
5130 'gl': 'glg',
5131 'gn': 'grn',
5132 'gu': 'guj',
5133 'gv': 'glv',
5134 'ha': 'hau',
5135 'he': 'heb',
b7acc835 5136 'iw': 'heb', # Replaced by he in 1989 revision
39672624
YCH
5137 'hi': 'hin',
5138 'ho': 'hmo',
5139 'hr': 'hrv',
5140 'ht': 'hat',
5141 'hu': 'hun',
5142 'hy': 'hye',
5143 'hz': 'her',
5144 'ia': 'ina',
5145 'id': 'ind',
b7acc835 5146 'in': 'ind', # Replaced by id in 1989 revision
39672624
YCH
5147 'ie': 'ile',
5148 'ig': 'ibo',
5149 'ii': 'iii',
5150 'ik': 'ipk',
5151 'io': 'ido',
5152 'is': 'isl',
5153 'it': 'ita',
5154 'iu': 'iku',
5155 'ja': 'jpn',
5156 'jv': 'jav',
5157 'ka': 'kat',
5158 'kg': 'kon',
5159 'ki': 'kik',
5160 'kj': 'kua',
5161 'kk': 'kaz',
5162 'kl': 'kal',
5163 'km': 'khm',
5164 'kn': 'kan',
5165 'ko': 'kor',
5166 'kr': 'kau',
5167 'ks': 'kas',
5168 'ku': 'kur',
5169 'kv': 'kom',
5170 'kw': 'cor',
5171 'ky': 'kir',
5172 'la': 'lat',
5173 'lb': 'ltz',
5174 'lg': 'lug',
5175 'li': 'lim',
5176 'ln': 'lin',
5177 'lo': 'lao',
5178 'lt': 'lit',
5179 'lu': 'lub',
5180 'lv': 'lav',
5181 'mg': 'mlg',
5182 'mh': 'mah',
5183 'mi': 'mri',
5184 'mk': 'mkd',
5185 'ml': 'mal',
5186 'mn': 'mon',
5187 'mr': 'mar',
5188 'ms': 'msa',
5189 'mt': 'mlt',
5190 'my': 'mya',
5191 'na': 'nau',
5192 'nb': 'nob',
5193 'nd': 'nde',
5194 'ne': 'nep',
5195 'ng': 'ndo',
5196 'nl': 'nld',
5197 'nn': 'nno',
5198 'no': 'nor',
5199 'nr': 'nbl',
5200 'nv': 'nav',
5201 'ny': 'nya',
5202 'oc': 'oci',
5203 'oj': 'oji',
5204 'om': 'orm',
5205 'or': 'ori',
5206 'os': 'oss',
5207 'pa': 'pan',
5208 'pi': 'pli',
5209 'pl': 'pol',
5210 'ps': 'pus',
5211 'pt': 'por',
5212 'qu': 'que',
5213 'rm': 'roh',
5214 'rn': 'run',
5215 'ro': 'ron',
5216 'ru': 'rus',
5217 'rw': 'kin',
5218 'sa': 'san',
5219 'sc': 'srd',
5220 'sd': 'snd',
5221 'se': 'sme',
5222 'sg': 'sag',
5223 'si': 'sin',
5224 'sk': 'slk',
5225 'sl': 'slv',
5226 'sm': 'smo',
5227 'sn': 'sna',
5228 'so': 'som',
5229 'sq': 'sqi',
5230 'sr': 'srp',
5231 'ss': 'ssw',
5232 'st': 'sot',
5233 'su': 'sun',
5234 'sv': 'swe',
5235 'sw': 'swa',
5236 'ta': 'tam',
5237 'te': 'tel',
5238 'tg': 'tgk',
5239 'th': 'tha',
5240 'ti': 'tir',
5241 'tk': 'tuk',
5242 'tl': 'tgl',
5243 'tn': 'tsn',
5244 'to': 'ton',
5245 'tr': 'tur',
5246 'ts': 'tso',
5247 'tt': 'tat',
5248 'tw': 'twi',
5249 'ty': 'tah',
5250 'ug': 'uig',
5251 'uk': 'ukr',
5252 'ur': 'urd',
5253 'uz': 'uzb',
5254 've': 'ven',
5255 'vi': 'vie',
5256 'vo': 'vol',
5257 'wa': 'wln',
5258 'wo': 'wol',
5259 'xh': 'xho',
5260 'yi': 'yid',
e9a50fba 5261 'ji': 'yid', # Replaced by yi in 1989 revision
39672624
YCH
5262 'yo': 'yor',
5263 'za': 'zha',
5264 'zh': 'zho',
5265 'zu': 'zul',
5266 }
5267
5268 @classmethod
5269 def short2long(cls, code):
5270 """Convert language code from ISO 639-1 to ISO 639-2/T"""
5271 return cls._lang_map.get(code[:2])
5272
5273 @classmethod
5274 def long2short(cls, code):
5275 """Convert language code from ISO 639-2/T to ISO 639-1"""
5276 for short_name, long_name in cls._lang_map.items():
5277 if long_name == code:
5278 return short_name
5279
5280
4eb10f66
YCH
5281class ISO3166Utils(object):
5282 # From http://data.okfn.org/data/core/country-list
5283 _country_map = {
5284 'AF': 'Afghanistan',
5285 'AX': 'Åland Islands',
5286 'AL': 'Albania',
5287 'DZ': 'Algeria',
5288 'AS': 'American Samoa',
5289 'AD': 'Andorra',
5290 'AO': 'Angola',
5291 'AI': 'Anguilla',
5292 'AQ': 'Antarctica',
5293 'AG': 'Antigua and Barbuda',
5294 'AR': 'Argentina',
5295 'AM': 'Armenia',
5296 'AW': 'Aruba',
5297 'AU': 'Australia',
5298 'AT': 'Austria',
5299 'AZ': 'Azerbaijan',
5300 'BS': 'Bahamas',
5301 'BH': 'Bahrain',
5302 'BD': 'Bangladesh',
5303 'BB': 'Barbados',
5304 'BY': 'Belarus',
5305 'BE': 'Belgium',
5306 'BZ': 'Belize',
5307 'BJ': 'Benin',
5308 'BM': 'Bermuda',
5309 'BT': 'Bhutan',
5310 'BO': 'Bolivia, Plurinational State of',
5311 'BQ': 'Bonaire, Sint Eustatius and Saba',
5312 'BA': 'Bosnia and Herzegovina',
5313 'BW': 'Botswana',
5314 'BV': 'Bouvet Island',
5315 'BR': 'Brazil',
5316 'IO': 'British Indian Ocean Territory',
5317 'BN': 'Brunei Darussalam',
5318 'BG': 'Bulgaria',
5319 'BF': 'Burkina Faso',
5320 'BI': 'Burundi',
5321 'KH': 'Cambodia',
5322 'CM': 'Cameroon',
5323 'CA': 'Canada',
5324 'CV': 'Cape Verde',
5325 'KY': 'Cayman Islands',
5326 'CF': 'Central African Republic',
5327 'TD': 'Chad',
5328 'CL': 'Chile',
5329 'CN': 'China',
5330 'CX': 'Christmas Island',
5331 'CC': 'Cocos (Keeling) Islands',
5332 'CO': 'Colombia',
5333 'KM': 'Comoros',
5334 'CG': 'Congo',
5335 'CD': 'Congo, the Democratic Republic of the',
5336 'CK': 'Cook Islands',
5337 'CR': 'Costa Rica',
5338 'CI': 'Côte d\'Ivoire',
5339 'HR': 'Croatia',
5340 'CU': 'Cuba',
5341 'CW': 'Curaçao',
5342 'CY': 'Cyprus',
5343 'CZ': 'Czech Republic',
5344 'DK': 'Denmark',
5345 'DJ': 'Djibouti',
5346 'DM': 'Dominica',
5347 'DO': 'Dominican Republic',
5348 'EC': 'Ecuador',
5349 'EG': 'Egypt',
5350 'SV': 'El Salvador',
5351 'GQ': 'Equatorial Guinea',
5352 'ER': 'Eritrea',
5353 'EE': 'Estonia',
5354 'ET': 'Ethiopia',
5355 'FK': 'Falkland Islands (Malvinas)',
5356 'FO': 'Faroe Islands',
5357 'FJ': 'Fiji',
5358 'FI': 'Finland',
5359 'FR': 'France',
5360 'GF': 'French Guiana',
5361 'PF': 'French Polynesia',
5362 'TF': 'French Southern Territories',
5363 'GA': 'Gabon',
5364 'GM': 'Gambia',
5365 'GE': 'Georgia',
5366 'DE': 'Germany',
5367 'GH': 'Ghana',
5368 'GI': 'Gibraltar',
5369 'GR': 'Greece',
5370 'GL': 'Greenland',
5371 'GD': 'Grenada',
5372 'GP': 'Guadeloupe',
5373 'GU': 'Guam',
5374 'GT': 'Guatemala',
5375 'GG': 'Guernsey',
5376 'GN': 'Guinea',
5377 'GW': 'Guinea-Bissau',
5378 'GY': 'Guyana',
5379 'HT': 'Haiti',
5380 'HM': 'Heard Island and McDonald Islands',
5381 'VA': 'Holy See (Vatican City State)',
5382 'HN': 'Honduras',
5383 'HK': 'Hong Kong',
5384 'HU': 'Hungary',
5385 'IS': 'Iceland',
5386 'IN': 'India',
5387 'ID': 'Indonesia',
5388 'IR': 'Iran, Islamic Republic of',
5389 'IQ': 'Iraq',
5390 'IE': 'Ireland',
5391 'IM': 'Isle of Man',
5392 'IL': 'Israel',
5393 'IT': 'Italy',
5394 'JM': 'Jamaica',
5395 'JP': 'Japan',
5396 'JE': 'Jersey',
5397 'JO': 'Jordan',
5398 'KZ': 'Kazakhstan',
5399 'KE': 'Kenya',
5400 'KI': 'Kiribati',
5401 'KP': 'Korea, Democratic People\'s Republic of',
5402 'KR': 'Korea, Republic of',
5403 'KW': 'Kuwait',
5404 'KG': 'Kyrgyzstan',
5405 'LA': 'Lao People\'s Democratic Republic',
5406 'LV': 'Latvia',
5407 'LB': 'Lebanon',
5408 'LS': 'Lesotho',
5409 'LR': 'Liberia',
5410 'LY': 'Libya',
5411 'LI': 'Liechtenstein',
5412 'LT': 'Lithuania',
5413 'LU': 'Luxembourg',
5414 'MO': 'Macao',
5415 'MK': 'Macedonia, the Former Yugoslav Republic of',
5416 'MG': 'Madagascar',
5417 'MW': 'Malawi',
5418 'MY': 'Malaysia',
5419 'MV': 'Maldives',
5420 'ML': 'Mali',
5421 'MT': 'Malta',
5422 'MH': 'Marshall Islands',
5423 'MQ': 'Martinique',
5424 'MR': 'Mauritania',
5425 'MU': 'Mauritius',
5426 'YT': 'Mayotte',
5427 'MX': 'Mexico',
5428 'FM': 'Micronesia, Federated States of',
5429 'MD': 'Moldova, Republic of',
5430 'MC': 'Monaco',
5431 'MN': 'Mongolia',
5432 'ME': 'Montenegro',
5433 'MS': 'Montserrat',
5434 'MA': 'Morocco',
5435 'MZ': 'Mozambique',
5436 'MM': 'Myanmar',
5437 'NA': 'Namibia',
5438 'NR': 'Nauru',
5439 'NP': 'Nepal',
5440 'NL': 'Netherlands',
5441 'NC': 'New Caledonia',
5442 'NZ': 'New Zealand',
5443 'NI': 'Nicaragua',
5444 'NE': 'Niger',
5445 'NG': 'Nigeria',
5446 'NU': 'Niue',
5447 'NF': 'Norfolk Island',
5448 'MP': 'Northern Mariana Islands',
5449 'NO': 'Norway',
5450 'OM': 'Oman',
5451 'PK': 'Pakistan',
5452 'PW': 'Palau',
5453 'PS': 'Palestine, State of',
5454 'PA': 'Panama',
5455 'PG': 'Papua New Guinea',
5456 'PY': 'Paraguay',
5457 'PE': 'Peru',
5458 'PH': 'Philippines',
5459 'PN': 'Pitcairn',
5460 'PL': 'Poland',
5461 'PT': 'Portugal',
5462 'PR': 'Puerto Rico',
5463 'QA': 'Qatar',
5464 'RE': 'Réunion',
5465 'RO': 'Romania',
5466 'RU': 'Russian Federation',
5467 'RW': 'Rwanda',
5468 'BL': 'Saint Barthélemy',
5469 'SH': 'Saint Helena, Ascension and Tristan da Cunha',
5470 'KN': 'Saint Kitts and Nevis',
5471 'LC': 'Saint Lucia',
5472 'MF': 'Saint Martin (French part)',
5473 'PM': 'Saint Pierre and Miquelon',
5474 'VC': 'Saint Vincent and the Grenadines',
5475 'WS': 'Samoa',
5476 'SM': 'San Marino',
5477 'ST': 'Sao Tome and Principe',
5478 'SA': 'Saudi Arabia',
5479 'SN': 'Senegal',
5480 'RS': 'Serbia',
5481 'SC': 'Seychelles',
5482 'SL': 'Sierra Leone',
5483 'SG': 'Singapore',
5484 'SX': 'Sint Maarten (Dutch part)',
5485 'SK': 'Slovakia',
5486 'SI': 'Slovenia',
5487 'SB': 'Solomon Islands',
5488 'SO': 'Somalia',
5489 'ZA': 'South Africa',
5490 'GS': 'South Georgia and the South Sandwich Islands',
5491 'SS': 'South Sudan',
5492 'ES': 'Spain',
5493 'LK': 'Sri Lanka',
5494 'SD': 'Sudan',
5495 'SR': 'Suriname',
5496 'SJ': 'Svalbard and Jan Mayen',
5497 'SZ': 'Swaziland',
5498 'SE': 'Sweden',
5499 'CH': 'Switzerland',
5500 'SY': 'Syrian Arab Republic',
5501 'TW': 'Taiwan, Province of China',
5502 'TJ': 'Tajikistan',
5503 'TZ': 'Tanzania, United Republic of',
5504 'TH': 'Thailand',
5505 'TL': 'Timor-Leste',
5506 'TG': 'Togo',
5507 'TK': 'Tokelau',
5508 'TO': 'Tonga',
5509 'TT': 'Trinidad and Tobago',
5510 'TN': 'Tunisia',
5511 'TR': 'Turkey',
5512 'TM': 'Turkmenistan',
5513 'TC': 'Turks and Caicos Islands',
5514 'TV': 'Tuvalu',
5515 'UG': 'Uganda',
5516 'UA': 'Ukraine',
5517 'AE': 'United Arab Emirates',
5518 'GB': 'United Kingdom',
5519 'US': 'United States',
5520 'UM': 'United States Minor Outlying Islands',
5521 'UY': 'Uruguay',
5522 'UZ': 'Uzbekistan',
5523 'VU': 'Vanuatu',
5524 'VE': 'Venezuela, Bolivarian Republic of',
5525 'VN': 'Viet Nam',
5526 'VG': 'Virgin Islands, British',
5527 'VI': 'Virgin Islands, U.S.',
5528 'WF': 'Wallis and Futuna',
5529 'EH': 'Western Sahara',
5530 'YE': 'Yemen',
5531 'ZM': 'Zambia',
5532 'ZW': 'Zimbabwe',
5533 }
5534
5535 @classmethod
5536 def short2full(cls, code):
5537 """Convert an ISO 3166-2 country code to the corresponding full name"""
5538 return cls._country_map.get(code.upper())
5539
5540
773f291d
S
5541class GeoUtils(object):
5542 # Major IPv4 address blocks per country
5543 _country_ip_map = {
53896ca5 5544 'AD': '46.172.224.0/19',
773f291d
S
5545 'AE': '94.200.0.0/13',
5546 'AF': '149.54.0.0/17',
5547 'AG': '209.59.64.0/18',
5548 'AI': '204.14.248.0/21',
5549 'AL': '46.99.0.0/16',
5550 'AM': '46.70.0.0/15',
5551 'AO': '105.168.0.0/13',
53896ca5
S
5552 'AP': '182.50.184.0/21',
5553 'AQ': '23.154.160.0/24',
773f291d
S
5554 'AR': '181.0.0.0/12',
5555 'AS': '202.70.112.0/20',
53896ca5 5556 'AT': '77.116.0.0/14',
773f291d
S
5557 'AU': '1.128.0.0/11',
5558 'AW': '181.41.0.0/18',
53896ca5
S
5559 'AX': '185.217.4.0/22',
5560 'AZ': '5.197.0.0/16',
773f291d
S
5561 'BA': '31.176.128.0/17',
5562 'BB': '65.48.128.0/17',
5563 'BD': '114.130.0.0/16',
5564 'BE': '57.0.0.0/8',
53896ca5 5565 'BF': '102.178.0.0/15',
773f291d
S
5566 'BG': '95.42.0.0/15',
5567 'BH': '37.131.0.0/17',
5568 'BI': '154.117.192.0/18',
5569 'BJ': '137.255.0.0/16',
53896ca5 5570 'BL': '185.212.72.0/23',
773f291d
S
5571 'BM': '196.12.64.0/18',
5572 'BN': '156.31.0.0/16',
5573 'BO': '161.56.0.0/16',
5574 'BQ': '161.0.80.0/20',
53896ca5 5575 'BR': '191.128.0.0/12',
773f291d
S
5576 'BS': '24.51.64.0/18',
5577 'BT': '119.2.96.0/19',
5578 'BW': '168.167.0.0/16',
5579 'BY': '178.120.0.0/13',
5580 'BZ': '179.42.192.0/18',
5581 'CA': '99.224.0.0/11',
5582 'CD': '41.243.0.0/16',
53896ca5
S
5583 'CF': '197.242.176.0/21',
5584 'CG': '160.113.0.0/16',
773f291d 5585 'CH': '85.0.0.0/13',
53896ca5 5586 'CI': '102.136.0.0/14',
773f291d
S
5587 'CK': '202.65.32.0/19',
5588 'CL': '152.172.0.0/14',
53896ca5 5589 'CM': '102.244.0.0/14',
773f291d
S
5590 'CN': '36.128.0.0/10',
5591 'CO': '181.240.0.0/12',
5592 'CR': '201.192.0.0/12',
5593 'CU': '152.206.0.0/15',
5594 'CV': '165.90.96.0/19',
5595 'CW': '190.88.128.0/17',
53896ca5 5596 'CY': '31.153.0.0/16',
773f291d
S
5597 'CZ': '88.100.0.0/14',
5598 'DE': '53.0.0.0/8',
5599 'DJ': '197.241.0.0/17',
5600 'DK': '87.48.0.0/12',
5601 'DM': '192.243.48.0/20',
5602 'DO': '152.166.0.0/15',
5603 'DZ': '41.96.0.0/12',
5604 'EC': '186.68.0.0/15',
5605 'EE': '90.190.0.0/15',
5606 'EG': '156.160.0.0/11',
5607 'ER': '196.200.96.0/20',
5608 'ES': '88.0.0.0/11',
5609 'ET': '196.188.0.0/14',
5610 'EU': '2.16.0.0/13',
5611 'FI': '91.152.0.0/13',
5612 'FJ': '144.120.0.0/16',
53896ca5 5613 'FK': '80.73.208.0/21',
773f291d
S
5614 'FM': '119.252.112.0/20',
5615 'FO': '88.85.32.0/19',
5616 'FR': '90.0.0.0/9',
5617 'GA': '41.158.0.0/15',
5618 'GB': '25.0.0.0/8',
5619 'GD': '74.122.88.0/21',
5620 'GE': '31.146.0.0/16',
5621 'GF': '161.22.64.0/18',
5622 'GG': '62.68.160.0/19',
53896ca5
S
5623 'GH': '154.160.0.0/12',
5624 'GI': '95.164.0.0/16',
773f291d
S
5625 'GL': '88.83.0.0/19',
5626 'GM': '160.182.0.0/15',
5627 'GN': '197.149.192.0/18',
5628 'GP': '104.250.0.0/19',
5629 'GQ': '105.235.224.0/20',
5630 'GR': '94.64.0.0/13',
5631 'GT': '168.234.0.0/16',
5632 'GU': '168.123.0.0/16',
5633 'GW': '197.214.80.0/20',
5634 'GY': '181.41.64.0/18',
5635 'HK': '113.252.0.0/14',
5636 'HN': '181.210.0.0/16',
5637 'HR': '93.136.0.0/13',
5638 'HT': '148.102.128.0/17',
5639 'HU': '84.0.0.0/14',
5640 'ID': '39.192.0.0/10',
5641 'IE': '87.32.0.0/12',
5642 'IL': '79.176.0.0/13',
5643 'IM': '5.62.80.0/20',
5644 'IN': '117.192.0.0/10',
5645 'IO': '203.83.48.0/21',
5646 'IQ': '37.236.0.0/14',
5647 'IR': '2.176.0.0/12',
5648 'IS': '82.221.0.0/16',
5649 'IT': '79.0.0.0/10',
5650 'JE': '87.244.64.0/18',
5651 'JM': '72.27.0.0/17',
5652 'JO': '176.29.0.0/16',
53896ca5 5653 'JP': '133.0.0.0/8',
773f291d
S
5654 'KE': '105.48.0.0/12',
5655 'KG': '158.181.128.0/17',
5656 'KH': '36.37.128.0/17',
5657 'KI': '103.25.140.0/22',
5658 'KM': '197.255.224.0/20',
53896ca5 5659 'KN': '198.167.192.0/19',
773f291d
S
5660 'KP': '175.45.176.0/22',
5661 'KR': '175.192.0.0/10',
5662 'KW': '37.36.0.0/14',
5663 'KY': '64.96.0.0/15',
5664 'KZ': '2.72.0.0/13',
5665 'LA': '115.84.64.0/18',
5666 'LB': '178.135.0.0/16',
53896ca5 5667 'LC': '24.92.144.0/20',
773f291d
S
5668 'LI': '82.117.0.0/19',
5669 'LK': '112.134.0.0/15',
53896ca5 5670 'LR': '102.183.0.0/16',
773f291d
S
5671 'LS': '129.232.0.0/17',
5672 'LT': '78.56.0.0/13',
5673 'LU': '188.42.0.0/16',
5674 'LV': '46.109.0.0/16',
5675 'LY': '41.252.0.0/14',
5676 'MA': '105.128.0.0/11',
5677 'MC': '88.209.64.0/18',
5678 'MD': '37.246.0.0/16',
5679 'ME': '178.175.0.0/17',
5680 'MF': '74.112.232.0/21',
5681 'MG': '154.126.0.0/17',
5682 'MH': '117.103.88.0/21',
5683 'MK': '77.28.0.0/15',
5684 'ML': '154.118.128.0/18',
5685 'MM': '37.111.0.0/17',
5686 'MN': '49.0.128.0/17',
5687 'MO': '60.246.0.0/16',
5688 'MP': '202.88.64.0/20',
5689 'MQ': '109.203.224.0/19',
5690 'MR': '41.188.64.0/18',
5691 'MS': '208.90.112.0/22',
5692 'MT': '46.11.0.0/16',
5693 'MU': '105.16.0.0/12',
5694 'MV': '27.114.128.0/18',
53896ca5 5695 'MW': '102.70.0.0/15',
773f291d
S
5696 'MX': '187.192.0.0/11',
5697 'MY': '175.136.0.0/13',
5698 'MZ': '197.218.0.0/15',
5699 'NA': '41.182.0.0/16',
5700 'NC': '101.101.0.0/18',
5701 'NE': '197.214.0.0/18',
5702 'NF': '203.17.240.0/22',
5703 'NG': '105.112.0.0/12',
5704 'NI': '186.76.0.0/15',
5705 'NL': '145.96.0.0/11',
5706 'NO': '84.208.0.0/13',
5707 'NP': '36.252.0.0/15',
5708 'NR': '203.98.224.0/19',
5709 'NU': '49.156.48.0/22',
5710 'NZ': '49.224.0.0/14',
5711 'OM': '5.36.0.0/15',
5712 'PA': '186.72.0.0/15',
5713 'PE': '186.160.0.0/14',
5714 'PF': '123.50.64.0/18',
5715 'PG': '124.240.192.0/19',
5716 'PH': '49.144.0.0/13',
5717 'PK': '39.32.0.0/11',
5718 'PL': '83.0.0.0/11',
5719 'PM': '70.36.0.0/20',
5720 'PR': '66.50.0.0/16',
5721 'PS': '188.161.0.0/16',
5722 'PT': '85.240.0.0/13',
5723 'PW': '202.124.224.0/20',
5724 'PY': '181.120.0.0/14',
5725 'QA': '37.210.0.0/15',
53896ca5 5726 'RE': '102.35.0.0/16',
773f291d 5727 'RO': '79.112.0.0/13',
53896ca5 5728 'RS': '93.86.0.0/15',
773f291d 5729 'RU': '5.136.0.0/13',
53896ca5 5730 'RW': '41.186.0.0/16',
773f291d
S
5731 'SA': '188.48.0.0/13',
5732 'SB': '202.1.160.0/19',
5733 'SC': '154.192.0.0/11',
53896ca5 5734 'SD': '102.120.0.0/13',
773f291d 5735 'SE': '78.64.0.0/12',
53896ca5 5736 'SG': '8.128.0.0/10',
773f291d
S
5737 'SI': '188.196.0.0/14',
5738 'SK': '78.98.0.0/15',
53896ca5 5739 'SL': '102.143.0.0/17',
773f291d
S
5740 'SM': '89.186.32.0/19',
5741 'SN': '41.82.0.0/15',
53896ca5 5742 'SO': '154.115.192.0/18',
773f291d
S
5743 'SR': '186.179.128.0/17',
5744 'SS': '105.235.208.0/21',
5745 'ST': '197.159.160.0/19',
5746 'SV': '168.243.0.0/16',
5747 'SX': '190.102.0.0/20',
5748 'SY': '5.0.0.0/16',
5749 'SZ': '41.84.224.0/19',
5750 'TC': '65.255.48.0/20',
5751 'TD': '154.68.128.0/19',
5752 'TG': '196.168.0.0/14',
5753 'TH': '171.96.0.0/13',
5754 'TJ': '85.9.128.0/18',
5755 'TK': '27.96.24.0/21',
5756 'TL': '180.189.160.0/20',
5757 'TM': '95.85.96.0/19',
5758 'TN': '197.0.0.0/11',
5759 'TO': '175.176.144.0/21',
5760 'TR': '78.160.0.0/11',
5761 'TT': '186.44.0.0/15',
5762 'TV': '202.2.96.0/19',
5763 'TW': '120.96.0.0/11',
5764 'TZ': '156.156.0.0/14',
53896ca5
S
5765 'UA': '37.52.0.0/14',
5766 'UG': '102.80.0.0/13',
5767 'US': '6.0.0.0/8',
773f291d 5768 'UY': '167.56.0.0/13',
53896ca5 5769 'UZ': '84.54.64.0/18',
773f291d 5770 'VA': '212.77.0.0/19',
53896ca5 5771 'VC': '207.191.240.0/21',
773f291d 5772 'VE': '186.88.0.0/13',
53896ca5 5773 'VG': '66.81.192.0/20',
773f291d
S
5774 'VI': '146.226.0.0/16',
5775 'VN': '14.160.0.0/11',
5776 'VU': '202.80.32.0/20',
5777 'WF': '117.20.32.0/21',
5778 'WS': '202.4.32.0/19',
5779 'YE': '134.35.0.0/16',
5780 'YT': '41.242.116.0/22',
5781 'ZA': '41.0.0.0/11',
53896ca5
S
5782 'ZM': '102.144.0.0/13',
5783 'ZW': '102.177.192.0/18',
773f291d
S
5784 }
5785
5786 @classmethod
5f95927a
S
5787 def random_ipv4(cls, code_or_block):
5788 if len(code_or_block) == 2:
5789 block = cls._country_ip_map.get(code_or_block.upper())
5790 if not block:
5791 return None
5792 else:
5793 block = code_or_block
773f291d
S
5794 addr, preflen = block.split('/')
5795 addr_min = compat_struct_unpack('!L', socket.inet_aton(addr))[0]
5796 addr_max = addr_min | (0xffffffff >> int(preflen))
18a0defa 5797 return compat_str(socket.inet_ntoa(
4248dad9 5798 compat_struct_pack('!L', random.randint(addr_min, addr_max))))
773f291d
S
5799
5800
91410c9b 5801class PerRequestProxyHandler(compat_urllib_request.ProxyHandler):
2461f79d
PH
5802 def __init__(self, proxies=None):
5803 # Set default handlers
5804 for type in ('http', 'https'):
5805 setattr(self, '%s_open' % type,
5806 lambda r, proxy='__noproxy__', type=type, meth=self.proxy_open:
5807 meth(r, proxy, type))
38e87f6c 5808 compat_urllib_request.ProxyHandler.__init__(self, proxies)
2461f79d 5809
91410c9b 5810 def proxy_open(self, req, proxy, type):
2461f79d 5811 req_proxy = req.headers.get('Ytdl-request-proxy')
91410c9b
PH
5812 if req_proxy is not None:
5813 proxy = req_proxy
2461f79d
PH
5814 del req.headers['Ytdl-request-proxy']
5815
5816 if proxy == '__noproxy__':
5817 return None # No Proxy
51fb4995 5818 if compat_urlparse.urlparse(proxy).scheme.lower() in ('socks', 'socks4', 'socks4a', 'socks5'):
71aff188 5819 req.add_header('Ytdl-socks-proxy', proxy)
7a5c1cfe 5820 # yt-dlp's http/https handlers do wrapping the socket with socks
71aff188 5821 return None
91410c9b
PH
5822 return compat_urllib_request.ProxyHandler.proxy_open(
5823 self, req, proxy, type)
5bc880b9
YCH
5824
5825
0a5445dd
YCH
5826# Both long_to_bytes and bytes_to_long are adapted from PyCrypto, which is
5827# released into Public Domain
5828# https://github.com/dlitz/pycrypto/blob/master/lib/Crypto/Util/number.py#L387
5829
5830def long_to_bytes(n, blocksize=0):
5831 """long_to_bytes(n:long, blocksize:int) : string
5832 Convert a long integer to a byte string.
5833
5834 If optional blocksize is given and greater than zero, pad the front of the
5835 byte string with binary zeros so that the length is a multiple of
5836 blocksize.
5837 """
5838 # after much testing, this algorithm was deemed to be the fastest
5839 s = b''
5840 n = int(n)
5841 while n > 0:
5842 s = compat_struct_pack('>I', n & 0xffffffff) + s
5843 n = n >> 32
5844 # strip off leading zeros
5845 for i in range(len(s)):
5846 if s[i] != b'\000'[0]:
5847 break
5848 else:
5849 # only happens when n == 0
5850 s = b'\000'
5851 i = 0
5852 s = s[i:]
5853 # add back some pad bytes. this could be done more efficiently w.r.t. the
5854 # de-padding being done above, but sigh...
5855 if blocksize > 0 and len(s) % blocksize:
5856 s = (blocksize - len(s) % blocksize) * b'\000' + s
5857 return s
5858
5859
5860def bytes_to_long(s):
5861 """bytes_to_long(string) : long
5862 Convert a byte string to a long integer.
5863
5864 This is (essentially) the inverse of long_to_bytes().
5865 """
5866 acc = 0
5867 length = len(s)
5868 if length % 4:
5869 extra = (4 - length % 4)
5870 s = b'\000' * extra + s
5871 length = length + extra
5872 for i in range(0, length, 4):
5873 acc = (acc << 32) + compat_struct_unpack('>I', s[i:i + 4])[0]
5874 return acc
5875
5876
5bc880b9
YCH
5877def ohdave_rsa_encrypt(data, exponent, modulus):
5878 '''
5879 Implement OHDave's RSA algorithm. See http://www.ohdave.com/rsa/
5880
5881 Input:
5882 data: data to encrypt, bytes-like object
5883 exponent, modulus: parameter e and N of RSA algorithm, both integer
5884 Output: hex string of encrypted data
5885
5886 Limitation: supports one block encryption only
5887 '''
5888
5889 payload = int(binascii.hexlify(data[::-1]), 16)
5890 encrypted = pow(payload, exponent, modulus)
5891 return '%x' % encrypted
81bdc8fd
YCH
5892
5893
f48409c7
YCH
5894def pkcs1pad(data, length):
5895 """
5896 Padding input data with PKCS#1 scheme
5897
5898 @param {int[]} data input data
5899 @param {int} length target length
5900 @returns {int[]} padded data
5901 """
5902 if len(data) > length - 11:
5903 raise ValueError('Input data too long for PKCS#1 padding')
5904
5905 pseudo_random = [random.randint(0, 254) for _ in range(length - len(data) - 3)]
5906 return [0, 2] + pseudo_random + [0] + data
5907
5908
5eb6bdce 5909def encode_base_n(num, n, table=None):
59f898b7 5910 FULL_TABLE = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
59f898b7
YCH
5911 if not table:
5912 table = FULL_TABLE[:n]
5913
5eb6bdce
YCH
5914 if n > len(table):
5915 raise ValueError('base %d exceeds table length %d' % (n, len(table)))
5916
5917 if num == 0:
5918 return table[0]
5919
81bdc8fd
YCH
5920 ret = ''
5921 while num:
5922 ret = table[num % n] + ret
5923 num = num // n
5924 return ret
f52354a8
YCH
5925
5926
5927def decode_packed_codes(code):
06b3fe29 5928 mobj = re.search(PACKED_CODES_RE, code)
a0566bbf 5929 obfuscated_code, base, count, symbols = mobj.groups()
f52354a8
YCH
5930 base = int(base)
5931 count = int(count)
5932 symbols = symbols.split('|')
5933 symbol_table = {}
5934
5935 while count:
5936 count -= 1
5eb6bdce 5937 base_n_count = encode_base_n(count, base)
f52354a8
YCH
5938 symbol_table[base_n_count] = symbols[count] or base_n_count
5939
5940 return re.sub(
5941 r'\b(\w+)\b', lambda mobj: symbol_table[mobj.group(0)],
a0566bbf 5942 obfuscated_code)
e154c651 5943
5944
1ced2221
S
5945def caesar(s, alphabet, shift):
5946 if shift == 0:
5947 return s
5948 l = len(alphabet)
5949 return ''.join(
5950 alphabet[(alphabet.index(c) + shift) % l] if c in alphabet else c
5951 for c in s)
5952
5953
5954def rot47(s):
5955 return caesar(s, r'''!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~''', 47)
5956
5957
e154c651 5958def parse_m3u8_attributes(attrib):
5959 info = {}
5960 for (key, val) in re.findall(r'(?P<key>[A-Z0-9-]+)=(?P<val>"[^"]+"|[^",]+)(?:,|$)', attrib):
5961 if val.startswith('"'):
5962 val = val[1:-1]
5963 info[key] = val
5964 return info
1143535d
YCH
5965
5966
5967def urshift(val, n):
5968 return val >> n if val >= 0 else (val + 0x100000000) >> n
d3f8e038
YCH
5969
5970
5971# Based on png2str() written by @gdkchan and improved by @yokrysty
067aa17e 5972# Originally posted at https://github.com/ytdl-org/youtube-dl/issues/9706
d3f8e038
YCH
5973def decode_png(png_data):
5974 # Reference: https://www.w3.org/TR/PNG/
5975 header = png_data[8:]
5976
5977 if png_data[:8] != b'\x89PNG\x0d\x0a\x1a\x0a' or header[4:8] != b'IHDR':
5978 raise IOError('Not a valid PNG file.')
5979
5980 int_map = {1: '>B', 2: '>H', 4: '>I'}
5981 unpack_integer = lambda x: compat_struct_unpack(int_map[len(x)], x)[0]
5982
5983 chunks = []
5984
5985 while header:
5986 length = unpack_integer(header[:4])
5987 header = header[4:]
5988
5989 chunk_type = header[:4]
5990 header = header[4:]
5991
5992 chunk_data = header[:length]
5993 header = header[length:]
5994
5995 header = header[4:] # Skip CRC
5996
5997 chunks.append({
5998 'type': chunk_type,
5999 'length': length,
6000 'data': chunk_data
6001 })
6002
6003 ihdr = chunks[0]['data']
6004
6005 width = unpack_integer(ihdr[:4])
6006 height = unpack_integer(ihdr[4:8])
6007
6008 idat = b''
6009
6010 for chunk in chunks:
6011 if chunk['type'] == b'IDAT':
6012 idat += chunk['data']
6013
6014 if not idat:
6015 raise IOError('Unable to read PNG data.')
6016
6017 decompressed_data = bytearray(zlib.decompress(idat))
6018
6019 stride = width * 3
6020 pixels = []
6021
6022 def _get_pixel(idx):
6023 x = idx % stride
6024 y = idx // stride
6025 return pixels[y][x]
6026
6027 for y in range(height):
6028 basePos = y * (1 + stride)
6029 filter_type = decompressed_data[basePos]
6030
6031 current_row = []
6032
6033 pixels.append(current_row)
6034
6035 for x in range(stride):
6036 color = decompressed_data[1 + basePos + x]
6037 basex = y * stride + x
6038 left = 0
6039 up = 0
6040
6041 if x > 2:
6042 left = _get_pixel(basex - 3)
6043 if y > 0:
6044 up = _get_pixel(basex - stride)
6045
6046 if filter_type == 1: # Sub
6047 color = (color + left) & 0xff
6048 elif filter_type == 2: # Up
6049 color = (color + up) & 0xff
6050 elif filter_type == 3: # Average
6051 color = (color + ((left + up) >> 1)) & 0xff
6052 elif filter_type == 4: # Paeth
6053 a = left
6054 b = up
6055 c = 0
6056
6057 if x > 2 and y > 0:
6058 c = _get_pixel(basex - stride - 3)
6059
6060 p = a + b - c
6061
6062 pa = abs(p - a)
6063 pb = abs(p - b)
6064 pc = abs(p - c)
6065
6066 if pa <= pb and pa <= pc:
6067 color = (color + a) & 0xff
6068 elif pb <= pc:
6069 color = (color + b) & 0xff
6070 else:
6071 color = (color + c) & 0xff
6072
6073 current_row.append(color)
6074
6075 return width, height, pixels
efa97bdc
YCH
6076
6077
6078def write_xattr(path, key, value):
6079 # This mess below finds the best xattr tool for the job
6080 try:
6081 # try the pyxattr module...
6082 import xattr
6083
53a7e3d2
YCH
6084 if hasattr(xattr, 'set'): # pyxattr
6085 # Unicode arguments are not supported in python-pyxattr until
6086 # version 0.5.0
067aa17e 6087 # See https://github.com/ytdl-org/youtube-dl/issues/5498
53a7e3d2
YCH
6088 pyxattr_required_version = '0.5.0'
6089 if version_tuple(xattr.__version__) < version_tuple(pyxattr_required_version):
6090 # TODO: fallback to CLI tools
6091 raise XAttrUnavailableError(
6092 'python-pyxattr is detected but is too old. '
7a5c1cfe 6093 'yt-dlp requires %s or above while your version is %s. '
53a7e3d2
YCH
6094 'Falling back to other xattr implementations' % (
6095 pyxattr_required_version, xattr.__version__))
6096
6097 setxattr = xattr.set
6098 else: # xattr
6099 setxattr = xattr.setxattr
efa97bdc
YCH
6100
6101 try:
53a7e3d2 6102 setxattr(path, key, value)
efa97bdc
YCH
6103 except EnvironmentError as e:
6104 raise XAttrMetadataError(e.errno, e.strerror)
6105
6106 except ImportError:
6107 if compat_os_name == 'nt':
6108 # Write xattrs to NTFS Alternate Data Streams:
6109 # http://en.wikipedia.org/wiki/NTFS#Alternate_data_streams_.28ADS.29
6110 assert ':' not in key
6111 assert os.path.exists(path)
6112
6113 ads_fn = path + ':' + key
6114 try:
6115 with open(ads_fn, 'wb') as f:
6116 f.write(value)
6117 except EnvironmentError as e:
6118 raise XAttrMetadataError(e.errno, e.strerror)
6119 else:
6120 user_has_setfattr = check_executable('setfattr', ['--version'])
6121 user_has_xattr = check_executable('xattr', ['-h'])
6122
6123 if user_has_setfattr or user_has_xattr:
6124
6125 value = value.decode('utf-8')
6126 if user_has_setfattr:
6127 executable = 'setfattr'
6128 opts = ['-n', key, '-v', value]
6129 elif user_has_xattr:
6130 executable = 'xattr'
6131 opts = ['-w', key, value]
6132
3089bc74
S
6133 cmd = ([encodeFilename(executable, True)]
6134 + [encodeArgument(o) for o in opts]
6135 + [encodeFilename(path, True)])
efa97bdc
YCH
6136
6137 try:
6138 p = subprocess.Popen(
6139 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
6140 except EnvironmentError as e:
6141 raise XAttrMetadataError(e.errno, e.strerror)
f5b1bca9 6142 stdout, stderr = process_communicate_or_kill(p)
efa97bdc
YCH
6143 stderr = stderr.decode('utf-8', 'replace')
6144 if p.returncode != 0:
6145 raise XAttrMetadataError(p.returncode, stderr)
6146
6147 else:
6148 # On Unix, and can't find pyxattr, setfattr, or xattr.
6149 if sys.platform.startswith('linux'):
6150 raise XAttrUnavailableError(
6151 "Couldn't find a tool to set the xattrs. "
6152 "Install either the python 'pyxattr' or 'xattr' "
6153 "modules, or the GNU 'attr' package "
6154 "(which contains the 'setfattr' tool).")
6155 else:
6156 raise XAttrUnavailableError(
6157 "Couldn't find a tool to set the xattrs. "
6158 "Install either the python 'xattr' module, "
6159 "or the 'xattr' binary.")
0c265486
YCH
6160
6161
6162def random_birthday(year_field, month_field, day_field):
aa374bc7
AS
6163 start_date = datetime.date(1950, 1, 1)
6164 end_date = datetime.date(1995, 12, 31)
6165 offset = random.randint(0, (end_date - start_date).days)
6166 random_date = start_date + datetime.timedelta(offset)
0c265486 6167 return {
aa374bc7
AS
6168 year_field: str(random_date.year),
6169 month_field: str(random_date.month),
6170 day_field: str(random_date.day),
0c265486 6171 }
732044af 6172
c76eb41b 6173
732044af 6174# Templates for internet shortcut files, which are plain text files.
6175DOT_URL_LINK_TEMPLATE = '''
6176[InternetShortcut]
6177URL=%(url)s
6178'''.lstrip()
6179
6180DOT_WEBLOC_LINK_TEMPLATE = '''
6181<?xml version="1.0" encoding="UTF-8"?>
6182<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
6183<plist version="1.0">
6184<dict>
6185\t<key>URL</key>
6186\t<string>%(url)s</string>
6187</dict>
6188</plist>
6189'''.lstrip()
6190
6191DOT_DESKTOP_LINK_TEMPLATE = '''
6192[Desktop Entry]
6193Encoding=UTF-8
6194Name=%(filename)s
6195Type=Link
6196URL=%(url)s
6197Icon=text-html
6198'''.lstrip()
6199
6200
6201def iri_to_uri(iri):
6202 """
6203 Converts an IRI (Internationalized Resource Identifier, allowing Unicode characters) to a URI (Uniform Resource Identifier, ASCII-only).
6204
6205 The function doesn't add an additional layer of escaping; e.g., it doesn't escape `%3C` as `%253C`. Instead, it percent-escapes characters with an underlying UTF-8 encoding *besides* those already escaped, leaving the URI intact.
6206 """
6207
6208 iri_parts = compat_urllib_parse_urlparse(iri)
6209
6210 if '[' in iri_parts.netloc:
6211 raise ValueError('IPv6 URIs are not, yet, supported.')
6212 # Querying `.netloc`, when there's only one bracket, also raises a ValueError.
6213
6214 # The `safe` argument values, that the following code uses, contain the characters that should not be percent-encoded. Everything else but letters, digits and '_.-' will be percent-encoded with an underlying UTF-8 encoding. Everything already percent-encoded will be left as is.
6215
6216 net_location = ''
6217 if iri_parts.username:
6218 net_location += compat_urllib_parse_quote(iri_parts.username, safe=r"!$%&'()*+,~")
6219 if iri_parts.password is not None:
6220 net_location += ':' + compat_urllib_parse_quote(iri_parts.password, safe=r"!$%&'()*+,~")
6221 net_location += '@'
6222
6223 net_location += iri_parts.hostname.encode('idna').decode('utf-8') # Punycode for Unicode hostnames.
6224 # The 'idna' encoding produces ASCII text.
6225 if iri_parts.port is not None and iri_parts.port != 80:
6226 net_location += ':' + str(iri_parts.port)
6227
6228 return compat_urllib_parse_urlunparse(
6229 (iri_parts.scheme,
6230 net_location,
6231
6232 compat_urllib_parse_quote_plus(iri_parts.path, safe=r"!$%&'()*+,/:;=@|~"),
6233
6234 # Unsure about the `safe` argument, since this is a legacy way of handling parameters.
6235 compat_urllib_parse_quote_plus(iri_parts.params, safe=r"!$%&'()*+,/:;=@|~"),
6236
6237 # Not totally sure about the `safe` argument, since the source does not explicitly mention the query URI component.
6238 compat_urllib_parse_quote_plus(iri_parts.query, safe=r"!$%&'()*+,/:;=?@{|}~"),
6239
6240 compat_urllib_parse_quote_plus(iri_parts.fragment, safe=r"!#$%&'()*+,/:;=?@{|}~")))
6241
6242 # Source for `safe` arguments: https://url.spec.whatwg.org/#percent-encoded-bytes.
6243
6244
6245def to_high_limit_path(path):
6246 if sys.platform in ['win32', 'cygwin']:
6247 # Work around MAX_PATH limitation on Windows. The maximum allowed length for the individual path segments may still be quite limited.
6248 return r'\\?\ '.rstrip() + os.path.abspath(path)
6249
6250 return path
76d321f6 6251
c76eb41b 6252
b868936c 6253def format_field(obj, field=None, template='%s', ignore=(None, ''), default='', func=None):
6254 if field is None:
6255 val = obj if obj is not None else default
6256 else:
6257 val = obj.get(field, default)
76d321f6 6258 if func and val not in ignore:
6259 val = func(val)
6260 return template % val if val not in ignore else default
00dd0cd5 6261
6262
6263def clean_podcast_url(url):
6264 return re.sub(r'''(?x)
6265 (?:
6266 (?:
6267 chtbl\.com/track|
6268 media\.blubrry\.com| # https://create.blubrry.com/resources/podcast-media-download-statistics/getting-started/
6269 play\.podtrac\.com
6270 )/[^/]+|
6271 (?:dts|www)\.podtrac\.com/(?:pts/)?redirect\.[0-9a-z]{3,4}| # http://analytics.podtrac.com/how-to-measure
6272 flex\.acast\.com|
6273 pd(?:
6274 cn\.co| # https://podcorn.com/analytics-prefix/
6275 st\.fm # https://podsights.com/docs/
6276 )/e
6277 )/''', '', url)
ffcb8191
THD
6278
6279
6280_HEX_TABLE = '0123456789abcdef'
6281
6282
6283def random_uuidv4():
6284 return re.sub(r'[xy]', lambda x: _HEX_TABLE[random.randint(0, 15)], 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx')
0202b52a 6285
6286
6287def make_dir(path, to_screen=None):
6288 try:
6289 dn = os.path.dirname(path)
6290 if dn and not os.path.exists(dn):
6291 os.makedirs(dn)
6292 return True
6293 except (OSError, IOError) as err:
6294 if callable(to_screen) is not None:
6295 to_screen('unable to create directory ' + error_to_compat_str(err))
6296 return False
f74980cb 6297
6298
6299def get_executable_path():
c552ae88 6300 from zipimport import zipimporter
6301 if hasattr(sys, 'frozen'): # Running from PyInstaller
6302 path = os.path.dirname(sys.executable)
6303 elif isinstance(globals().get('__loader__'), zipimporter): # Running from ZIP
6304 path = os.path.join(os.path.dirname(__file__), '../..')
6305 else:
6306 path = os.path.join(os.path.dirname(__file__), '..')
f74980cb 6307 return os.path.abspath(path)
6308
6309
2f567473 6310def load_plugins(name, suffix, namespace):
f74980cb 6311 plugin_info = [None]
3ae5e797 6312 classes = {}
f74980cb 6313 try:
6314 plugin_info = imp.find_module(
6315 name, [os.path.join(get_executable_path(), 'ytdlp_plugins')])
6316 plugins = imp.load_module(name, *plugin_info)
6317 for name in dir(plugins):
2f567473 6318 if name in namespace:
6319 continue
6320 if not name.endswith(suffix):
f74980cb 6321 continue
6322 klass = getattr(plugins, name)
3ae5e797 6323 classes[name] = namespace[name] = klass
f74980cb 6324 except ImportError:
6325 pass
6326 finally:
6327 if plugin_info[0] is not None:
6328 plugin_info[0].close()
6329 return classes
06167fbb 6330
6331
325ebc17 6332def traverse_obj(
352d63fd 6333 obj, *path_list, default=None, expected_type=None, get_all=True,
325ebc17 6334 casesense=True, is_user_input=False, traverse_string=False):
324ad820 6335 ''' Traverse nested list/dict/tuple
8f334380 6336 @param path_list A list of paths which are checked one by one.
6337 Each path is a list of keys where each key is a string,
6338 a tuple of strings or "...". When a tuple is given,
6339 all the keys given in the tuple are traversed, and
6340 "..." traverses all the keys in the object
325ebc17 6341 @param default Default value to return
352d63fd 6342 @param expected_type Only accept final value of this type (Can also be any callable)
6343 @param get_all Return all the values obtained from a path or only the first one
324ad820 6344 @param casesense Whether to consider dictionary keys as case sensitive
6345 @param is_user_input Whether the keys are generated from user input. If True,
6346 strings are converted to int/slice if necessary
6347 @param traverse_string Whether to traverse inside strings. If True, any
6348 non-compatible object will also be converted into a string
8f334380 6349 # TODO: Write tests
324ad820 6350 '''
325ebc17 6351 if not casesense:
dbf5416a 6352 _lower = lambda k: (k.lower() if isinstance(k, str) else k)
8f334380 6353 path_list = (map(_lower, variadic(path)) for path in path_list)
6354
6355 def _traverse_obj(obj, path, _current_depth=0):
6356 nonlocal depth
575e17a1 6357 if obj is None:
6358 return None
8f334380 6359 path = tuple(variadic(path))
6360 for i, key in enumerate(path):
6361 if isinstance(key, (list, tuple)):
6362 obj = [_traverse_obj(obj, sub_key, _current_depth) for sub_key in key]
6363 key = ...
6364 if key is ...:
6365 obj = (obj.values() if isinstance(obj, dict)
6366 else obj if isinstance(obj, (list, tuple, LazyList))
6367 else str(obj) if traverse_string else [])
6368 _current_depth += 1
6369 depth = max(depth, _current_depth)
6370 return [_traverse_obj(inner_obj, path[i + 1:], _current_depth) for inner_obj in obj]
575e17a1 6371 elif isinstance(obj, dict) and not (is_user_input and key == ':'):
325ebc17 6372 obj = (obj.get(key) if casesense or (key in obj)
6373 else next((v for k, v in obj.items() if _lower(k) == key), None))
6374 else:
6375 if is_user_input:
6376 key = (int_or_none(key) if ':' not in key
6377 else slice(*map(int_or_none, key.split(':'))))
8f334380 6378 if key == slice(None):
575e17a1 6379 return _traverse_obj(obj, (..., *path[i + 1:]), _current_depth)
325ebc17 6380 if not isinstance(key, (int, slice)):
9fea350f 6381 return None
8f334380 6382 if not isinstance(obj, (list, tuple, LazyList)):
325ebc17 6383 if not traverse_string:
6384 return None
6385 obj = str(obj)
6386 try:
6387 obj = obj[key]
6388 except IndexError:
324ad820 6389 return None
325ebc17 6390 return obj
6391
352d63fd 6392 if isinstance(expected_type, type):
6393 type_test = lambda val: val if isinstance(val, expected_type) else None
6394 elif expected_type is not None:
6395 type_test = expected_type
6396 else:
6397 type_test = lambda val: val
6398
8f334380 6399 for path in path_list:
6400 depth = 0
6401 val = _traverse_obj(obj, path)
325ebc17 6402 if val is not None:
8f334380 6403 if depth:
6404 for _ in range(depth - 1):
6586bca9 6405 val = itertools.chain.from_iterable(v for v in val if v is not None)
352d63fd 6406 val = [v for v in map(type_test, val) if v is not None]
8f334380 6407 if val:
352d63fd 6408 return val if get_all else val[0]
6409 else:
6410 val = type_test(val)
6411 if val is not None:
8f334380 6412 return val
325ebc17 6413 return default
324ad820 6414
6415
6416def traverse_dict(dictn, keys, casesense=True):
6417 ''' For backward compatibility. Do not use '''
6418 return traverse_obj(dictn, keys, casesense=casesense,
6419 is_user_input=True, traverse_string=True)
6606817a 6420
6421
c634ad2a 6422def variadic(x, allowed_types=(str, bytes)):
cb89cfc1 6423 return x if isinstance(x, collections.abc.Iterable) and not isinstance(x, allowed_types) else (x,)
bd50a52b
THD
6424
6425
49fa4d9a
N
6426# create a JSON Web Signature (jws) with HS256 algorithm
6427# the resulting format is in JWS Compact Serialization
6428# implemented following JWT https://www.rfc-editor.org/rfc/rfc7519.html
6429# implemented following JWS https://www.rfc-editor.org/rfc/rfc7515.html
6430def jwt_encode_hs256(payload_data, key, headers={}):
6431 header_data = {
6432 'alg': 'HS256',
6433 'typ': 'JWT',
6434 }
6435 if headers:
6436 header_data.update(headers)
6437 header_b64 = base64.b64encode(json.dumps(header_data).encode('utf-8'))
6438 payload_b64 = base64.b64encode(json.dumps(payload_data).encode('utf-8'))
6439 h = hmac.new(key.encode('utf-8'), header_b64 + b'.' + payload_b64, hashlib.sha256)
6440 signature_b64 = base64.b64encode(h.digest())
6441 token = header_b64 + b'.' + payload_b64 + b'.' + signature_b64
6442 return token
819e0531 6443
6444
6445def supports_terminal_sequences(stream):
6446 if compat_os_name == 'nt':
6447 if get_windows_version() < (10, ):
6448 return False
6449 elif not os.getenv('TERM'):
6450 return False
6451 try:
6452 return stream.isatty()
6453 except BaseException:
6454 return False
6455
6456
6457TERMINAL_SEQUENCES = {
6458 'DOWN': '\n',
6459 'UP': '\x1b[A',
6460 'ERASE_LINE': '\x1b[K',
6461 'RED': '\033[0;31m',
6462 'YELLOW': '\033[0;33m',
6463 'BLUE': '\033[0;34m',
6464 'RESET_STYLE': '\033[0m',
6465}