[utils] Recognize more patterns in strip_jsonp()
[youtube-dl] / youtube_dl / utils.py
1 #!/usr/bin/env python
2 # coding: utf-8
3
4 from __future__ import unicode_literals
5
6 import base64
7 import binascii
8 import calendar
9 import codecs
10 import contextlib
11 import ctypes
12 import datetime
13 import email.utils
14 import email.header
15 import errno
16 import functools
17 import gzip
18 import io
19 import itertools
20 import json
21 import locale
22 import math
23 import operator
24 import os
25 import pipes
26 import platform
27 import random
28 import re
29 import socket
30 import ssl
31 import subprocess
32 import sys
33 import tempfile
34 import traceback
35 import xml.etree.ElementTree
36 import zlib
37
38 from .compat import (
39     compat_HTMLParser,
40     compat_basestring,
41     compat_chr,
42     compat_etree_fromstring,
43     compat_expanduser,
44     compat_html_entities,
45     compat_html_entities_html5,
46     compat_http_client,
47     compat_kwargs,
48     compat_os_name,
49     compat_parse_qs,
50     compat_shlex_quote,
51     compat_socket_create_connection,
52     compat_str,
53     compat_struct_pack,
54     compat_struct_unpack,
55     compat_urllib_error,
56     compat_urllib_parse,
57     compat_urllib_parse_urlencode,
58     compat_urllib_parse_urlparse,
59     compat_urllib_parse_unquote_plus,
60     compat_urllib_request,
61     compat_urlparse,
62     compat_xpath,
63 )
64
65 from .socks import (
66     ProxyType,
67     sockssocket,
68 )
69
70
71 def register_socks_protocols():
72     # "Register" SOCKS protocols
73     # In Python < 2.6.5, urlsplit() suffers from bug https://bugs.python.org/issue7904
74     # URLs with protocols not in urlparse.uses_netloc are not handled correctly
75     for scheme in ('socks', 'socks4', 'socks4a', 'socks5'):
76         if scheme not in compat_urlparse.uses_netloc:
77             compat_urlparse.uses_netloc.append(scheme)
78
79
80 # This is not clearly defined otherwise
81 compiled_regex_type = type(re.compile(''))
82
83 std_headers = {
84     'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)',
85     'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
86     'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
87     'Accept-Encoding': 'gzip, deflate',
88     'Accept-Language': 'en-us,en;q=0.5',
89 }
90
91
92 USER_AGENTS = {
93     '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',
94 }
95
96
97 NO_DEFAULT = object()
98
99 ENGLISH_MONTH_NAMES = [
100     'January', 'February', 'March', 'April', 'May', 'June',
101     'July', 'August', 'September', 'October', 'November', 'December']
102
103 MONTH_NAMES = {
104     'en': ENGLISH_MONTH_NAMES,
105     'fr': [
106         'janvier', 'février', 'mars', 'avril', 'mai', 'juin',
107         'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'],
108 }
109
110 KNOWN_EXTENSIONS = (
111     'mp4', 'm4a', 'm4p', 'm4b', 'm4r', 'm4v', 'aac',
112     'flv', 'f4v', 'f4a', 'f4b',
113     'webm', 'ogg', 'ogv', 'oga', 'ogx', 'spx', 'opus',
114     'mkv', 'mka', 'mk3d',
115     'avi', 'divx',
116     'mov',
117     'asf', 'wmv', 'wma',
118     '3gp', '3g2',
119     'mp3',
120     'flac',
121     'ape',
122     'wav',
123     'f4f', 'f4m', 'm3u8', 'smil')
124
125 # needed for sanitizing filenames in restricted mode
126 ACCENT_CHARS = dict(zip('ÂÃÄÀÁÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖŐØŒÙÚÛÜŰÝÞßàáâãäåæçèéêëìíîïðñòóôõöőøœùúûüűýþÿ',
127                         itertools.chain('AAAAAA', ['AE'], 'CEEEEIIIIDNOOOOOOO', ['OE'], 'UUUUUYP', ['ss'],
128                                         'aaaaaa', ['ae'], 'ceeeeiiiionooooooo', ['oe'], 'uuuuuypy')))
129
130 DATE_FORMATS = (
131     '%d %B %Y',
132     '%d %b %Y',
133     '%B %d %Y',
134     '%B %dst %Y',
135     '%B %dnd %Y',
136     '%B %dth %Y',
137     '%b %d %Y',
138     '%b %dst %Y',
139     '%b %dnd %Y',
140     '%b %dth %Y',
141     '%b %dst %Y %I:%M',
142     '%b %dnd %Y %I:%M',
143     '%b %dth %Y %I:%M',
144     '%Y %m %d',
145     '%Y-%m-%d',
146     '%Y/%m/%d',
147     '%Y/%m/%d %H:%M',
148     '%Y/%m/%d %H:%M:%S',
149     '%Y-%m-%d %H:%M',
150     '%Y-%m-%d %H:%M:%S',
151     '%Y-%m-%d %H:%M:%S.%f',
152     '%d.%m.%Y %H:%M',
153     '%d.%m.%Y %H.%M',
154     '%Y-%m-%dT%H:%M:%SZ',
155     '%Y-%m-%dT%H:%M:%S.%fZ',
156     '%Y-%m-%dT%H:%M:%S.%f0Z',
157     '%Y-%m-%dT%H:%M:%S',
158     '%Y-%m-%dT%H:%M:%S.%f',
159     '%Y-%m-%dT%H:%M',
160     '%b %d %Y at %H:%M',
161     '%b %d %Y at %H:%M:%S',
162 )
163
164 DATE_FORMATS_DAY_FIRST = list(DATE_FORMATS)
165 DATE_FORMATS_DAY_FIRST.extend([
166     '%d-%m-%Y',
167     '%d.%m.%Y',
168     '%d.%m.%y',
169     '%d/%m/%Y',
170     '%d/%m/%y',
171     '%d/%m/%Y %H:%M:%S',
172 ])
173
174 DATE_FORMATS_MONTH_FIRST = list(DATE_FORMATS)
175 DATE_FORMATS_MONTH_FIRST.extend([
176     '%m-%d-%Y',
177     '%m.%d.%Y',
178     '%m/%d/%Y',
179     '%m/%d/%y',
180     '%m/%d/%Y %H:%M:%S',
181 ])
182
183 PACKED_CODES_RE = r"}\('(.+)',(\d+),(\d+),'([^']+)'\.split\('\|'\)"
184
185
186 def preferredencoding():
187     """Get preferred encoding.
188
189     Returns the best encoding scheme for the system, based on
190     locale.getpreferredencoding() and some further tweaks.
191     """
192     try:
193         pref = locale.getpreferredencoding()
194         'TEST'.encode(pref)
195     except Exception:
196         pref = 'UTF-8'
197
198     return pref
199
200
201 def write_json_file(obj, fn):
202     """ Encode obj as JSON and write it to fn, atomically if possible """
203
204     fn = encodeFilename(fn)
205     if sys.version_info < (3, 0) and sys.platform != 'win32':
206         encoding = get_filesystem_encoding()
207         # os.path.basename returns a bytes object, but NamedTemporaryFile
208         # will fail if the filename contains non ascii characters unless we
209         # use a unicode object
210         path_basename = lambda f: os.path.basename(fn).decode(encoding)
211         # the same for os.path.dirname
212         path_dirname = lambda f: os.path.dirname(fn).decode(encoding)
213     else:
214         path_basename = os.path.basename
215         path_dirname = os.path.dirname
216
217     args = {
218         'suffix': '.tmp',
219         'prefix': path_basename(fn) + '.',
220         'dir': path_dirname(fn),
221         'delete': False,
222     }
223
224     # In Python 2.x, json.dump expects a bytestream.
225     # In Python 3.x, it writes to a character stream
226     if sys.version_info < (3, 0):
227         args['mode'] = 'wb'
228     else:
229         args.update({
230             'mode': 'w',
231             'encoding': 'utf-8',
232         })
233
234     tf = tempfile.NamedTemporaryFile(**compat_kwargs(args))
235
236     try:
237         with tf:
238             json.dump(obj, tf)
239         if sys.platform == 'win32':
240             # Need to remove existing file on Windows, else os.rename raises
241             # WindowsError or FileExistsError.
242             try:
243                 os.unlink(fn)
244             except OSError:
245                 pass
246         os.rename(tf.name, fn)
247     except Exception:
248         try:
249             os.remove(tf.name)
250         except OSError:
251             pass
252         raise
253
254
255 if sys.version_info >= (2, 7):
256     def find_xpath_attr(node, xpath, key, val=None):
257         """ Find the xpath xpath[@key=val] """
258         assert re.match(r'^[a-zA-Z_-]+$', key)
259         expr = xpath + ('[@%s]' % key if val is None else "[@%s='%s']" % (key, val))
260         return node.find(expr)
261 else:
262     def find_xpath_attr(node, xpath, key, val=None):
263         for f in node.findall(compat_xpath(xpath)):
264             if key not in f.attrib:
265                 continue
266             if val is None or f.attrib.get(key) == val:
267                 return f
268         return None
269
270 # On python2.6 the xml.etree.ElementTree.Element methods don't support
271 # the namespace parameter
272
273
274 def xpath_with_ns(path, ns_map):
275     components = [c.split(':') for c in path.split('/')]
276     replaced = []
277     for c in components:
278         if len(c) == 1:
279             replaced.append(c[0])
280         else:
281             ns, tag = c
282             replaced.append('{%s}%s' % (ns_map[ns], tag))
283     return '/'.join(replaced)
284
285
286 def xpath_element(node, xpath, name=None, fatal=False, default=NO_DEFAULT):
287     def _find_xpath(xpath):
288         return node.find(compat_xpath(xpath))
289
290     if isinstance(xpath, (str, compat_str)):
291         n = _find_xpath(xpath)
292     else:
293         for xp in xpath:
294             n = _find_xpath(xp)
295             if n is not None:
296                 break
297
298     if n is None:
299         if default is not NO_DEFAULT:
300             return default
301         elif fatal:
302             name = xpath if name is None else name
303             raise ExtractorError('Could not find XML element %s' % name)
304         else:
305             return None
306     return n
307
308
309 def xpath_text(node, xpath, name=None, fatal=False, default=NO_DEFAULT):
310     n = xpath_element(node, xpath, name, fatal=fatal, default=default)
311     if n is None or n == default:
312         return n
313     if n.text is None:
314         if default is not NO_DEFAULT:
315             return default
316         elif fatal:
317             name = xpath if name is None else name
318             raise ExtractorError('Could not find XML element\'s text %s' % name)
319         else:
320             return None
321     return n.text
322
323
324 def xpath_attr(node, xpath, key, name=None, fatal=False, default=NO_DEFAULT):
325     n = find_xpath_attr(node, xpath, key)
326     if n is None:
327         if default is not NO_DEFAULT:
328             return default
329         elif fatal:
330             name = '%s[@%s]' % (xpath, key) if name is None else name
331             raise ExtractorError('Could not find XML attribute %s' % name)
332         else:
333             return None
334     return n.attrib[key]
335
336
337 def get_element_by_id(id, html):
338     """Return the content of the tag with the specified ID in the passed HTML document"""
339     return get_element_by_attribute('id', id, html)
340
341
342 def get_element_by_class(class_name, html):
343     """Return the content of the first tag with the specified class in the passed HTML document"""
344     retval = get_elements_by_class(class_name, html)
345     return retval[0] if retval else None
346
347
348 def get_element_by_attribute(attribute, value, html, escape_value=True):
349     retval = get_elements_by_attribute(attribute, value, html, escape_value)
350     return retval[0] if retval else None
351
352
353 def get_elements_by_class(class_name, html):
354     """Return the content of all tags with the specified class in the passed HTML document as a list"""
355     return get_elements_by_attribute(
356         'class', r'[^\'"]*\b%s\b[^\'"]*' % re.escape(class_name),
357         html, escape_value=False)
358
359
360 def get_elements_by_attribute(attribute, value, html, escape_value=True):
361     """Return the content of the tag with the specified attribute in the passed HTML document"""
362
363     value = re.escape(value) if escape_value else value
364
365     retlist = []
366     for m in re.finditer(r'''(?xs)
367         <([a-zA-Z0-9:._-]+)
368          (?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]*|="[^"]*"|='[^']*'))*?
369          \s+%s=['"]?%s['"]?
370          (?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]*|="[^"]*"|='[^']*'))*?
371         \s*>
372         (?P<content>.*?)
373         </\1>
374     ''' % (re.escape(attribute), value), html):
375         res = m.group('content')
376
377         if res.startswith('"') or res.startswith("'"):
378             res = res[1:-1]
379
380         retlist.append(unescapeHTML(res))
381
382     return retlist
383
384
385 class HTMLAttributeParser(compat_HTMLParser):
386     """Trivial HTML parser to gather the attributes for a single element"""
387     def __init__(self):
388         self.attrs = {}
389         compat_HTMLParser.__init__(self)
390
391     def handle_starttag(self, tag, attrs):
392         self.attrs = dict(attrs)
393
394
395 def extract_attributes(html_element):
396     """Given a string for an HTML element such as
397     <el
398          a="foo" B="bar" c="&98;az" d=boz
399          empty= noval entity="&amp;"
400          sq='"' dq="'"
401     >
402     Decode and return a dictionary of attributes.
403     {
404         'a': 'foo', 'b': 'bar', c: 'baz', d: 'boz',
405         'empty': '', 'noval': None, 'entity': '&',
406         'sq': '"', 'dq': '\''
407     }.
408     NB HTMLParser is stricter in Python 2.6 & 3.2 than in later versions,
409     but the cases in the unit test will work for all of 2.6, 2.7, 3.2-3.5.
410     """
411     parser = HTMLAttributeParser()
412     parser.feed(html_element)
413     parser.close()
414     return parser.attrs
415
416
417 def clean_html(html):
418     """Clean an HTML snippet into a readable string"""
419
420     if html is None:  # Convenience for sanitizing descriptions etc.
421         return html
422
423     # Newline vs <br />
424     html = html.replace('\n', ' ')
425     html = re.sub(r'(?u)\s*<\s*br\s*/?\s*>\s*', '\n', html)
426     html = re.sub(r'(?u)<\s*/\s*p\s*>\s*<\s*p[^>]*>', '\n', html)
427     # Strip html tags
428     html = re.sub('<.*?>', '', html)
429     # Replace html entities
430     html = unescapeHTML(html)
431     return html.strip()
432
433
434 def sanitize_open(filename, open_mode):
435     """Try to open the given filename, and slightly tweak it if this fails.
436
437     Attempts to open the given filename. If this fails, it tries to change
438     the filename slightly, step by step, until it's either able to open it
439     or it fails and raises a final exception, like the standard open()
440     function.
441
442     It returns the tuple (stream, definitive_file_name).
443     """
444     try:
445         if filename == '-':
446             if sys.platform == 'win32':
447                 import msvcrt
448                 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
449             return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename)
450         stream = open(encodeFilename(filename), open_mode)
451         return (stream, filename)
452     except (IOError, OSError) as err:
453         if err.errno in (errno.EACCES,):
454             raise
455
456         # In case of error, try to remove win32 forbidden chars
457         alt_filename = sanitize_path(filename)
458         if alt_filename == filename:
459             raise
460         else:
461             # An exception here should be caught in the caller
462             stream = open(encodeFilename(alt_filename), open_mode)
463             return (stream, alt_filename)
464
465
466 def timeconvert(timestr):
467     """Convert RFC 2822 defined time string into system timestamp"""
468     timestamp = None
469     timetuple = email.utils.parsedate_tz(timestr)
470     if timetuple is not None:
471         timestamp = email.utils.mktime_tz(timetuple)
472     return timestamp
473
474
475 def sanitize_filename(s, restricted=False, is_id=False):
476     """Sanitizes a string so it could be used as part of a filename.
477     If restricted is set, use a stricter subset of allowed characters.
478     Set is_id if this is not an arbitrary string, but an ID that should be kept
479     if possible.
480     """
481     def replace_insane(char):
482         if restricted and char in ACCENT_CHARS:
483             return ACCENT_CHARS[char]
484         if char == '?' or ord(char) < 32 or ord(char) == 127:
485             return ''
486         elif char == '"':
487             return '' if restricted else '\''
488         elif char == ':':
489             return '_-' if restricted else ' -'
490         elif char in '\\/|*<>':
491             return '_'
492         if restricted and (char in '!&\'()[]{}$;`^,#' or char.isspace()):
493             return '_'
494         if restricted and ord(char) > 127:
495             return '_'
496         return char
497
498     # Handle timestamps
499     s = re.sub(r'[0-9]+(?::[0-9]+)+', lambda m: m.group(0).replace(':', '_'), s)
500     result = ''.join(map(replace_insane, s))
501     if not is_id:
502         while '__' in result:
503             result = result.replace('__', '_')
504         result = result.strip('_')
505         # Common case of "Foreign band name - English song title"
506         if restricted and result.startswith('-_'):
507             result = result[2:]
508         if result.startswith('-'):
509             result = '_' + result[len('-'):]
510         result = result.lstrip('.')
511         if not result:
512             result = '_'
513     return result
514
515
516 def sanitize_path(s):
517     """Sanitizes and normalizes path on Windows"""
518     if sys.platform != 'win32':
519         return s
520     drive_or_unc, _ = os.path.splitdrive(s)
521     if sys.version_info < (2, 7) and not drive_or_unc:
522         drive_or_unc, _ = os.path.splitunc(s)
523     norm_path = os.path.normpath(remove_start(s, drive_or_unc)).split(os.path.sep)
524     if drive_or_unc:
525         norm_path.pop(0)
526     sanitized_path = [
527         path_part if path_part in ['.', '..'] else re.sub(r'(?:[/<>:"\|\\?\*]|[\s.]$)', '#', path_part)
528         for path_part in norm_path]
529     if drive_or_unc:
530         sanitized_path.insert(0, drive_or_unc + os.path.sep)
531     return os.path.join(*sanitized_path)
532
533
534 # Prepend protocol-less URLs with `http:` scheme in order to mitigate the number of
535 # unwanted failures due to missing protocol
536 def sanitize_url(url):
537     return 'http:%s' % url if url.startswith('//') else url
538
539
540 def sanitized_Request(url, *args, **kwargs):
541     return compat_urllib_request.Request(sanitize_url(url), *args, **kwargs)
542
543
544 def expand_path(s):
545     """Expand shell variables and ~"""
546     return os.path.expandvars(compat_expanduser(s))
547
548
549 def orderedSet(iterable):
550     """ Remove all duplicates from the input iterable """
551     res = []
552     for el in iterable:
553         if el not in res:
554             res.append(el)
555     return res
556
557
558 def _htmlentity_transform(entity_with_semicolon):
559     """Transforms an HTML entity to a character."""
560     entity = entity_with_semicolon[:-1]
561
562     # Known non-numeric HTML entity
563     if entity in compat_html_entities.name2codepoint:
564         return compat_chr(compat_html_entities.name2codepoint[entity])
565
566     # TODO: HTML5 allows entities without a semicolon. For example,
567     # '&Eacuteric' should be decoded as 'Éric'.
568     if entity_with_semicolon in compat_html_entities_html5:
569         return compat_html_entities_html5[entity_with_semicolon]
570
571     mobj = re.match(r'#(x[0-9a-fA-F]+|[0-9]+)', entity)
572     if mobj is not None:
573         numstr = mobj.group(1)
574         if numstr.startswith('x'):
575             base = 16
576             numstr = '0%s' % numstr
577         else:
578             base = 10
579         # See https://github.com/rg3/youtube-dl/issues/7518
580         try:
581             return compat_chr(int(numstr, base))
582         except ValueError:
583             pass
584
585     # Unknown entity in name, return its literal representation
586     return '&%s;' % entity
587
588
589 def unescapeHTML(s):
590     if s is None:
591         return None
592     assert type(s) == compat_str
593
594     return re.sub(
595         r'&([^;]+;)', lambda m: _htmlentity_transform(m.group(1)), s)
596
597
598 def get_subprocess_encoding():
599     if sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
600         # For subprocess calls, encode with locale encoding
601         # Refer to http://stackoverflow.com/a/9951851/35070
602         encoding = preferredencoding()
603     else:
604         encoding = sys.getfilesystemencoding()
605     if encoding is None:
606         encoding = 'utf-8'
607     return encoding
608
609
610 def encodeFilename(s, for_subprocess=False):
611     """
612     @param s The name of the file
613     """
614
615     assert type(s) == compat_str
616
617     # Python 3 has a Unicode API
618     if sys.version_info >= (3, 0):
619         return s
620
621     # Pass '' directly to use Unicode APIs on Windows 2000 and up
622     # (Detecting Windows NT 4 is tricky because 'major >= 4' would
623     # match Windows 9x series as well. Besides, NT 4 is obsolete.)
624     if not for_subprocess and sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
625         return s
626
627     # Jython assumes filenames are Unicode strings though reported as Python 2.x compatible
628     if sys.platform.startswith('java'):
629         return s
630
631     return s.encode(get_subprocess_encoding(), 'ignore')
632
633
634 def decodeFilename(b, for_subprocess=False):
635
636     if sys.version_info >= (3, 0):
637         return b
638
639     if not isinstance(b, bytes):
640         return b
641
642     return b.decode(get_subprocess_encoding(), 'ignore')
643
644
645 def encodeArgument(s):
646     if not isinstance(s, compat_str):
647         # Legacy code that uses byte strings
648         # Uncomment the following line after fixing all post processors
649         # assert False, 'Internal error: %r should be of type %r, is %r' % (s, compat_str, type(s))
650         s = s.decode('ascii')
651     return encodeFilename(s, True)
652
653
654 def decodeArgument(b):
655     return decodeFilename(b, True)
656
657
658 def decodeOption(optval):
659     if optval is None:
660         return optval
661     if isinstance(optval, bytes):
662         optval = optval.decode(preferredencoding())
663
664     assert isinstance(optval, compat_str)
665     return optval
666
667
668 def formatSeconds(secs):
669     if secs > 3600:
670         return '%d:%02d:%02d' % (secs // 3600, (secs % 3600) // 60, secs % 60)
671     elif secs > 60:
672         return '%d:%02d' % (secs // 60, secs % 60)
673     else:
674         return '%d' % secs
675
676
677 def make_HTTPS_handler(params, **kwargs):
678     opts_no_check_certificate = params.get('nocheckcertificate', False)
679     if hasattr(ssl, 'create_default_context'):  # Python >= 3.4 or 2.7.9
680         context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
681         if opts_no_check_certificate:
682             context.check_hostname = False
683             context.verify_mode = ssl.CERT_NONE
684         try:
685             return YoutubeDLHTTPSHandler(params, context=context, **kwargs)
686         except TypeError:
687             # Python 2.7.8
688             # (create_default_context present but HTTPSHandler has no context=)
689             pass
690
691     if sys.version_info < (3, 2):
692         return YoutubeDLHTTPSHandler(params, **kwargs)
693     else:  # Python < 3.4
694         context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
695         context.verify_mode = (ssl.CERT_NONE
696                                if opts_no_check_certificate
697                                else ssl.CERT_REQUIRED)
698         context.set_default_verify_paths()
699         return YoutubeDLHTTPSHandler(params, context=context, **kwargs)
700
701
702 def bug_reports_message():
703     if ytdl_is_updateable():
704         update_cmd = 'type  youtube-dl -U  to update'
705     else:
706         update_cmd = 'see  https://yt-dl.org/update  on how to update'
707     msg = '; please report this issue on https://yt-dl.org/bug .'
708     msg += ' Make sure you are using the latest version; %s.' % update_cmd
709     msg += ' Be sure to call youtube-dl with the --verbose flag and include its complete output.'
710     return msg
711
712
713 class YoutubeDLError(Exception):
714     """Base exception for YoutubeDL errors."""
715     pass
716
717
718 class ExtractorError(YoutubeDLError):
719     """Error during info extraction."""
720
721     def __init__(self, msg, tb=None, expected=False, cause=None, video_id=None):
722         """ tb, if given, is the original traceback (so that it can be printed out).
723         If expected is set, this is a normal error message and most likely not a bug in youtube-dl.
724         """
725
726         if sys.exc_info()[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError):
727             expected = True
728         if video_id is not None:
729             msg = video_id + ': ' + msg
730         if cause:
731             msg += ' (caused by %r)' % cause
732         if not expected:
733             msg += bug_reports_message()
734         super(ExtractorError, self).__init__(msg)
735
736         self.traceback = tb
737         self.exc_info = sys.exc_info()  # preserve original exception
738         self.cause = cause
739         self.video_id = video_id
740
741     def format_traceback(self):
742         if self.traceback is None:
743             return None
744         return ''.join(traceback.format_tb(self.traceback))
745
746
747 class UnsupportedError(ExtractorError):
748     def __init__(self, url):
749         super(UnsupportedError, self).__init__(
750             'Unsupported URL: %s' % url, expected=True)
751         self.url = url
752
753
754 class RegexNotFoundError(ExtractorError):
755     """Error when a regex didn't match"""
756     pass
757
758
759 class GeoRestrictedError(ExtractorError):
760     """Geographic restriction Error exception.
761
762     This exception may be thrown when a video is not available from your
763     geographic location due to geographic restrictions imposed by a website.
764     """
765     def __init__(self, msg, countries=None):
766         super(GeoRestrictedError, self).__init__(msg, expected=True)
767         self.msg = msg
768         self.countries = countries
769
770
771 class DownloadError(YoutubeDLError):
772     """Download Error exception.
773
774     This exception may be thrown by FileDownloader objects if they are not
775     configured to continue on errors. They will contain the appropriate
776     error message.
777     """
778
779     def __init__(self, msg, exc_info=None):
780         """ exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """
781         super(DownloadError, self).__init__(msg)
782         self.exc_info = exc_info
783
784
785 class SameFileError(YoutubeDLError):
786     """Same File exception.
787
788     This exception will be thrown by FileDownloader objects if they detect
789     multiple files would have to be downloaded to the same file on disk.
790     """
791     pass
792
793
794 class PostProcessingError(YoutubeDLError):
795     """Post Processing exception.
796
797     This exception may be raised by PostProcessor's .run() method to
798     indicate an error in the postprocessing task.
799     """
800
801     def __init__(self, msg):
802         super(PostProcessingError, self).__init__(msg)
803         self.msg = msg
804
805
806 class MaxDownloadsReached(YoutubeDLError):
807     """ --max-downloads limit has been reached. """
808     pass
809
810
811 class UnavailableVideoError(YoutubeDLError):
812     """Unavailable Format exception.
813
814     This exception will be thrown when a video is requested
815     in a format that is not available for that video.
816     """
817     pass
818
819
820 class ContentTooShortError(YoutubeDLError):
821     """Content Too Short exception.
822
823     This exception may be raised by FileDownloader objects when a file they
824     download is too small for what the server announced first, indicating
825     the connection was probably interrupted.
826     """
827
828     def __init__(self, downloaded, expected):
829         super(ContentTooShortError, self).__init__(
830             'Downloaded {0} bytes, expected {1} bytes'.format(downloaded, expected)
831         )
832         # Both in bytes
833         self.downloaded = downloaded
834         self.expected = expected
835
836
837 class XAttrMetadataError(YoutubeDLError):
838     def __init__(self, code=None, msg='Unknown error'):
839         super(XAttrMetadataError, self).__init__(msg)
840         self.code = code
841         self.msg = msg
842
843         # Parsing code and msg
844         if (self.code in (errno.ENOSPC, errno.EDQUOT) or
845                 'No space left' in self.msg or 'Disk quota excedded' in self.msg):
846             self.reason = 'NO_SPACE'
847         elif self.code == errno.E2BIG or 'Argument list too long' in self.msg:
848             self.reason = 'VALUE_TOO_LONG'
849         else:
850             self.reason = 'NOT_SUPPORTED'
851
852
853 class XAttrUnavailableError(YoutubeDLError):
854     pass
855
856
857 def _create_http_connection(ydl_handler, http_class, is_https, *args, **kwargs):
858     # Working around python 2 bug (see http://bugs.python.org/issue17849) by limiting
859     # expected HTTP responses to meet HTTP/1.0 or later (see also
860     # https://github.com/rg3/youtube-dl/issues/6727)
861     if sys.version_info < (3, 0):
862         kwargs[b'strict'] = True
863     hc = http_class(*args, **kwargs)
864     source_address = ydl_handler._params.get('source_address')
865     if source_address is not None:
866         sa = (source_address, 0)
867         if hasattr(hc, 'source_address'):  # Python 2.7+
868             hc.source_address = sa
869         else:  # Python 2.6
870             def _hc_connect(self, *args, **kwargs):
871                 sock = compat_socket_create_connection(
872                     (self.host, self.port), self.timeout, sa)
873                 if is_https:
874                     self.sock = ssl.wrap_socket(
875                         sock, self.key_file, self.cert_file,
876                         ssl_version=ssl.PROTOCOL_TLSv1)
877                 else:
878                     self.sock = sock
879             hc.connect = functools.partial(_hc_connect, hc)
880
881     return hc
882
883
884 def handle_youtubedl_headers(headers):
885     filtered_headers = headers
886
887     if 'Youtubedl-no-compression' in filtered_headers:
888         filtered_headers = dict((k, v) for k, v in filtered_headers.items() if k.lower() != 'accept-encoding')
889         del filtered_headers['Youtubedl-no-compression']
890
891     return filtered_headers
892
893
894 class YoutubeDLHandler(compat_urllib_request.HTTPHandler):
895     """Handler for HTTP requests and responses.
896
897     This class, when installed with an OpenerDirector, automatically adds
898     the standard headers to every HTTP request and handles gzipped and
899     deflated responses from web servers. If compression is to be avoided in
900     a particular request, the original request in the program code only has
901     to include the HTTP header "Youtubedl-no-compression", which will be
902     removed before making the real request.
903
904     Part of this code was copied from:
905
906     http://techknack.net/python-urllib2-handlers/
907
908     Andrew Rowls, the author of that code, agreed to release it to the
909     public domain.
910     """
911
912     def __init__(self, params, *args, **kwargs):
913         compat_urllib_request.HTTPHandler.__init__(self, *args, **kwargs)
914         self._params = params
915
916     def http_open(self, req):
917         conn_class = compat_http_client.HTTPConnection
918
919         socks_proxy = req.headers.get('Ytdl-socks-proxy')
920         if socks_proxy:
921             conn_class = make_socks_conn_class(conn_class, socks_proxy)
922             del req.headers['Ytdl-socks-proxy']
923
924         return self.do_open(functools.partial(
925             _create_http_connection, self, conn_class, False),
926             req)
927
928     @staticmethod
929     def deflate(data):
930         try:
931             return zlib.decompress(data, -zlib.MAX_WBITS)
932         except zlib.error:
933             return zlib.decompress(data)
934
935     @staticmethod
936     def addinfourl_wrapper(stream, headers, url, code):
937         if hasattr(compat_urllib_request.addinfourl, 'getcode'):
938             return compat_urllib_request.addinfourl(stream, headers, url, code)
939         ret = compat_urllib_request.addinfourl(stream, headers, url)
940         ret.code = code
941         return ret
942
943     def http_request(self, req):
944         # According to RFC 3986, URLs can not contain non-ASCII characters, however this is not
945         # always respected by websites, some tend to give out URLs with non percent-encoded
946         # non-ASCII characters (see telemb.py, ard.py [#3412])
947         # urllib chokes on URLs with non-ASCII characters (see http://bugs.python.org/issue3991)
948         # To work around aforementioned issue we will replace request's original URL with
949         # percent-encoded one
950         # Since redirects are also affected (e.g. http://www.southpark.de/alle-episoden/s18e09)
951         # the code of this workaround has been moved here from YoutubeDL.urlopen()
952         url = req.get_full_url()
953         url_escaped = escape_url(url)
954
955         # Substitute URL if any change after escaping
956         if url != url_escaped:
957             req = update_Request(req, url=url_escaped)
958
959         for h, v in std_headers.items():
960             # Capitalize is needed because of Python bug 2275: http://bugs.python.org/issue2275
961             # The dict keys are capitalized because of this bug by urllib
962             if h.capitalize() not in req.headers:
963                 req.add_header(h, v)
964
965         req.headers = handle_youtubedl_headers(req.headers)
966
967         if sys.version_info < (2, 7) and '#' in req.get_full_url():
968             # Python 2.6 is brain-dead when it comes to fragments
969             req._Request__original = req._Request__original.partition('#')[0]
970             req._Request__r_type = req._Request__r_type.partition('#')[0]
971
972         return req
973
974     def http_response(self, req, resp):
975         old_resp = resp
976         # gzip
977         if resp.headers.get('Content-encoding', '') == 'gzip':
978             content = resp.read()
979             gz = gzip.GzipFile(fileobj=io.BytesIO(content), mode='rb')
980             try:
981                 uncompressed = io.BytesIO(gz.read())
982             except IOError as original_ioerror:
983                 # There may be junk add the end of the file
984                 # See http://stackoverflow.com/q/4928560/35070 for details
985                 for i in range(1, 1024):
986                     try:
987                         gz = gzip.GzipFile(fileobj=io.BytesIO(content[:-i]), mode='rb')
988                         uncompressed = io.BytesIO(gz.read())
989                     except IOError:
990                         continue
991                     break
992                 else:
993                     raise original_ioerror
994             resp = self.addinfourl_wrapper(uncompressed, old_resp.headers, old_resp.url, old_resp.code)
995             resp.msg = old_resp.msg
996             del resp.headers['Content-encoding']
997         # deflate
998         if resp.headers.get('Content-encoding', '') == 'deflate':
999             gz = io.BytesIO(self.deflate(resp.read()))
1000             resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code)
1001             resp.msg = old_resp.msg
1002             del resp.headers['Content-encoding']
1003         # Percent-encode redirect URL of Location HTTP header to satisfy RFC 3986 (see
1004         # https://github.com/rg3/youtube-dl/issues/6457).
1005         if 300 <= resp.code < 400:
1006             location = resp.headers.get('Location')
1007             if location:
1008                 # As of RFC 2616 default charset is iso-8859-1 that is respected by python 3
1009                 if sys.version_info >= (3, 0):
1010                     location = location.encode('iso-8859-1').decode('utf-8')
1011                 else:
1012                     location = location.decode('utf-8')
1013                 location_escaped = escape_url(location)
1014                 if location != location_escaped:
1015                     del resp.headers['Location']
1016                     if sys.version_info < (3, 0):
1017                         location_escaped = location_escaped.encode('utf-8')
1018                     resp.headers['Location'] = location_escaped
1019         return resp
1020
1021     https_request = http_request
1022     https_response = http_response
1023
1024
1025 def make_socks_conn_class(base_class, socks_proxy):
1026     assert issubclass(base_class, (
1027         compat_http_client.HTTPConnection, compat_http_client.HTTPSConnection))
1028
1029     url_components = compat_urlparse.urlparse(socks_proxy)
1030     if url_components.scheme.lower() == 'socks5':
1031         socks_type = ProxyType.SOCKS5
1032     elif url_components.scheme.lower() in ('socks', 'socks4'):
1033         socks_type = ProxyType.SOCKS4
1034     elif url_components.scheme.lower() == 'socks4a':
1035         socks_type = ProxyType.SOCKS4A
1036
1037     def unquote_if_non_empty(s):
1038         if not s:
1039             return s
1040         return compat_urllib_parse_unquote_plus(s)
1041
1042     proxy_args = (
1043         socks_type,
1044         url_components.hostname, url_components.port or 1080,
1045         True,  # Remote DNS
1046         unquote_if_non_empty(url_components.username),
1047         unquote_if_non_empty(url_components.password),
1048     )
1049
1050     class SocksConnection(base_class):
1051         def connect(self):
1052             self.sock = sockssocket()
1053             self.sock.setproxy(*proxy_args)
1054             if type(self.timeout) in (int, float):
1055                 self.sock.settimeout(self.timeout)
1056             self.sock.connect((self.host, self.port))
1057
1058             if isinstance(self, compat_http_client.HTTPSConnection):
1059                 if hasattr(self, '_context'):  # Python > 2.6
1060                     self.sock = self._context.wrap_socket(
1061                         self.sock, server_hostname=self.host)
1062                 else:
1063                     self.sock = ssl.wrap_socket(self.sock)
1064
1065     return SocksConnection
1066
1067
1068 class YoutubeDLHTTPSHandler(compat_urllib_request.HTTPSHandler):
1069     def __init__(self, params, https_conn_class=None, *args, **kwargs):
1070         compat_urllib_request.HTTPSHandler.__init__(self, *args, **kwargs)
1071         self._https_conn_class = https_conn_class or compat_http_client.HTTPSConnection
1072         self._params = params
1073
1074     def https_open(self, req):
1075         kwargs = {}
1076         conn_class = self._https_conn_class
1077
1078         if hasattr(self, '_context'):  # python > 2.6
1079             kwargs['context'] = self._context
1080         if hasattr(self, '_check_hostname'):  # python 3.x
1081             kwargs['check_hostname'] = self._check_hostname
1082
1083         socks_proxy = req.headers.get('Ytdl-socks-proxy')
1084         if socks_proxy:
1085             conn_class = make_socks_conn_class(conn_class, socks_proxy)
1086             del req.headers['Ytdl-socks-proxy']
1087
1088         return self.do_open(functools.partial(
1089             _create_http_connection, self, conn_class, True),
1090             req, **kwargs)
1091
1092
1093 class YoutubeDLCookieProcessor(compat_urllib_request.HTTPCookieProcessor):
1094     def __init__(self, cookiejar=None):
1095         compat_urllib_request.HTTPCookieProcessor.__init__(self, cookiejar)
1096
1097     def http_response(self, request, response):
1098         # Python 2 will choke on next HTTP request in row if there are non-ASCII
1099         # characters in Set-Cookie HTTP header of last response (see
1100         # https://github.com/rg3/youtube-dl/issues/6769).
1101         # In order to at least prevent crashing we will percent encode Set-Cookie
1102         # header before HTTPCookieProcessor starts processing it.
1103         # if sys.version_info < (3, 0) and response.headers:
1104         #     for set_cookie_header in ('Set-Cookie', 'Set-Cookie2'):
1105         #         set_cookie = response.headers.get(set_cookie_header)
1106         #         if set_cookie:
1107         #             set_cookie_escaped = compat_urllib_parse.quote(set_cookie, b"%/;:@&=+$,!~*'()?#[] ")
1108         #             if set_cookie != set_cookie_escaped:
1109         #                 del response.headers[set_cookie_header]
1110         #                 response.headers[set_cookie_header] = set_cookie_escaped
1111         return compat_urllib_request.HTTPCookieProcessor.http_response(self, request, response)
1112
1113     https_request = compat_urllib_request.HTTPCookieProcessor.http_request
1114     https_response = http_response
1115
1116
1117 def extract_timezone(date_str):
1118     m = re.search(
1119         r'^.{8,}?(?P<tz>Z$| ?(?P<sign>\+|-)(?P<hours>[0-9]{2}):?(?P<minutes>[0-9]{2})$)',
1120         date_str)
1121     if not m:
1122         timezone = datetime.timedelta()
1123     else:
1124         date_str = date_str[:-len(m.group('tz'))]
1125         if not m.group('sign'):
1126             timezone = datetime.timedelta()
1127         else:
1128             sign = 1 if m.group('sign') == '+' else -1
1129             timezone = datetime.timedelta(
1130                 hours=sign * int(m.group('hours')),
1131                 minutes=sign * int(m.group('minutes')))
1132     return timezone, date_str
1133
1134
1135 def parse_iso8601(date_str, delimiter='T', timezone=None):
1136     """ Return a UNIX timestamp from the given date """
1137
1138     if date_str is None:
1139         return None
1140
1141     date_str = re.sub(r'\.[0-9]+', '', date_str)
1142
1143     if timezone is None:
1144         timezone, date_str = extract_timezone(date_str)
1145
1146     try:
1147         date_format = '%Y-%m-%d{0}%H:%M:%S'.format(delimiter)
1148         dt = datetime.datetime.strptime(date_str, date_format) - timezone
1149         return calendar.timegm(dt.timetuple())
1150     except ValueError:
1151         pass
1152
1153
1154 def date_formats(day_first=True):
1155     return DATE_FORMATS_DAY_FIRST if day_first else DATE_FORMATS_MONTH_FIRST
1156
1157
1158 def unified_strdate(date_str, day_first=True):
1159     """Return a string with the date in the format YYYYMMDD"""
1160
1161     if date_str is None:
1162         return None
1163     upload_date = None
1164     # Replace commas
1165     date_str = date_str.replace(',', ' ')
1166     # Remove AM/PM + timezone
1167     date_str = re.sub(r'(?i)\s*(?:AM|PM)(?:\s+[A-Z]+)?', '', date_str)
1168     _, date_str = extract_timezone(date_str)
1169
1170     for expression in date_formats(day_first):
1171         try:
1172             upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
1173         except ValueError:
1174             pass
1175     if upload_date is None:
1176         timetuple = email.utils.parsedate_tz(date_str)
1177         if timetuple:
1178             try:
1179                 upload_date = datetime.datetime(*timetuple[:6]).strftime('%Y%m%d')
1180             except ValueError:
1181                 pass
1182     if upload_date is not None:
1183         return compat_str(upload_date)
1184
1185
1186 def unified_timestamp(date_str, day_first=True):
1187     if date_str is None:
1188         return None
1189
1190     date_str = date_str.replace(',', ' ')
1191
1192     pm_delta = 12 if re.search(r'(?i)PM', date_str) else 0
1193     timezone, date_str = extract_timezone(date_str)
1194
1195     # Remove AM/PM + timezone
1196     date_str = re.sub(r'(?i)\s*(?:AM|PM)(?:\s+[A-Z]+)?', '', date_str)
1197
1198     # Remove unrecognized timezones from ISO 8601 alike timestamps
1199     m = re.search(r'\d{1,2}:\d{1,2}(?:\.\d+)?(?P<tz>\s*[A-Z]+)$', date_str)
1200     if m:
1201         date_str = date_str[:-len(m.group('tz'))]
1202
1203     for expression in date_formats(day_first):
1204         try:
1205             dt = datetime.datetime.strptime(date_str, expression) - timezone + datetime.timedelta(hours=pm_delta)
1206             return calendar.timegm(dt.timetuple())
1207         except ValueError:
1208             pass
1209     timetuple = email.utils.parsedate_tz(date_str)
1210     if timetuple:
1211         return calendar.timegm(timetuple) + pm_delta * 3600
1212
1213
1214 def determine_ext(url, default_ext='unknown_video'):
1215     if url is None:
1216         return default_ext
1217     guess = url.partition('?')[0].rpartition('.')[2]
1218     if re.match(r'^[A-Za-z0-9]+$', guess):
1219         return guess
1220     # Try extract ext from URLs like http://example.com/foo/bar.mp4/?download
1221     elif guess.rstrip('/') in KNOWN_EXTENSIONS:
1222         return guess.rstrip('/')
1223     else:
1224         return default_ext
1225
1226
1227 def subtitles_filename(filename, sub_lang, sub_format):
1228     return filename.rsplit('.', 1)[0] + '.' + sub_lang + '.' + sub_format
1229
1230
1231 def date_from_str(date_str):
1232     """
1233     Return a datetime object from a string in the format YYYYMMDD or
1234     (now|today)[+-][0-9](day|week|month|year)(s)?"""
1235     today = datetime.date.today()
1236     if date_str in ('now', 'today'):
1237         return today
1238     if date_str == 'yesterday':
1239         return today - datetime.timedelta(days=1)
1240     match = re.match(r'(now|today)(?P<sign>[+-])(?P<time>\d+)(?P<unit>day|week|month|year)(s)?', date_str)
1241     if match is not None:
1242         sign = match.group('sign')
1243         time = int(match.group('time'))
1244         if sign == '-':
1245             time = -time
1246         unit = match.group('unit')
1247         # A bad approximation?
1248         if unit == 'month':
1249             unit = 'day'
1250             time *= 30
1251         elif unit == 'year':
1252             unit = 'day'
1253             time *= 365
1254         unit += 's'
1255         delta = datetime.timedelta(**{unit: time})
1256         return today + delta
1257     return datetime.datetime.strptime(date_str, '%Y%m%d').date()
1258
1259
1260 def hyphenate_date(date_str):
1261     """
1262     Convert a date in 'YYYYMMDD' format to 'YYYY-MM-DD' format"""
1263     match = re.match(r'^(\d\d\d\d)(\d\d)(\d\d)$', date_str)
1264     if match is not None:
1265         return '-'.join(match.groups())
1266     else:
1267         return date_str
1268
1269
1270 class DateRange(object):
1271     """Represents a time interval between two dates"""
1272
1273     def __init__(self, start=None, end=None):
1274         """start and end must be strings in the format accepted by date"""
1275         if start is not None:
1276             self.start = date_from_str(start)
1277         else:
1278             self.start = datetime.datetime.min.date()
1279         if end is not None:
1280             self.end = date_from_str(end)
1281         else:
1282             self.end = datetime.datetime.max.date()
1283         if self.start > self.end:
1284             raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
1285
1286     @classmethod
1287     def day(cls, day):
1288         """Returns a range that only contains the given day"""
1289         return cls(day, day)
1290
1291     def __contains__(self, date):
1292         """Check if the date is in the range"""
1293         if not isinstance(date, datetime.date):
1294             date = date_from_str(date)
1295         return self.start <= date <= self.end
1296
1297     def __str__(self):
1298         return '%s - %s' % (self.start.isoformat(), self.end.isoformat())
1299
1300
1301 def platform_name():
1302     """ Returns the platform name as a compat_str """
1303     res = platform.platform()
1304     if isinstance(res, bytes):
1305         res = res.decode(preferredencoding())
1306
1307     assert isinstance(res, compat_str)
1308     return res
1309
1310
1311 def _windows_write_string(s, out):
1312     """ Returns True if the string was written using special methods,
1313     False if it has yet to be written out."""
1314     # Adapted from http://stackoverflow.com/a/3259271/35070
1315
1316     import ctypes
1317     import ctypes.wintypes
1318
1319     WIN_OUTPUT_IDS = {
1320         1: -11,
1321         2: -12,
1322     }
1323
1324     try:
1325         fileno = out.fileno()
1326     except AttributeError:
1327         # If the output stream doesn't have a fileno, it's virtual
1328         return False
1329     except io.UnsupportedOperation:
1330         # Some strange Windows pseudo files?
1331         return False
1332     if fileno not in WIN_OUTPUT_IDS:
1333         return False
1334
1335     GetStdHandle = ctypes.WINFUNCTYPE(
1336         ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD)(
1337         (b'GetStdHandle', ctypes.windll.kernel32))
1338     h = GetStdHandle(WIN_OUTPUT_IDS[fileno])
1339
1340     WriteConsoleW = ctypes.WINFUNCTYPE(
1341         ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE, ctypes.wintypes.LPWSTR,
1342         ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD),
1343         ctypes.wintypes.LPVOID)((b'WriteConsoleW', ctypes.windll.kernel32))
1344     written = ctypes.wintypes.DWORD(0)
1345
1346     GetFileType = ctypes.WINFUNCTYPE(ctypes.wintypes.DWORD, ctypes.wintypes.DWORD)((b'GetFileType', ctypes.windll.kernel32))
1347     FILE_TYPE_CHAR = 0x0002
1348     FILE_TYPE_REMOTE = 0x8000
1349     GetConsoleMode = ctypes.WINFUNCTYPE(
1350         ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE,
1351         ctypes.POINTER(ctypes.wintypes.DWORD))(
1352         (b'GetConsoleMode', ctypes.windll.kernel32))
1353     INVALID_HANDLE_VALUE = ctypes.wintypes.DWORD(-1).value
1354
1355     def not_a_console(handle):
1356         if handle == INVALID_HANDLE_VALUE or handle is None:
1357             return True
1358         return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR or
1359                 GetConsoleMode(handle, ctypes.byref(ctypes.wintypes.DWORD())) == 0)
1360
1361     if not_a_console(h):
1362         return False
1363
1364     def next_nonbmp_pos(s):
1365         try:
1366             return next(i for i, c in enumerate(s) if ord(c) > 0xffff)
1367         except StopIteration:
1368             return len(s)
1369
1370     while s:
1371         count = min(next_nonbmp_pos(s), 1024)
1372
1373         ret = WriteConsoleW(
1374             h, s, count if count else 2, ctypes.byref(written), None)
1375         if ret == 0:
1376             raise OSError('Failed to write string')
1377         if not count:  # We just wrote a non-BMP character
1378             assert written.value == 2
1379             s = s[1:]
1380         else:
1381             assert written.value > 0
1382             s = s[written.value:]
1383     return True
1384
1385
1386 def write_string(s, out=None, encoding=None):
1387     if out is None:
1388         out = sys.stderr
1389     assert type(s) == compat_str
1390
1391     if sys.platform == 'win32' and encoding is None and hasattr(out, 'fileno'):
1392         if _windows_write_string(s, out):
1393             return
1394
1395     if ('b' in getattr(out, 'mode', '') or
1396             sys.version_info[0] < 3):  # Python 2 lies about mode of sys.stderr
1397         byt = s.encode(encoding or preferredencoding(), 'ignore')
1398         out.write(byt)
1399     elif hasattr(out, 'buffer'):
1400         enc = encoding or getattr(out, 'encoding', None) or preferredencoding()
1401         byt = s.encode(enc, 'ignore')
1402         out.buffer.write(byt)
1403     else:
1404         out.write(s)
1405     out.flush()
1406
1407
1408 def bytes_to_intlist(bs):
1409     if not bs:
1410         return []
1411     if isinstance(bs[0], int):  # Python 3
1412         return list(bs)
1413     else:
1414         return [ord(c) for c in bs]
1415
1416
1417 def intlist_to_bytes(xs):
1418     if not xs:
1419         return b''
1420     return compat_struct_pack('%dB' % len(xs), *xs)
1421
1422
1423 # Cross-platform file locking
1424 if sys.platform == 'win32':
1425     import ctypes.wintypes
1426     import msvcrt
1427
1428     class OVERLAPPED(ctypes.Structure):
1429         _fields_ = [
1430             ('Internal', ctypes.wintypes.LPVOID),
1431             ('InternalHigh', ctypes.wintypes.LPVOID),
1432             ('Offset', ctypes.wintypes.DWORD),
1433             ('OffsetHigh', ctypes.wintypes.DWORD),
1434             ('hEvent', ctypes.wintypes.HANDLE),
1435         ]
1436
1437     kernel32 = ctypes.windll.kernel32
1438     LockFileEx = kernel32.LockFileEx
1439     LockFileEx.argtypes = [
1440         ctypes.wintypes.HANDLE,     # hFile
1441         ctypes.wintypes.DWORD,      # dwFlags
1442         ctypes.wintypes.DWORD,      # dwReserved
1443         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
1444         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
1445         ctypes.POINTER(OVERLAPPED)  # Overlapped
1446     ]
1447     LockFileEx.restype = ctypes.wintypes.BOOL
1448     UnlockFileEx = kernel32.UnlockFileEx
1449     UnlockFileEx.argtypes = [
1450         ctypes.wintypes.HANDLE,     # hFile
1451         ctypes.wintypes.DWORD,      # dwReserved
1452         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
1453         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
1454         ctypes.POINTER(OVERLAPPED)  # Overlapped
1455     ]
1456     UnlockFileEx.restype = ctypes.wintypes.BOOL
1457     whole_low = 0xffffffff
1458     whole_high = 0x7fffffff
1459
1460     def _lock_file(f, exclusive):
1461         overlapped = OVERLAPPED()
1462         overlapped.Offset = 0
1463         overlapped.OffsetHigh = 0
1464         overlapped.hEvent = 0
1465         f._lock_file_overlapped_p = ctypes.pointer(overlapped)
1466         handle = msvcrt.get_osfhandle(f.fileno())
1467         if not LockFileEx(handle, 0x2 if exclusive else 0x0, 0,
1468                           whole_low, whole_high, f._lock_file_overlapped_p):
1469             raise OSError('Locking file failed: %r' % ctypes.FormatError())
1470
1471     def _unlock_file(f):
1472         assert f._lock_file_overlapped_p
1473         handle = msvcrt.get_osfhandle(f.fileno())
1474         if not UnlockFileEx(handle, 0,
1475                             whole_low, whole_high, f._lock_file_overlapped_p):
1476             raise OSError('Unlocking file failed: %r' % ctypes.FormatError())
1477
1478 else:
1479     # Some platforms, such as Jython, is missing fcntl
1480     try:
1481         import fcntl
1482
1483         def _lock_file(f, exclusive):
1484             fcntl.flock(f, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
1485
1486         def _unlock_file(f):
1487             fcntl.flock(f, fcntl.LOCK_UN)
1488     except ImportError:
1489         UNSUPPORTED_MSG = 'file locking is not supported on this platform'
1490
1491         def _lock_file(f, exclusive):
1492             raise IOError(UNSUPPORTED_MSG)
1493
1494         def _unlock_file(f):
1495             raise IOError(UNSUPPORTED_MSG)
1496
1497
1498 class locked_file(object):
1499     def __init__(self, filename, mode, encoding=None):
1500         assert mode in ['r', 'a', 'w']
1501         self.f = io.open(filename, mode, encoding=encoding)
1502         self.mode = mode
1503
1504     def __enter__(self):
1505         exclusive = self.mode != 'r'
1506         try:
1507             _lock_file(self.f, exclusive)
1508         except IOError:
1509             self.f.close()
1510             raise
1511         return self
1512
1513     def __exit__(self, etype, value, traceback):
1514         try:
1515             _unlock_file(self.f)
1516         finally:
1517             self.f.close()
1518
1519     def __iter__(self):
1520         return iter(self.f)
1521
1522     def write(self, *args):
1523         return self.f.write(*args)
1524
1525     def read(self, *args):
1526         return self.f.read(*args)
1527
1528
1529 def get_filesystem_encoding():
1530     encoding = sys.getfilesystemencoding()
1531     return encoding if encoding is not None else 'utf-8'
1532
1533
1534 def shell_quote(args):
1535     quoted_args = []
1536     encoding = get_filesystem_encoding()
1537     for a in args:
1538         if isinstance(a, bytes):
1539             # We may get a filename encoded with 'encodeFilename'
1540             a = a.decode(encoding)
1541         quoted_args.append(pipes.quote(a))
1542     return ' '.join(quoted_args)
1543
1544
1545 def smuggle_url(url, data):
1546     """ Pass additional data in a URL for internal use. """
1547
1548     url, idata = unsmuggle_url(url, {})
1549     data.update(idata)
1550     sdata = compat_urllib_parse_urlencode(
1551         {'__youtubedl_smuggle': json.dumps(data)})
1552     return url + '#' + sdata
1553
1554
1555 def unsmuggle_url(smug_url, default=None):
1556     if '#__youtubedl_smuggle' not in smug_url:
1557         return smug_url, default
1558     url, _, sdata = smug_url.rpartition('#')
1559     jsond = compat_parse_qs(sdata)['__youtubedl_smuggle'][0]
1560     data = json.loads(jsond)
1561     return url, data
1562
1563
1564 def format_bytes(bytes):
1565     if bytes is None:
1566         return 'N/A'
1567     if type(bytes) is str:
1568         bytes = float(bytes)
1569     if bytes == 0.0:
1570         exponent = 0
1571     else:
1572         exponent = int(math.log(bytes, 1024.0))
1573     suffix = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'][exponent]
1574     converted = float(bytes) / float(1024 ** exponent)
1575     return '%.2f%s' % (converted, suffix)
1576
1577
1578 def lookup_unit_table(unit_table, s):
1579     units_re = '|'.join(re.escape(u) for u in unit_table)
1580     m = re.match(
1581         r'(?P<num>[0-9]+(?:[,.][0-9]*)?)\s*(?P<unit>%s)\b' % units_re, s)
1582     if not m:
1583         return None
1584     num_str = m.group('num').replace(',', '.')
1585     mult = unit_table[m.group('unit')]
1586     return int(float(num_str) * mult)
1587
1588
1589 def parse_filesize(s):
1590     if s is None:
1591         return None
1592
1593     # The lower-case forms are of course incorrect and unofficial,
1594     # but we support those too
1595     _UNIT_TABLE = {
1596         'B': 1,
1597         'b': 1,
1598         'bytes': 1,
1599         'KiB': 1024,
1600         'KB': 1000,
1601         'kB': 1024,
1602         'Kb': 1000,
1603         'kb': 1000,
1604         'kilobytes': 1000,
1605         'kibibytes': 1024,
1606         'MiB': 1024 ** 2,
1607         'MB': 1000 ** 2,
1608         'mB': 1024 ** 2,
1609         'Mb': 1000 ** 2,
1610         'mb': 1000 ** 2,
1611         'megabytes': 1000 ** 2,
1612         'mebibytes': 1024 ** 2,
1613         'GiB': 1024 ** 3,
1614         'GB': 1000 ** 3,
1615         'gB': 1024 ** 3,
1616         'Gb': 1000 ** 3,
1617         'gb': 1000 ** 3,
1618         'gigabytes': 1000 ** 3,
1619         'gibibytes': 1024 ** 3,
1620         'TiB': 1024 ** 4,
1621         'TB': 1000 ** 4,
1622         'tB': 1024 ** 4,
1623         'Tb': 1000 ** 4,
1624         'tb': 1000 ** 4,
1625         'terabytes': 1000 ** 4,
1626         'tebibytes': 1024 ** 4,
1627         'PiB': 1024 ** 5,
1628         'PB': 1000 ** 5,
1629         'pB': 1024 ** 5,
1630         'Pb': 1000 ** 5,
1631         'pb': 1000 ** 5,
1632         'petabytes': 1000 ** 5,
1633         'pebibytes': 1024 ** 5,
1634         'EiB': 1024 ** 6,
1635         'EB': 1000 ** 6,
1636         'eB': 1024 ** 6,
1637         'Eb': 1000 ** 6,
1638         'eb': 1000 ** 6,
1639         'exabytes': 1000 ** 6,
1640         'exbibytes': 1024 ** 6,
1641         'ZiB': 1024 ** 7,
1642         'ZB': 1000 ** 7,
1643         'zB': 1024 ** 7,
1644         'Zb': 1000 ** 7,
1645         'zb': 1000 ** 7,
1646         'zettabytes': 1000 ** 7,
1647         'zebibytes': 1024 ** 7,
1648         'YiB': 1024 ** 8,
1649         'YB': 1000 ** 8,
1650         'yB': 1024 ** 8,
1651         'Yb': 1000 ** 8,
1652         'yb': 1000 ** 8,
1653         'yottabytes': 1000 ** 8,
1654         'yobibytes': 1024 ** 8,
1655     }
1656
1657     return lookup_unit_table(_UNIT_TABLE, s)
1658
1659
1660 def parse_count(s):
1661     if s is None:
1662         return None
1663
1664     s = s.strip()
1665
1666     if re.match(r'^[\d,.]+$', s):
1667         return str_to_int(s)
1668
1669     _UNIT_TABLE = {
1670         'k': 1000,
1671         'K': 1000,
1672         'm': 1000 ** 2,
1673         'M': 1000 ** 2,
1674         'kk': 1000 ** 2,
1675         'KK': 1000 ** 2,
1676     }
1677
1678     return lookup_unit_table(_UNIT_TABLE, s)
1679
1680
1681 def month_by_name(name, lang='en'):
1682     """ Return the number of a month by (locale-independently) English name """
1683
1684     month_names = MONTH_NAMES.get(lang, MONTH_NAMES['en'])
1685
1686     try:
1687         return month_names.index(name) + 1
1688     except ValueError:
1689         return None
1690
1691
1692 def month_by_abbreviation(abbrev):
1693     """ Return the number of a month by (locale-independently) English
1694         abbreviations """
1695
1696     try:
1697         return [s[:3] for s in ENGLISH_MONTH_NAMES].index(abbrev) + 1
1698     except ValueError:
1699         return None
1700
1701
1702 def fix_xml_ampersands(xml_str):
1703     """Replace all the '&' by '&amp;' in XML"""
1704     return re.sub(
1705         r'&(?!amp;|lt;|gt;|apos;|quot;|#x[0-9a-fA-F]{,4};|#[0-9]{,4};)',
1706         '&amp;',
1707         xml_str)
1708
1709
1710 def setproctitle(title):
1711     assert isinstance(title, compat_str)
1712
1713     # ctypes in Jython is not complete
1714     # http://bugs.jython.org/issue2148
1715     if sys.platform.startswith('java'):
1716         return
1717
1718     try:
1719         libc = ctypes.cdll.LoadLibrary('libc.so.6')
1720     except OSError:
1721         return
1722     except TypeError:
1723         # LoadLibrary in Windows Python 2.7.13 only expects
1724         # a bytestring, but since unicode_literals turns
1725         # every string into a unicode string, it fails.
1726         return
1727     title_bytes = title.encode('utf-8')
1728     buf = ctypes.create_string_buffer(len(title_bytes))
1729     buf.value = title_bytes
1730     try:
1731         libc.prctl(15, buf, 0, 0, 0)
1732     except AttributeError:
1733         return  # Strange libc, just skip this
1734
1735
1736 def remove_start(s, start):
1737     return s[len(start):] if s is not None and s.startswith(start) else s
1738
1739
1740 def remove_end(s, end):
1741     return s[:-len(end)] if s is not None and s.endswith(end) else s
1742
1743
1744 def remove_quotes(s):
1745     if s is None or len(s) < 2:
1746         return s
1747     for quote in ('"', "'", ):
1748         if s[0] == quote and s[-1] == quote:
1749             return s[1:-1]
1750     return s
1751
1752
1753 def url_basename(url):
1754     path = compat_urlparse.urlparse(url).path
1755     return path.strip('/').split('/')[-1]
1756
1757
1758 def base_url(url):
1759     return re.match(r'https?://[^?#&]+/', url).group()
1760
1761
1762 def urljoin(base, path):
1763     if isinstance(path, bytes):
1764         path = path.decode('utf-8')
1765     if not isinstance(path, compat_str) or not path:
1766         return None
1767     if re.match(r'^(?:https?:)?//', path):
1768         return path
1769     if isinstance(base, bytes):
1770         base = base.decode('utf-8')
1771     if not isinstance(base, compat_str) or not re.match(
1772             r'^(?:https?:)?//', base):
1773         return None
1774     return compat_urlparse.urljoin(base, path)
1775
1776
1777 class HEADRequest(compat_urllib_request.Request):
1778     def get_method(self):
1779         return 'HEAD'
1780
1781
1782 class PUTRequest(compat_urllib_request.Request):
1783     def get_method(self):
1784         return 'PUT'
1785
1786
1787 def int_or_none(v, scale=1, default=None, get_attr=None, invscale=1):
1788     if get_attr:
1789         if v is not None:
1790             v = getattr(v, get_attr, None)
1791     if v == '':
1792         v = None
1793     if v is None:
1794         return default
1795     try:
1796         return int(v) * invscale // scale
1797     except ValueError:
1798         return default
1799
1800
1801 def str_or_none(v, default=None):
1802     return default if v is None else compat_str(v)
1803
1804
1805 def str_to_int(int_str):
1806     """ A more relaxed version of int_or_none """
1807     if int_str is None:
1808         return None
1809     int_str = re.sub(r'[,\.\+]', '', int_str)
1810     return int(int_str)
1811
1812
1813 def float_or_none(v, scale=1, invscale=1, default=None):
1814     if v is None:
1815         return default
1816     try:
1817         return float(v) * invscale / scale
1818     except ValueError:
1819         return default
1820
1821
1822 def strip_or_none(v):
1823     return None if v is None else v.strip()
1824
1825
1826 def parse_duration(s):
1827     if not isinstance(s, compat_basestring):
1828         return None
1829
1830     s = s.strip()
1831
1832     days, hours, mins, secs, ms = [None] * 5
1833     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)
1834     if m:
1835         days, hours, mins, secs, ms = m.groups()
1836     else:
1837         m = re.match(
1838             r'''(?ix)(?:P?T)?
1839                 (?:
1840                     (?P<days>[0-9]+)\s*d(?:ays?)?\s*
1841                 )?
1842                 (?:
1843                     (?P<hours>[0-9]+)\s*h(?:ours?)?\s*
1844                 )?
1845                 (?:
1846                     (?P<mins>[0-9]+)\s*m(?:in(?:ute)?s?)?\s*
1847                 )?
1848                 (?:
1849                     (?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?\s*s(?:ec(?:ond)?s?)?\s*
1850                 )?Z?$''', s)
1851         if m:
1852             days, hours, mins, secs, ms = m.groups()
1853         else:
1854             m = re.match(r'(?i)(?:(?P<hours>[0-9.]+)\s*(?:hours?)|(?P<mins>[0-9.]+)\s*(?:mins?\.?|minutes?)\s*)Z?$', s)
1855             if m:
1856                 hours, mins = m.groups()
1857             else:
1858                 return None
1859
1860     duration = 0
1861     if secs:
1862         duration += float(secs)
1863     if mins:
1864         duration += float(mins) * 60
1865     if hours:
1866         duration += float(hours) * 60 * 60
1867     if days:
1868         duration += float(days) * 24 * 60 * 60
1869     if ms:
1870         duration += float(ms)
1871     return duration
1872
1873
1874 def prepend_extension(filename, ext, expected_real_ext=None):
1875     name, real_ext = os.path.splitext(filename)
1876     return (
1877         '{0}.{1}{2}'.format(name, ext, real_ext)
1878         if not expected_real_ext or real_ext[1:] == expected_real_ext
1879         else '{0}.{1}'.format(filename, ext))
1880
1881
1882 def replace_extension(filename, ext, expected_real_ext=None):
1883     name, real_ext = os.path.splitext(filename)
1884     return '{0}.{1}'.format(
1885         name if not expected_real_ext or real_ext[1:] == expected_real_ext else filename,
1886         ext)
1887
1888
1889 def check_executable(exe, args=[]):
1890     """ Checks if the given binary is installed somewhere in PATH, and returns its name.
1891     args can be a list of arguments for a short output (like -version) """
1892     try:
1893         subprocess.Popen([exe] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
1894     except OSError:
1895         return False
1896     return exe
1897
1898
1899 def get_exe_version(exe, args=['--version'],
1900                     version_re=None, unrecognized='present'):
1901     """ Returns the version of the specified executable,
1902     or False if the executable is not present """
1903     try:
1904         # STDIN should be redirected too. On UNIX-like systems, ffmpeg triggers
1905         # SIGTTOU if youtube-dl is run in the background.
1906         # See https://github.com/rg3/youtube-dl/issues/955#issuecomment-209789656
1907         out, _ = subprocess.Popen(
1908             [encodeArgument(exe)] + args,
1909             stdin=subprocess.PIPE,
1910             stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()
1911     except OSError:
1912         return False
1913     if isinstance(out, bytes):  # Python 2.x
1914         out = out.decode('ascii', 'ignore')
1915     return detect_exe_version(out, version_re, unrecognized)
1916
1917
1918 def detect_exe_version(output, version_re=None, unrecognized='present'):
1919     assert isinstance(output, compat_str)
1920     if version_re is None:
1921         version_re = r'version\s+([-0-9._a-zA-Z]+)'
1922     m = re.search(version_re, output)
1923     if m:
1924         return m.group(1)
1925     else:
1926         return unrecognized
1927
1928
1929 class PagedList(object):
1930     def __len__(self):
1931         # This is only useful for tests
1932         return len(self.getslice())
1933
1934
1935 class OnDemandPagedList(PagedList):
1936     def __init__(self, pagefunc, pagesize, use_cache=False):
1937         self._pagefunc = pagefunc
1938         self._pagesize = pagesize
1939         self._use_cache = use_cache
1940         if use_cache:
1941             self._cache = {}
1942
1943     def getslice(self, start=0, end=None):
1944         res = []
1945         for pagenum in itertools.count(start // self._pagesize):
1946             firstid = pagenum * self._pagesize
1947             nextfirstid = pagenum * self._pagesize + self._pagesize
1948             if start >= nextfirstid:
1949                 continue
1950
1951             page_results = None
1952             if self._use_cache:
1953                 page_results = self._cache.get(pagenum)
1954             if page_results is None:
1955                 page_results = list(self._pagefunc(pagenum))
1956             if self._use_cache:
1957                 self._cache[pagenum] = page_results
1958
1959             startv = (
1960                 start % self._pagesize
1961                 if firstid <= start < nextfirstid
1962                 else 0)
1963
1964             endv = (
1965                 ((end - 1) % self._pagesize) + 1
1966                 if (end is not None and firstid <= end <= nextfirstid)
1967                 else None)
1968
1969             if startv != 0 or endv is not None:
1970                 page_results = page_results[startv:endv]
1971             res.extend(page_results)
1972
1973             # A little optimization - if current page is not "full", ie. does
1974             # not contain page_size videos then we can assume that this page
1975             # is the last one - there are no more ids on further pages -
1976             # i.e. no need to query again.
1977             if len(page_results) + startv < self._pagesize:
1978                 break
1979
1980             # If we got the whole page, but the next page is not interesting,
1981             # break out early as well
1982             if end == nextfirstid:
1983                 break
1984         return res
1985
1986
1987 class InAdvancePagedList(PagedList):
1988     def __init__(self, pagefunc, pagecount, pagesize):
1989         self._pagefunc = pagefunc
1990         self._pagecount = pagecount
1991         self._pagesize = pagesize
1992
1993     def getslice(self, start=0, end=None):
1994         res = []
1995         start_page = start // self._pagesize
1996         end_page = (
1997             self._pagecount if end is None else (end // self._pagesize + 1))
1998         skip_elems = start - start_page * self._pagesize
1999         only_more = None if end is None else end - start
2000         for pagenum in range(start_page, end_page):
2001             page = list(self._pagefunc(pagenum))
2002             if skip_elems:
2003                 page = page[skip_elems:]
2004                 skip_elems = None
2005             if only_more is not None:
2006                 if len(page) < only_more:
2007                     only_more -= len(page)
2008                 else:
2009                     page = page[:only_more]
2010                     res.extend(page)
2011                     break
2012             res.extend(page)
2013         return res
2014
2015
2016 def uppercase_escape(s):
2017     unicode_escape = codecs.getdecoder('unicode_escape')
2018     return re.sub(
2019         r'\\U[0-9a-fA-F]{8}',
2020         lambda m: unicode_escape(m.group(0))[0],
2021         s)
2022
2023
2024 def lowercase_escape(s):
2025     unicode_escape = codecs.getdecoder('unicode_escape')
2026     return re.sub(
2027         r'\\u[0-9a-fA-F]{4}',
2028         lambda m: unicode_escape(m.group(0))[0],
2029         s)
2030
2031
2032 def escape_rfc3986(s):
2033     """Escape non-ASCII characters as suggested by RFC 3986"""
2034     if sys.version_info < (3, 0) and isinstance(s, compat_str):
2035         s = s.encode('utf-8')
2036     return compat_urllib_parse.quote(s, b"%/;:@&=+$,!~*'()?#[]")
2037
2038
2039 def escape_url(url):
2040     """Escape URL as suggested by RFC 3986"""
2041     url_parsed = compat_urllib_parse_urlparse(url)
2042     return url_parsed._replace(
2043         netloc=url_parsed.netloc.encode('idna').decode('ascii'),
2044         path=escape_rfc3986(url_parsed.path),
2045         params=escape_rfc3986(url_parsed.params),
2046         query=escape_rfc3986(url_parsed.query),
2047         fragment=escape_rfc3986(url_parsed.fragment)
2048     ).geturl()
2049
2050
2051 def read_batch_urls(batch_fd):
2052     def fixup(url):
2053         if not isinstance(url, compat_str):
2054             url = url.decode('utf-8', 'replace')
2055         BOM_UTF8 = '\xef\xbb\xbf'
2056         if url.startswith(BOM_UTF8):
2057             url = url[len(BOM_UTF8):]
2058         url = url.strip()
2059         if url.startswith(('#', ';', ']')):
2060             return False
2061         return url
2062
2063     with contextlib.closing(batch_fd) as fd:
2064         return [url for url in map(fixup, fd) if url]
2065
2066
2067 def urlencode_postdata(*args, **kargs):
2068     return compat_urllib_parse_urlencode(*args, **kargs).encode('ascii')
2069
2070
2071 def update_url_query(url, query):
2072     if not query:
2073         return url
2074     parsed_url = compat_urlparse.urlparse(url)
2075     qs = compat_parse_qs(parsed_url.query)
2076     qs.update(query)
2077     return compat_urlparse.urlunparse(parsed_url._replace(
2078         query=compat_urllib_parse_urlencode(qs, True)))
2079
2080
2081 def update_Request(req, url=None, data=None, headers={}, query={}):
2082     req_headers = req.headers.copy()
2083     req_headers.update(headers)
2084     req_data = data or req.data
2085     req_url = update_url_query(url or req.get_full_url(), query)
2086     req_get_method = req.get_method()
2087     if req_get_method == 'HEAD':
2088         req_type = HEADRequest
2089     elif req_get_method == 'PUT':
2090         req_type = PUTRequest
2091     else:
2092         req_type = compat_urllib_request.Request
2093     new_req = req_type(
2094         req_url, data=req_data, headers=req_headers,
2095         origin_req_host=req.origin_req_host, unverifiable=req.unverifiable)
2096     if hasattr(req, 'timeout'):
2097         new_req.timeout = req.timeout
2098     return new_req
2099
2100
2101 def _multipart_encode_impl(data, boundary):
2102     content_type = 'multipart/form-data; boundary=%s' % boundary
2103
2104     out = b''
2105     for k, v in data.items():
2106         out += b'--' + boundary.encode('ascii') + b'\r\n'
2107         if isinstance(k, compat_str):
2108             k = k.encode('utf-8')
2109         if isinstance(v, compat_str):
2110             v = v.encode('utf-8')
2111         # RFC 2047 requires non-ASCII field names to be encoded, while RFC 7578
2112         # suggests sending UTF-8 directly. Firefox sends UTF-8, too
2113         content = b'Content-Disposition: form-data; name="' + k + b'"\r\n\r\n' + v + b'\r\n'
2114         if boundary.encode('ascii') in content:
2115             raise ValueError('Boundary overlaps with data')
2116         out += content
2117
2118     out += b'--' + boundary.encode('ascii') + b'--\r\n'
2119
2120     return out, content_type
2121
2122
2123 def multipart_encode(data, boundary=None):
2124     '''
2125     Encode a dict to RFC 7578-compliant form-data
2126
2127     data:
2128         A dict where keys and values can be either Unicode or bytes-like
2129         objects.
2130     boundary:
2131         If specified a Unicode object, it's used as the boundary. Otherwise
2132         a random boundary is generated.
2133
2134     Reference: https://tools.ietf.org/html/rfc7578
2135     '''
2136     has_specified_boundary = boundary is not None
2137
2138     while True:
2139         if boundary is None:
2140             boundary = '---------------' + str(random.randrange(0x0fffffff, 0xffffffff))
2141
2142         try:
2143             out, content_type = _multipart_encode_impl(data, boundary)
2144             break
2145         except ValueError:
2146             if has_specified_boundary:
2147                 raise
2148             boundary = None
2149
2150     return out, content_type
2151
2152
2153 def dict_get(d, key_or_keys, default=None, skip_false_values=True):
2154     if isinstance(key_or_keys, (list, tuple)):
2155         for key in key_or_keys:
2156             if key not in d or d[key] is None or skip_false_values and not d[key]:
2157                 continue
2158             return d[key]
2159         return default
2160     return d.get(key_or_keys, default)
2161
2162
2163 def try_get(src, getter, expected_type=None):
2164     if not isinstance(getter, (list, tuple)):
2165         getter = [getter]
2166     for get in getter:
2167         try:
2168             v = get(src)
2169         except (AttributeError, KeyError, TypeError, IndexError):
2170             pass
2171         else:
2172             if expected_type is None or isinstance(v, expected_type):
2173                 return v
2174
2175
2176 def encode_compat_str(string, encoding=preferredencoding(), errors='strict'):
2177     return string if isinstance(string, compat_str) else compat_str(string, encoding, errors)
2178
2179
2180 US_RATINGS = {
2181     'G': 0,
2182     'PG': 10,
2183     'PG-13': 13,
2184     'R': 16,
2185     'NC': 18,
2186 }
2187
2188
2189 TV_PARENTAL_GUIDELINES = {
2190     'TV-Y': 0,
2191     'TV-Y7': 7,
2192     'TV-G': 0,
2193     'TV-PG': 0,
2194     'TV-14': 14,
2195     'TV-MA': 17,
2196 }
2197
2198
2199 def parse_age_limit(s):
2200     if type(s) == int:
2201         return s if 0 <= s <= 21 else None
2202     if not isinstance(s, compat_basestring):
2203         return None
2204     m = re.match(r'^(?P<age>\d{1,2})\+?$', s)
2205     if m:
2206         return int(m.group('age'))
2207     if s in US_RATINGS:
2208         return US_RATINGS[s]
2209     return TV_PARENTAL_GUIDELINES.get(s)
2210
2211
2212 def strip_jsonp(code):
2213     return re.sub(
2214         r'''(?sx)^
2215             (?:window\.)?(?P<func_name>[a-zA-Z0-9_.$]+)
2216             (?:\s*&&\s*(?P=func_name))?
2217             \s*\(\s*(?P<callback_data>.*)\);?
2218             \s*?(?://[^\n]*)*$''',
2219         r'\g<callback_data>', code)
2220
2221
2222 def js_to_json(code):
2223     COMMENT_RE = r'/\*(?:(?!\*/).)*?\*/|//[^\n]*'
2224     SKIP_RE = r'\s*(?:{comment})?\s*'.format(comment=COMMENT_RE)
2225     INTEGER_TABLE = (
2226         (r'(?s)^(0[xX][0-9a-fA-F]+){skip}:?$'.format(skip=SKIP_RE), 16),
2227         (r'(?s)^(0+[0-7]+){skip}:?$'.format(skip=SKIP_RE), 8),
2228     )
2229
2230     def fix_kv(m):
2231         v = m.group(0)
2232         if v in ('true', 'false', 'null'):
2233             return v
2234         elif v.startswith('/*') or v.startswith('//') or v == ',':
2235             return ""
2236
2237         if v[0] in ("'", '"'):
2238             v = re.sub(r'(?s)\\.|"', lambda m: {
2239                 '"': '\\"',
2240                 "\\'": "'",
2241                 '\\\n': '',
2242                 '\\x': '\\u00',
2243             }.get(m.group(0), m.group(0)), v[1:-1])
2244
2245         for regex, base in INTEGER_TABLE:
2246             im = re.match(regex, v)
2247             if im:
2248                 i = int(im.group(1), base)
2249                 return '"%d":' % i if v.endswith(':') else '%d' % i
2250
2251         return '"%s"' % v
2252
2253     return re.sub(r'''(?sx)
2254         "(?:[^"\\]*(?:\\\\|\\['"nurtbfx/\n]))*[^"\\]*"|
2255         '(?:[^'\\]*(?:\\\\|\\['"nurtbfx/\n]))*[^'\\]*'|
2256         {comment}|,(?={skip}[\]}}])|
2257         [a-zA-Z_][.a-zA-Z_0-9]*|
2258         \b(?:0[xX][0-9a-fA-F]+|0+[0-7]+)(?:{skip}:)?|
2259         [0-9]+(?={skip}:)
2260         '''.format(comment=COMMENT_RE, skip=SKIP_RE), fix_kv, code)
2261
2262
2263 def qualities(quality_ids):
2264     """ Get a numeric quality value out of a list of possible values """
2265     def q(qid):
2266         try:
2267             return quality_ids.index(qid)
2268         except ValueError:
2269             return -1
2270     return q
2271
2272
2273 DEFAULT_OUTTMPL = '%(title)s-%(id)s.%(ext)s'
2274
2275
2276 def limit_length(s, length):
2277     """ Add ellipses to overly long strings """
2278     if s is None:
2279         return None
2280     ELLIPSES = '...'
2281     if len(s) > length:
2282         return s[:length - len(ELLIPSES)] + ELLIPSES
2283     return s
2284
2285
2286 def version_tuple(v):
2287     return tuple(int(e) for e in re.split(r'[-.]', v))
2288
2289
2290 def is_outdated_version(version, limit, assume_new=True):
2291     if not version:
2292         return not assume_new
2293     try:
2294         return version_tuple(version) < version_tuple(limit)
2295     except ValueError:
2296         return not assume_new
2297
2298
2299 def ytdl_is_updateable():
2300     """ Returns if youtube-dl can be updated with -U """
2301     from zipimport import zipimporter
2302
2303     return isinstance(globals().get('__loader__'), zipimporter) or hasattr(sys, 'frozen')
2304
2305
2306 def args_to_str(args):
2307     # Get a short string representation for a subprocess command
2308     return ' '.join(compat_shlex_quote(a) for a in args)
2309
2310
2311 def error_to_compat_str(err):
2312     err_str = str(err)
2313     # On python 2 error byte string must be decoded with proper
2314     # encoding rather than ascii
2315     if sys.version_info[0] < 3:
2316         err_str = err_str.decode(preferredencoding())
2317     return err_str
2318
2319
2320 def mimetype2ext(mt):
2321     if mt is None:
2322         return None
2323
2324     ext = {
2325         'audio/mp4': 'm4a',
2326         # Per RFC 3003, audio/mpeg can be .mp1, .mp2 or .mp3. Here use .mp3 as
2327         # it's the most popular one
2328         'audio/mpeg': 'mp3',
2329     }.get(mt)
2330     if ext is not None:
2331         return ext
2332
2333     _, _, res = mt.rpartition('/')
2334     res = res.split(';')[0].strip().lower()
2335
2336     return {
2337         '3gpp': '3gp',
2338         'smptett+xml': 'tt',
2339         'ttaf+xml': 'dfxp',
2340         'ttml+xml': 'ttml',
2341         'x-flv': 'flv',
2342         'x-mp4-fragmented': 'mp4',
2343         'x-ms-wmv': 'wmv',
2344         'mpegurl': 'm3u8',
2345         'x-mpegurl': 'm3u8',
2346         'vnd.apple.mpegurl': 'm3u8',
2347         'dash+xml': 'mpd',
2348         'f4m+xml': 'f4m',
2349         'hds+xml': 'f4m',
2350         'vnd.ms-sstr+xml': 'ism',
2351         'quicktime': 'mov',
2352         'mp2t': 'ts',
2353     }.get(res, res)
2354
2355
2356 def parse_codecs(codecs_str):
2357     # http://tools.ietf.org/html/rfc6381
2358     if not codecs_str:
2359         return {}
2360     splited_codecs = list(filter(None, map(
2361         lambda str: str.strip(), codecs_str.strip().strip(',').split(','))))
2362     vcodec, acodec = None, None
2363     for full_codec in splited_codecs:
2364         codec = full_codec.split('.')[0]
2365         if codec in ('avc1', 'avc2', 'avc3', 'avc4', 'vp9', 'vp8', 'hev1', 'hev2', 'h263', 'h264', 'mp4v'):
2366             if not vcodec:
2367                 vcodec = full_codec
2368         elif codec in ('mp4a', 'opus', 'vorbis', 'mp3', 'aac', 'ac-3', 'ec-3', 'eac3', 'dtsc', 'dtse', 'dtsh', 'dtsl'):
2369             if not acodec:
2370                 acodec = full_codec
2371         else:
2372             write_string('WARNING: Unknown codec %s\n' % full_codec, sys.stderr)
2373     if not vcodec and not acodec:
2374         if len(splited_codecs) == 2:
2375             return {
2376                 'vcodec': vcodec,
2377                 'acodec': acodec,
2378             }
2379         elif len(splited_codecs) == 1:
2380             return {
2381                 'vcodec': 'none',
2382                 'acodec': vcodec,
2383             }
2384     else:
2385         return {
2386             'vcodec': vcodec or 'none',
2387             'acodec': acodec or 'none',
2388         }
2389     return {}
2390
2391
2392 def urlhandle_detect_ext(url_handle):
2393     getheader = url_handle.headers.get
2394
2395     cd = getheader('Content-Disposition')
2396     if cd:
2397         m = re.match(r'attachment;\s*filename="(?P<filename>[^"]+)"', cd)
2398         if m:
2399             e = determine_ext(m.group('filename'), default_ext=None)
2400             if e:
2401                 return e
2402
2403     return mimetype2ext(getheader('Content-Type'))
2404
2405
2406 def encode_data_uri(data, mime_type):
2407     return 'data:%s;base64,%s' % (mime_type, base64.b64encode(data).decode('ascii'))
2408
2409
2410 def age_restricted(content_limit, age_limit):
2411     """ Returns True iff the content should be blocked """
2412
2413     if age_limit is None:  # No limit set
2414         return False
2415     if content_limit is None:
2416         return False  # Content available for everyone
2417     return age_limit < content_limit
2418
2419
2420 def is_html(first_bytes):
2421     """ Detect whether a file contains HTML by examining its first bytes. """
2422
2423     BOMS = [
2424         (b'\xef\xbb\xbf', 'utf-8'),
2425         (b'\x00\x00\xfe\xff', 'utf-32-be'),
2426         (b'\xff\xfe\x00\x00', 'utf-32-le'),
2427         (b'\xff\xfe', 'utf-16-le'),
2428         (b'\xfe\xff', 'utf-16-be'),
2429     ]
2430     for bom, enc in BOMS:
2431         if first_bytes.startswith(bom):
2432             s = first_bytes[len(bom):].decode(enc, 'replace')
2433             break
2434     else:
2435         s = first_bytes.decode('utf-8', 'replace')
2436
2437     return re.match(r'^\s*<', s)
2438
2439
2440 def determine_protocol(info_dict):
2441     protocol = info_dict.get('protocol')
2442     if protocol is not None:
2443         return protocol
2444
2445     url = info_dict['url']
2446     if url.startswith('rtmp'):
2447         return 'rtmp'
2448     elif url.startswith('mms'):
2449         return 'mms'
2450     elif url.startswith('rtsp'):
2451         return 'rtsp'
2452
2453     ext = determine_ext(url)
2454     if ext == 'm3u8':
2455         return 'm3u8'
2456     elif ext == 'f4m':
2457         return 'f4m'
2458
2459     return compat_urllib_parse_urlparse(url).scheme
2460
2461
2462 def render_table(header_row, data):
2463     """ Render a list of rows, each as a list of values """
2464     table = [header_row] + data
2465     max_lens = [max(len(compat_str(v)) for v in col) for col in zip(*table)]
2466     format_str = ' '.join('%-' + compat_str(ml + 1) + 's' for ml in max_lens[:-1]) + '%s'
2467     return '\n'.join(format_str % tuple(row) for row in table)
2468
2469
2470 def _match_one(filter_part, dct):
2471     COMPARISON_OPERATORS = {
2472         '<': operator.lt,
2473         '<=': operator.le,
2474         '>': operator.gt,
2475         '>=': operator.ge,
2476         '=': operator.eq,
2477         '!=': operator.ne,
2478     }
2479     operator_rex = re.compile(r'''(?x)\s*
2480         (?P<key>[a-z_]+)
2481         \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
2482         (?:
2483             (?P<intval>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)|
2484             (?P<quote>["\'])(?P<quotedstrval>(?:\\.|(?!(?P=quote)|\\).)+?)(?P=quote)|
2485             (?P<strval>(?![0-9.])[a-z0-9A-Z]*)
2486         )
2487         \s*$
2488         ''' % '|'.join(map(re.escape, COMPARISON_OPERATORS.keys())))
2489     m = operator_rex.search(filter_part)
2490     if m:
2491         op = COMPARISON_OPERATORS[m.group('op')]
2492         actual_value = dct.get(m.group('key'))
2493         if (m.group('quotedstrval') is not None or
2494             m.group('strval') is not None or
2495             # If the original field is a string and matching comparisonvalue is
2496             # a number we should respect the origin of the original field
2497             # and process comparison value as a string (see
2498             # https://github.com/rg3/youtube-dl/issues/11082).
2499             actual_value is not None and m.group('intval') is not None and
2500                 isinstance(actual_value, compat_str)):
2501             if m.group('op') not in ('=', '!='):
2502                 raise ValueError(
2503                     'Operator %s does not support string values!' % m.group('op'))
2504             comparison_value = m.group('quotedstrval') or m.group('strval') or m.group('intval')
2505             quote = m.group('quote')
2506             if quote is not None:
2507                 comparison_value = comparison_value.replace(r'\%s' % quote, quote)
2508         else:
2509             try:
2510                 comparison_value = int(m.group('intval'))
2511             except ValueError:
2512                 comparison_value = parse_filesize(m.group('intval'))
2513                 if comparison_value is None:
2514                     comparison_value = parse_filesize(m.group('intval') + 'B')
2515                 if comparison_value is None:
2516                     raise ValueError(
2517                         'Invalid integer value %r in filter part %r' % (
2518                             m.group('intval'), filter_part))
2519         if actual_value is None:
2520             return m.group('none_inclusive')
2521         return op(actual_value, comparison_value)
2522
2523     UNARY_OPERATORS = {
2524         '': lambda v: v is not None,
2525         '!': lambda v: v is None,
2526     }
2527     operator_rex = re.compile(r'''(?x)\s*
2528         (?P<op>%s)\s*(?P<key>[a-z_]+)
2529         \s*$
2530         ''' % '|'.join(map(re.escape, UNARY_OPERATORS.keys())))
2531     m = operator_rex.search(filter_part)
2532     if m:
2533         op = UNARY_OPERATORS[m.group('op')]
2534         actual_value = dct.get(m.group('key'))
2535         return op(actual_value)
2536
2537     raise ValueError('Invalid filter part %r' % filter_part)
2538
2539
2540 def match_str(filter_str, dct):
2541     """ Filter a dictionary with a simple string syntax. Returns True (=passes filter) or false """
2542
2543     return all(
2544         _match_one(filter_part, dct) for filter_part in filter_str.split('&'))
2545
2546
2547 def match_filter_func(filter_str):
2548     def _match_func(info_dict):
2549         if match_str(filter_str, info_dict):
2550             return None
2551         else:
2552             video_title = info_dict.get('title', info_dict.get('id', 'video'))
2553             return '%s does not pass filter %s, skipping ..' % (video_title, filter_str)
2554     return _match_func
2555
2556
2557 def parse_dfxp_time_expr(time_expr):
2558     if not time_expr:
2559         return
2560
2561     mobj = re.match(r'^(?P<time_offset>\d+(?:\.\d+)?)s?$', time_expr)
2562     if mobj:
2563         return float(mobj.group('time_offset'))
2564
2565     mobj = re.match(r'^(\d+):(\d\d):(\d\d(?:(?:\.|:)\d+)?)$', time_expr)
2566     if mobj:
2567         return 3600 * int(mobj.group(1)) + 60 * int(mobj.group(2)) + float(mobj.group(3).replace(':', '.'))
2568
2569
2570 def srt_subtitles_timecode(seconds):
2571     return '%02d:%02d:%02d,%03d' % (seconds / 3600, (seconds % 3600) / 60, seconds % 60, (seconds % 1) * 1000)
2572
2573
2574 def dfxp2srt(dfxp_data):
2575     LEGACY_NAMESPACES = (
2576         ('http://www.w3.org/ns/ttml', [
2577             'http://www.w3.org/2004/11/ttaf1',
2578             'http://www.w3.org/2006/04/ttaf1',
2579             'http://www.w3.org/2006/10/ttaf1',
2580         ]),
2581         ('http://www.w3.org/ns/ttml#styling', [
2582             'http://www.w3.org/ns/ttml#style',
2583         ]),
2584     )
2585
2586     SUPPORTED_STYLING = [
2587         'color',
2588         'fontFamily',
2589         'fontSize',
2590         'fontStyle',
2591         'fontWeight',
2592         'textDecoration'
2593     ]
2594
2595     _x = functools.partial(xpath_with_ns, ns_map={
2596         'ttml': 'http://www.w3.org/ns/ttml',
2597         'tts': 'http://www.w3.org/ns/ttml#styling',
2598     })
2599
2600     styles = {}
2601     default_style = {}
2602
2603     class TTMLPElementParser(object):
2604         _out = ''
2605         _unclosed_elements = []
2606         _applied_styles = []
2607
2608         def start(self, tag, attrib):
2609             if tag in (_x('ttml:br'), 'br'):
2610                 self._out += '\n'
2611             else:
2612                 unclosed_elements = []
2613                 style = {}
2614                 element_style_id = attrib.get('style')
2615                 if default_style:
2616                     style.update(default_style)
2617                 if element_style_id:
2618                     style.update(styles.get(element_style_id, {}))
2619                 for prop in SUPPORTED_STYLING:
2620                     prop_val = attrib.get(_x('tts:' + prop))
2621                     if prop_val:
2622                         style[prop] = prop_val
2623                 if style:
2624                     font = ''
2625                     for k, v in sorted(style.items()):
2626                         if self._applied_styles and self._applied_styles[-1].get(k) == v:
2627                             continue
2628                         if k == 'color':
2629                             font += ' color="%s"' % v
2630                         elif k == 'fontSize':
2631                             font += ' size="%s"' % v
2632                         elif k == 'fontFamily':
2633                             font += ' face="%s"' % v
2634                         elif k == 'fontWeight' and v == 'bold':
2635                             self._out += '<b>'
2636                             unclosed_elements.append('b')
2637                         elif k == 'fontStyle' and v == 'italic':
2638                             self._out += '<i>'
2639                             unclosed_elements.append('i')
2640                         elif k == 'textDecoration' and v == 'underline':
2641                             self._out += '<u>'
2642                             unclosed_elements.append('u')
2643                     if font:
2644                         self._out += '<font' + font + '>'
2645                         unclosed_elements.append('font')
2646                     applied_style = {}
2647                     if self._applied_styles:
2648                         applied_style.update(self._applied_styles[-1])
2649                     applied_style.update(style)
2650                     self._applied_styles.append(applied_style)
2651                 self._unclosed_elements.append(unclosed_elements)
2652
2653         def end(self, tag):
2654             if tag not in (_x('ttml:br'), 'br'):
2655                 unclosed_elements = self._unclosed_elements.pop()
2656                 for element in reversed(unclosed_elements):
2657                     self._out += '</%s>' % element
2658                 if unclosed_elements and self._applied_styles:
2659                     self._applied_styles.pop()
2660
2661         def data(self, data):
2662             self._out += data
2663
2664         def close(self):
2665             return self._out.strip()
2666
2667     def parse_node(node):
2668         target = TTMLPElementParser()
2669         parser = xml.etree.ElementTree.XMLParser(target=target)
2670         parser.feed(xml.etree.ElementTree.tostring(node))
2671         return parser.close()
2672
2673     for k, v in LEGACY_NAMESPACES:
2674         for ns in v:
2675             dfxp_data = dfxp_data.replace(ns, k)
2676
2677     dfxp = compat_etree_fromstring(dfxp_data.encode('utf-8'))
2678     out = []
2679     paras = dfxp.findall(_x('.//ttml:p')) or dfxp.findall('.//p')
2680
2681     if not paras:
2682         raise ValueError('Invalid dfxp/TTML subtitle')
2683
2684     repeat = False
2685     while True:
2686         for style in dfxp.findall(_x('.//ttml:style')):
2687             style_id = style.get('id')
2688             parent_style_id = style.get('style')
2689             if parent_style_id:
2690                 if parent_style_id not in styles:
2691                     repeat = True
2692                     continue
2693                 styles[style_id] = styles[parent_style_id].copy()
2694             for prop in SUPPORTED_STYLING:
2695                 prop_val = style.get(_x('tts:' + prop))
2696                 if prop_val:
2697                     styles.setdefault(style_id, {})[prop] = prop_val
2698         if repeat:
2699             repeat = False
2700         else:
2701             break
2702
2703     for p in ('body', 'div'):
2704         ele = xpath_element(dfxp, [_x('.//ttml:' + p), './/' + p])
2705         if ele is None:
2706             continue
2707         style = styles.get(ele.get('style'))
2708         if not style:
2709             continue
2710         default_style.update(style)
2711
2712     for para, index in zip(paras, itertools.count(1)):
2713         begin_time = parse_dfxp_time_expr(para.attrib.get('begin'))
2714         end_time = parse_dfxp_time_expr(para.attrib.get('end'))
2715         dur = parse_dfxp_time_expr(para.attrib.get('dur'))
2716         if begin_time is None:
2717             continue
2718         if not end_time:
2719             if not dur:
2720                 continue
2721             end_time = begin_time + dur
2722         out.append('%d\n%s --> %s\n%s\n\n' % (
2723             index,
2724             srt_subtitles_timecode(begin_time),
2725             srt_subtitles_timecode(end_time),
2726             parse_node(para)))
2727
2728     return ''.join(out)
2729
2730
2731 def cli_option(params, command_option, param):
2732     param = params.get(param)
2733     if param:
2734         param = compat_str(param)
2735     return [command_option, param] if param is not None else []
2736
2737
2738 def cli_bool_option(params, command_option, param, true_value='true', false_value='false', separator=None):
2739     param = params.get(param)
2740     assert isinstance(param, bool)
2741     if separator:
2742         return [command_option + separator + (true_value if param else false_value)]
2743     return [command_option, true_value if param else false_value]
2744
2745
2746 def cli_valueless_option(params, command_option, param, expected_value=True):
2747     param = params.get(param)
2748     return [command_option] if param == expected_value else []
2749
2750
2751 def cli_configuration_args(params, param, default=[]):
2752     ex_args = params.get(param)
2753     if ex_args is None:
2754         return default
2755     assert isinstance(ex_args, list)
2756     return ex_args
2757
2758
2759 class ISO639Utils(object):
2760     # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
2761     _lang_map = {
2762         'aa': 'aar',
2763         'ab': 'abk',
2764         'ae': 'ave',
2765         'af': 'afr',
2766         'ak': 'aka',
2767         'am': 'amh',
2768         'an': 'arg',
2769         'ar': 'ara',
2770         'as': 'asm',
2771         'av': 'ava',
2772         'ay': 'aym',
2773         'az': 'aze',
2774         'ba': 'bak',
2775         'be': 'bel',
2776         'bg': 'bul',
2777         'bh': 'bih',
2778         'bi': 'bis',
2779         'bm': 'bam',
2780         'bn': 'ben',
2781         'bo': 'bod',
2782         'br': 'bre',
2783         'bs': 'bos',
2784         'ca': 'cat',
2785         'ce': 'che',
2786         'ch': 'cha',
2787         'co': 'cos',
2788         'cr': 'cre',
2789         'cs': 'ces',
2790         'cu': 'chu',
2791         'cv': 'chv',
2792         'cy': 'cym',
2793         'da': 'dan',
2794         'de': 'deu',
2795         'dv': 'div',
2796         'dz': 'dzo',
2797         'ee': 'ewe',
2798         'el': 'ell',
2799         'en': 'eng',
2800         'eo': 'epo',
2801         'es': 'spa',
2802         'et': 'est',
2803         'eu': 'eus',
2804         'fa': 'fas',
2805         'ff': 'ful',
2806         'fi': 'fin',
2807         'fj': 'fij',
2808         'fo': 'fao',
2809         'fr': 'fra',
2810         'fy': 'fry',
2811         'ga': 'gle',
2812         'gd': 'gla',
2813         'gl': 'glg',
2814         'gn': 'grn',
2815         'gu': 'guj',
2816         'gv': 'glv',
2817         'ha': 'hau',
2818         'he': 'heb',
2819         'hi': 'hin',
2820         'ho': 'hmo',
2821         'hr': 'hrv',
2822         'ht': 'hat',
2823         'hu': 'hun',
2824         'hy': 'hye',
2825         'hz': 'her',
2826         'ia': 'ina',
2827         'id': 'ind',
2828         'ie': 'ile',
2829         'ig': 'ibo',
2830         'ii': 'iii',
2831         'ik': 'ipk',
2832         'io': 'ido',
2833         'is': 'isl',
2834         'it': 'ita',
2835         'iu': 'iku',
2836         'ja': 'jpn',
2837         'jv': 'jav',
2838         'ka': 'kat',
2839         'kg': 'kon',
2840         'ki': 'kik',
2841         'kj': 'kua',
2842         'kk': 'kaz',
2843         'kl': 'kal',
2844         'km': 'khm',
2845         'kn': 'kan',
2846         'ko': 'kor',
2847         'kr': 'kau',
2848         'ks': 'kas',
2849         'ku': 'kur',
2850         'kv': 'kom',
2851         'kw': 'cor',
2852         'ky': 'kir',
2853         'la': 'lat',
2854         'lb': 'ltz',
2855         'lg': 'lug',
2856         'li': 'lim',
2857         'ln': 'lin',
2858         'lo': 'lao',
2859         'lt': 'lit',
2860         'lu': 'lub',
2861         'lv': 'lav',
2862         'mg': 'mlg',
2863         'mh': 'mah',
2864         'mi': 'mri',
2865         'mk': 'mkd',
2866         'ml': 'mal',
2867         'mn': 'mon',
2868         'mr': 'mar',
2869         'ms': 'msa',
2870         'mt': 'mlt',
2871         'my': 'mya',
2872         'na': 'nau',
2873         'nb': 'nob',
2874         'nd': 'nde',
2875         'ne': 'nep',
2876         'ng': 'ndo',
2877         'nl': 'nld',
2878         'nn': 'nno',
2879         'no': 'nor',
2880         'nr': 'nbl',
2881         'nv': 'nav',
2882         'ny': 'nya',
2883         'oc': 'oci',
2884         'oj': 'oji',
2885         'om': 'orm',
2886         'or': 'ori',
2887         'os': 'oss',
2888         'pa': 'pan',
2889         'pi': 'pli',
2890         'pl': 'pol',
2891         'ps': 'pus',
2892         'pt': 'por',
2893         'qu': 'que',
2894         'rm': 'roh',
2895         'rn': 'run',
2896         'ro': 'ron',
2897         'ru': 'rus',
2898         'rw': 'kin',
2899         'sa': 'san',
2900         'sc': 'srd',
2901         'sd': 'snd',
2902         'se': 'sme',
2903         'sg': 'sag',
2904         'si': 'sin',
2905         'sk': 'slk',
2906         'sl': 'slv',
2907         'sm': 'smo',
2908         'sn': 'sna',
2909         'so': 'som',
2910         'sq': 'sqi',
2911         'sr': 'srp',
2912         'ss': 'ssw',
2913         'st': 'sot',
2914         'su': 'sun',
2915         'sv': 'swe',
2916         'sw': 'swa',
2917         'ta': 'tam',
2918         'te': 'tel',
2919         'tg': 'tgk',
2920         'th': 'tha',
2921         'ti': 'tir',
2922         'tk': 'tuk',
2923         'tl': 'tgl',
2924         'tn': 'tsn',
2925         'to': 'ton',
2926         'tr': 'tur',
2927         'ts': 'tso',
2928         'tt': 'tat',
2929         'tw': 'twi',
2930         'ty': 'tah',
2931         'ug': 'uig',
2932         'uk': 'ukr',
2933         'ur': 'urd',
2934         'uz': 'uzb',
2935         've': 'ven',
2936         'vi': 'vie',
2937         'vo': 'vol',
2938         'wa': 'wln',
2939         'wo': 'wol',
2940         'xh': 'xho',
2941         'yi': 'yid',
2942         'yo': 'yor',
2943         'za': 'zha',
2944         'zh': 'zho',
2945         'zu': 'zul',
2946     }
2947
2948     @classmethod
2949     def short2long(cls, code):
2950         """Convert language code from ISO 639-1 to ISO 639-2/T"""
2951         return cls._lang_map.get(code[:2])
2952
2953     @classmethod
2954     def long2short(cls, code):
2955         """Convert language code from ISO 639-2/T to ISO 639-1"""
2956         for short_name, long_name in cls._lang_map.items():
2957             if long_name == code:
2958                 return short_name
2959
2960
2961 class ISO3166Utils(object):
2962     # From http://data.okfn.org/data/core/country-list
2963     _country_map = {
2964         'AF': 'Afghanistan',
2965         'AX': 'Åland Islands',
2966         'AL': 'Albania',
2967         'DZ': 'Algeria',
2968         'AS': 'American Samoa',
2969         'AD': 'Andorra',
2970         'AO': 'Angola',
2971         'AI': 'Anguilla',
2972         'AQ': 'Antarctica',
2973         'AG': 'Antigua and Barbuda',
2974         'AR': 'Argentina',
2975         'AM': 'Armenia',
2976         'AW': 'Aruba',
2977         'AU': 'Australia',
2978         'AT': 'Austria',
2979         'AZ': 'Azerbaijan',
2980         'BS': 'Bahamas',
2981         'BH': 'Bahrain',
2982         'BD': 'Bangladesh',
2983         'BB': 'Barbados',
2984         'BY': 'Belarus',
2985         'BE': 'Belgium',
2986         'BZ': 'Belize',
2987         'BJ': 'Benin',
2988         'BM': 'Bermuda',
2989         'BT': 'Bhutan',
2990         'BO': 'Bolivia, Plurinational State of',
2991         'BQ': 'Bonaire, Sint Eustatius and Saba',
2992         'BA': 'Bosnia and Herzegovina',
2993         'BW': 'Botswana',
2994         'BV': 'Bouvet Island',
2995         'BR': 'Brazil',
2996         'IO': 'British Indian Ocean Territory',
2997         'BN': 'Brunei Darussalam',
2998         'BG': 'Bulgaria',
2999         'BF': 'Burkina Faso',
3000         'BI': 'Burundi',
3001         'KH': 'Cambodia',
3002         'CM': 'Cameroon',
3003         'CA': 'Canada',
3004         'CV': 'Cape Verde',
3005         'KY': 'Cayman Islands',
3006         'CF': 'Central African Republic',
3007         'TD': 'Chad',
3008         'CL': 'Chile',
3009         'CN': 'China',
3010         'CX': 'Christmas Island',
3011         'CC': 'Cocos (Keeling) Islands',
3012         'CO': 'Colombia',
3013         'KM': 'Comoros',
3014         'CG': 'Congo',
3015         'CD': 'Congo, the Democratic Republic of the',
3016         'CK': 'Cook Islands',
3017         'CR': 'Costa Rica',
3018         'CI': 'Côte d\'Ivoire',
3019         'HR': 'Croatia',
3020         'CU': 'Cuba',
3021         'CW': 'Curaçao',
3022         'CY': 'Cyprus',
3023         'CZ': 'Czech Republic',
3024         'DK': 'Denmark',
3025         'DJ': 'Djibouti',
3026         'DM': 'Dominica',
3027         'DO': 'Dominican Republic',
3028         'EC': 'Ecuador',
3029         'EG': 'Egypt',
3030         'SV': 'El Salvador',
3031         'GQ': 'Equatorial Guinea',
3032         'ER': 'Eritrea',
3033         'EE': 'Estonia',
3034         'ET': 'Ethiopia',
3035         'FK': 'Falkland Islands (Malvinas)',
3036         'FO': 'Faroe Islands',
3037         'FJ': 'Fiji',
3038         'FI': 'Finland',
3039         'FR': 'France',
3040         'GF': 'French Guiana',
3041         'PF': 'French Polynesia',
3042         'TF': 'French Southern Territories',
3043         'GA': 'Gabon',
3044         'GM': 'Gambia',
3045         'GE': 'Georgia',
3046         'DE': 'Germany',
3047         'GH': 'Ghana',
3048         'GI': 'Gibraltar',
3049         'GR': 'Greece',
3050         'GL': 'Greenland',
3051         'GD': 'Grenada',
3052         'GP': 'Guadeloupe',
3053         'GU': 'Guam',
3054         'GT': 'Guatemala',
3055         'GG': 'Guernsey',
3056         'GN': 'Guinea',
3057         'GW': 'Guinea-Bissau',
3058         'GY': 'Guyana',
3059         'HT': 'Haiti',
3060         'HM': 'Heard Island and McDonald Islands',
3061         'VA': 'Holy See (Vatican City State)',
3062         'HN': 'Honduras',
3063         'HK': 'Hong Kong',
3064         'HU': 'Hungary',
3065         'IS': 'Iceland',
3066         'IN': 'India',
3067         'ID': 'Indonesia',
3068         'IR': 'Iran, Islamic Republic of',
3069         'IQ': 'Iraq',
3070         'IE': 'Ireland',
3071         'IM': 'Isle of Man',
3072         'IL': 'Israel',
3073         'IT': 'Italy',
3074         'JM': 'Jamaica',
3075         'JP': 'Japan',
3076         'JE': 'Jersey',
3077         'JO': 'Jordan',
3078         'KZ': 'Kazakhstan',
3079         'KE': 'Kenya',
3080         'KI': 'Kiribati',
3081         'KP': 'Korea, Democratic People\'s Republic of',
3082         'KR': 'Korea, Republic of',
3083         'KW': 'Kuwait',
3084         'KG': 'Kyrgyzstan',
3085         'LA': 'Lao People\'s Democratic Republic',
3086         'LV': 'Latvia',
3087         'LB': 'Lebanon',
3088         'LS': 'Lesotho',
3089         'LR': 'Liberia',
3090         'LY': 'Libya',
3091         'LI': 'Liechtenstein',
3092         'LT': 'Lithuania',
3093         'LU': 'Luxembourg',
3094         'MO': 'Macao',
3095         'MK': 'Macedonia, the Former Yugoslav Republic of',
3096         'MG': 'Madagascar',
3097         'MW': 'Malawi',
3098         'MY': 'Malaysia',
3099         'MV': 'Maldives',
3100         'ML': 'Mali',
3101         'MT': 'Malta',
3102         'MH': 'Marshall Islands',
3103         'MQ': 'Martinique',
3104         'MR': 'Mauritania',
3105         'MU': 'Mauritius',
3106         'YT': 'Mayotte',
3107         'MX': 'Mexico',
3108         'FM': 'Micronesia, Federated States of',
3109         'MD': 'Moldova, Republic of',
3110         'MC': 'Monaco',
3111         'MN': 'Mongolia',
3112         'ME': 'Montenegro',
3113         'MS': 'Montserrat',
3114         'MA': 'Morocco',
3115         'MZ': 'Mozambique',
3116         'MM': 'Myanmar',
3117         'NA': 'Namibia',
3118         'NR': 'Nauru',
3119         'NP': 'Nepal',
3120         'NL': 'Netherlands',
3121         'NC': 'New Caledonia',
3122         'NZ': 'New Zealand',
3123         'NI': 'Nicaragua',
3124         'NE': 'Niger',
3125         'NG': 'Nigeria',
3126         'NU': 'Niue',
3127         'NF': 'Norfolk Island',
3128         'MP': 'Northern Mariana Islands',
3129         'NO': 'Norway',
3130         'OM': 'Oman',
3131         'PK': 'Pakistan',
3132         'PW': 'Palau',
3133         'PS': 'Palestine, State of',
3134         'PA': 'Panama',
3135         'PG': 'Papua New Guinea',
3136         'PY': 'Paraguay',
3137         'PE': 'Peru',
3138         'PH': 'Philippines',
3139         'PN': 'Pitcairn',
3140         'PL': 'Poland',
3141         'PT': 'Portugal',
3142         'PR': 'Puerto Rico',
3143         'QA': 'Qatar',
3144         'RE': 'Réunion',
3145         'RO': 'Romania',
3146         'RU': 'Russian Federation',
3147         'RW': 'Rwanda',
3148         'BL': 'Saint Barthélemy',
3149         'SH': 'Saint Helena, Ascension and Tristan da Cunha',
3150         'KN': 'Saint Kitts and Nevis',
3151         'LC': 'Saint Lucia',
3152         'MF': 'Saint Martin (French part)',
3153         'PM': 'Saint Pierre and Miquelon',
3154         'VC': 'Saint Vincent and the Grenadines',
3155         'WS': 'Samoa',
3156         'SM': 'San Marino',
3157         'ST': 'Sao Tome and Principe',
3158         'SA': 'Saudi Arabia',
3159         'SN': 'Senegal',
3160         'RS': 'Serbia',
3161         'SC': 'Seychelles',
3162         'SL': 'Sierra Leone',
3163         'SG': 'Singapore',
3164         'SX': 'Sint Maarten (Dutch part)',
3165         'SK': 'Slovakia',
3166         'SI': 'Slovenia',
3167         'SB': 'Solomon Islands',
3168         'SO': 'Somalia',
3169         'ZA': 'South Africa',
3170         'GS': 'South Georgia and the South Sandwich Islands',
3171         'SS': 'South Sudan',
3172         'ES': 'Spain',
3173         'LK': 'Sri Lanka',
3174         'SD': 'Sudan',
3175         'SR': 'Suriname',
3176         'SJ': 'Svalbard and Jan Mayen',
3177         'SZ': 'Swaziland',
3178         'SE': 'Sweden',
3179         'CH': 'Switzerland',
3180         'SY': 'Syrian Arab Republic',
3181         'TW': 'Taiwan, Province of China',
3182         'TJ': 'Tajikistan',
3183         'TZ': 'Tanzania, United Republic of',
3184         'TH': 'Thailand',
3185         'TL': 'Timor-Leste',
3186         'TG': 'Togo',
3187         'TK': 'Tokelau',
3188         'TO': 'Tonga',
3189         'TT': 'Trinidad and Tobago',
3190         'TN': 'Tunisia',
3191         'TR': 'Turkey',
3192         'TM': 'Turkmenistan',
3193         'TC': 'Turks and Caicos Islands',
3194         'TV': 'Tuvalu',
3195         'UG': 'Uganda',
3196         'UA': 'Ukraine',
3197         'AE': 'United Arab Emirates',
3198         'GB': 'United Kingdom',
3199         'US': 'United States',
3200         'UM': 'United States Minor Outlying Islands',
3201         'UY': 'Uruguay',
3202         'UZ': 'Uzbekistan',
3203         'VU': 'Vanuatu',
3204         'VE': 'Venezuela, Bolivarian Republic of',
3205         'VN': 'Viet Nam',
3206         'VG': 'Virgin Islands, British',
3207         'VI': 'Virgin Islands, U.S.',
3208         'WF': 'Wallis and Futuna',
3209         'EH': 'Western Sahara',
3210         'YE': 'Yemen',
3211         'ZM': 'Zambia',
3212         'ZW': 'Zimbabwe',
3213     }
3214
3215     @classmethod
3216     def short2full(cls, code):
3217         """Convert an ISO 3166-2 country code to the corresponding full name"""
3218         return cls._country_map.get(code.upper())
3219
3220
3221 class GeoUtils(object):
3222     # Major IPv4 address blocks per country
3223     _country_ip_map = {
3224         'AD': '85.94.160.0/19',
3225         'AE': '94.200.0.0/13',
3226         'AF': '149.54.0.0/17',
3227         'AG': '209.59.64.0/18',
3228         'AI': '204.14.248.0/21',
3229         'AL': '46.99.0.0/16',
3230         'AM': '46.70.0.0/15',
3231         'AO': '105.168.0.0/13',
3232         'AP': '159.117.192.0/21',
3233         'AR': '181.0.0.0/12',
3234         'AS': '202.70.112.0/20',
3235         'AT': '84.112.0.0/13',
3236         'AU': '1.128.0.0/11',
3237         'AW': '181.41.0.0/18',
3238         'AZ': '5.191.0.0/16',
3239         'BA': '31.176.128.0/17',
3240         'BB': '65.48.128.0/17',
3241         'BD': '114.130.0.0/16',
3242         'BE': '57.0.0.0/8',
3243         'BF': '129.45.128.0/17',
3244         'BG': '95.42.0.0/15',
3245         'BH': '37.131.0.0/17',
3246         'BI': '154.117.192.0/18',
3247         'BJ': '137.255.0.0/16',
3248         'BL': '192.131.134.0/24',
3249         'BM': '196.12.64.0/18',
3250         'BN': '156.31.0.0/16',
3251         'BO': '161.56.0.0/16',
3252         'BQ': '161.0.80.0/20',
3253         'BR': '152.240.0.0/12',
3254         'BS': '24.51.64.0/18',
3255         'BT': '119.2.96.0/19',
3256         'BW': '168.167.0.0/16',
3257         'BY': '178.120.0.0/13',
3258         'BZ': '179.42.192.0/18',
3259         'CA': '99.224.0.0/11',
3260         'CD': '41.243.0.0/16',
3261         'CF': '196.32.200.0/21',
3262         'CG': '197.214.128.0/17',
3263         'CH': '85.0.0.0/13',
3264         'CI': '154.232.0.0/14',
3265         'CK': '202.65.32.0/19',
3266         'CL': '152.172.0.0/14',
3267         'CM': '165.210.0.0/15',
3268         'CN': '36.128.0.0/10',
3269         'CO': '181.240.0.0/12',
3270         'CR': '201.192.0.0/12',
3271         'CU': '152.206.0.0/15',
3272         'CV': '165.90.96.0/19',
3273         'CW': '190.88.128.0/17',
3274         'CY': '46.198.0.0/15',
3275         'CZ': '88.100.0.0/14',
3276         'DE': '53.0.0.0/8',
3277         'DJ': '197.241.0.0/17',
3278         'DK': '87.48.0.0/12',
3279         'DM': '192.243.48.0/20',
3280         'DO': '152.166.0.0/15',
3281         'DZ': '41.96.0.0/12',
3282         'EC': '186.68.0.0/15',
3283         'EE': '90.190.0.0/15',
3284         'EG': '156.160.0.0/11',
3285         'ER': '196.200.96.0/20',
3286         'ES': '88.0.0.0/11',
3287         'ET': '196.188.0.0/14',
3288         'EU': '2.16.0.0/13',
3289         'FI': '91.152.0.0/13',
3290         'FJ': '144.120.0.0/16',
3291         'FM': '119.252.112.0/20',
3292         'FO': '88.85.32.0/19',
3293         'FR': '90.0.0.0/9',
3294         'GA': '41.158.0.0/15',
3295         'GB': '25.0.0.0/8',
3296         'GD': '74.122.88.0/21',
3297         'GE': '31.146.0.0/16',
3298         'GF': '161.22.64.0/18',
3299         'GG': '62.68.160.0/19',
3300         'GH': '45.208.0.0/14',
3301         'GI': '85.115.128.0/19',
3302         'GL': '88.83.0.0/19',
3303         'GM': '160.182.0.0/15',
3304         'GN': '197.149.192.0/18',
3305         'GP': '104.250.0.0/19',
3306         'GQ': '105.235.224.0/20',
3307         'GR': '94.64.0.0/13',
3308         'GT': '168.234.0.0/16',
3309         'GU': '168.123.0.0/16',
3310         'GW': '197.214.80.0/20',
3311         'GY': '181.41.64.0/18',
3312         'HK': '113.252.0.0/14',
3313         'HN': '181.210.0.0/16',
3314         'HR': '93.136.0.0/13',
3315         'HT': '148.102.128.0/17',
3316         'HU': '84.0.0.0/14',
3317         'ID': '39.192.0.0/10',
3318         'IE': '87.32.0.0/12',
3319         'IL': '79.176.0.0/13',
3320         'IM': '5.62.80.0/20',
3321         'IN': '117.192.0.0/10',
3322         'IO': '203.83.48.0/21',
3323         'IQ': '37.236.0.0/14',
3324         'IR': '2.176.0.0/12',
3325         'IS': '82.221.0.0/16',
3326         'IT': '79.0.0.0/10',
3327         'JE': '87.244.64.0/18',
3328         'JM': '72.27.0.0/17',
3329         'JO': '176.29.0.0/16',
3330         'JP': '126.0.0.0/8',
3331         'KE': '105.48.0.0/12',
3332         'KG': '158.181.128.0/17',
3333         'KH': '36.37.128.0/17',
3334         'KI': '103.25.140.0/22',
3335         'KM': '197.255.224.0/20',
3336         'KN': '198.32.32.0/19',
3337         'KP': '175.45.176.0/22',
3338         'KR': '175.192.0.0/10',
3339         'KW': '37.36.0.0/14',
3340         'KY': '64.96.0.0/15',
3341         'KZ': '2.72.0.0/13',
3342         'LA': '115.84.64.0/18',
3343         'LB': '178.135.0.0/16',
3344         'LC': '192.147.231.0/24',
3345         'LI': '82.117.0.0/19',
3346         'LK': '112.134.0.0/15',
3347         'LR': '41.86.0.0/19',
3348         'LS': '129.232.0.0/17',
3349         'LT': '78.56.0.0/13',
3350         'LU': '188.42.0.0/16',
3351         'LV': '46.109.0.0/16',
3352         'LY': '41.252.0.0/14',
3353         'MA': '105.128.0.0/11',
3354         'MC': '88.209.64.0/18',
3355         'MD': '37.246.0.0/16',
3356         'ME': '178.175.0.0/17',
3357         'MF': '74.112.232.0/21',
3358         'MG': '154.126.0.0/17',
3359         'MH': '117.103.88.0/21',
3360         'MK': '77.28.0.0/15',
3361         'ML': '154.118.128.0/18',
3362         'MM': '37.111.0.0/17',
3363         'MN': '49.0.128.0/17',
3364         'MO': '60.246.0.0/16',
3365         'MP': '202.88.64.0/20',
3366         'MQ': '109.203.224.0/19',
3367         'MR': '41.188.64.0/18',
3368         'MS': '208.90.112.0/22',
3369         'MT': '46.11.0.0/16',
3370         'MU': '105.16.0.0/12',
3371         'MV': '27.114.128.0/18',
3372         'MW': '105.234.0.0/16',
3373         'MX': '187.192.0.0/11',
3374         'MY': '175.136.0.0/13',
3375         'MZ': '197.218.0.0/15',
3376         'NA': '41.182.0.0/16',
3377         'NC': '101.101.0.0/18',
3378         'NE': '197.214.0.0/18',
3379         'NF': '203.17.240.0/22',
3380         'NG': '105.112.0.0/12',
3381         'NI': '186.76.0.0/15',
3382         'NL': '145.96.0.0/11',
3383         'NO': '84.208.0.0/13',
3384         'NP': '36.252.0.0/15',
3385         'NR': '203.98.224.0/19',
3386         'NU': '49.156.48.0/22',
3387         'NZ': '49.224.0.0/14',
3388         'OM': '5.36.0.0/15',
3389         'PA': '186.72.0.0/15',
3390         'PE': '186.160.0.0/14',
3391         'PF': '123.50.64.0/18',
3392         'PG': '124.240.192.0/19',
3393         'PH': '49.144.0.0/13',
3394         'PK': '39.32.0.0/11',
3395         'PL': '83.0.0.0/11',
3396         'PM': '70.36.0.0/20',
3397         'PR': '66.50.0.0/16',
3398         'PS': '188.161.0.0/16',
3399         'PT': '85.240.0.0/13',
3400         'PW': '202.124.224.0/20',
3401         'PY': '181.120.0.0/14',
3402         'QA': '37.210.0.0/15',
3403         'RE': '139.26.0.0/16',
3404         'RO': '79.112.0.0/13',
3405         'RS': '178.220.0.0/14',
3406         'RU': '5.136.0.0/13',
3407         'RW': '105.178.0.0/15',
3408         'SA': '188.48.0.0/13',
3409         'SB': '202.1.160.0/19',
3410         'SC': '154.192.0.0/11',
3411         'SD': '154.96.0.0/13',
3412         'SE': '78.64.0.0/12',
3413         'SG': '152.56.0.0/14',
3414         'SI': '188.196.0.0/14',
3415         'SK': '78.98.0.0/15',
3416         'SL': '197.215.0.0/17',
3417         'SM': '89.186.32.0/19',
3418         'SN': '41.82.0.0/15',
3419         'SO': '197.220.64.0/19',
3420         'SR': '186.179.128.0/17',
3421         'SS': '105.235.208.0/21',
3422         'ST': '197.159.160.0/19',
3423         'SV': '168.243.0.0/16',
3424         'SX': '190.102.0.0/20',
3425         'SY': '5.0.0.0/16',
3426         'SZ': '41.84.224.0/19',
3427         'TC': '65.255.48.0/20',
3428         'TD': '154.68.128.0/19',
3429         'TG': '196.168.0.0/14',
3430         'TH': '171.96.0.0/13',
3431         'TJ': '85.9.128.0/18',
3432         'TK': '27.96.24.0/21',
3433         'TL': '180.189.160.0/20',
3434         'TM': '95.85.96.0/19',
3435         'TN': '197.0.0.0/11',
3436         'TO': '175.176.144.0/21',
3437         'TR': '78.160.0.0/11',
3438         'TT': '186.44.0.0/15',
3439         'TV': '202.2.96.0/19',
3440         'TW': '120.96.0.0/11',
3441         'TZ': '156.156.0.0/14',
3442         'UA': '93.72.0.0/13',
3443         'UG': '154.224.0.0/13',
3444         'US': '3.0.0.0/8',
3445         'UY': '167.56.0.0/13',
3446         'UZ': '82.215.64.0/18',
3447         'VA': '212.77.0.0/19',
3448         'VC': '24.92.144.0/20',
3449         'VE': '186.88.0.0/13',
3450         'VG': '172.103.64.0/18',
3451         'VI': '146.226.0.0/16',
3452         'VN': '14.160.0.0/11',
3453         'VU': '202.80.32.0/20',
3454         'WF': '117.20.32.0/21',
3455         'WS': '202.4.32.0/19',
3456         'YE': '134.35.0.0/16',
3457         'YT': '41.242.116.0/22',
3458         'ZA': '41.0.0.0/11',
3459         'ZM': '165.56.0.0/13',
3460         'ZW': '41.85.192.0/19',
3461     }
3462
3463     @classmethod
3464     def random_ipv4(cls, code):
3465         block = cls._country_ip_map.get(code.upper())
3466         if not block:
3467             return None
3468         addr, preflen = block.split('/')
3469         addr_min = compat_struct_unpack('!L', socket.inet_aton(addr))[0]
3470         addr_max = addr_min | (0xffffffff >> int(preflen))
3471         return compat_str(socket.inet_ntoa(
3472             compat_struct_pack('!L', random.randint(addr_min, addr_max))))
3473
3474
3475 class PerRequestProxyHandler(compat_urllib_request.ProxyHandler):
3476     def __init__(self, proxies=None):
3477         # Set default handlers
3478         for type in ('http', 'https'):
3479             setattr(self, '%s_open' % type,
3480                     lambda r, proxy='__noproxy__', type=type, meth=self.proxy_open:
3481                         meth(r, proxy, type))
3482         return compat_urllib_request.ProxyHandler.__init__(self, proxies)
3483
3484     def proxy_open(self, req, proxy, type):
3485         req_proxy = req.headers.get('Ytdl-request-proxy')
3486         if req_proxy is not None:
3487             proxy = req_proxy
3488             del req.headers['Ytdl-request-proxy']
3489
3490         if proxy == '__noproxy__':
3491             return None  # No Proxy
3492         if compat_urlparse.urlparse(proxy).scheme.lower() in ('socks', 'socks4', 'socks4a', 'socks5'):
3493             req.add_header('Ytdl-socks-proxy', proxy)
3494             # youtube-dl's http/https handlers do wrapping the socket with socks
3495             return None
3496         return compat_urllib_request.ProxyHandler.proxy_open(
3497             self, req, proxy, type)
3498
3499
3500 # Both long_to_bytes and bytes_to_long are adapted from PyCrypto, which is
3501 # released into Public Domain
3502 # https://github.com/dlitz/pycrypto/blob/master/lib/Crypto/Util/number.py#L387
3503
3504 def long_to_bytes(n, blocksize=0):
3505     """long_to_bytes(n:long, blocksize:int) : string
3506     Convert a long integer to a byte string.
3507
3508     If optional blocksize is given and greater than zero, pad the front of the
3509     byte string with binary zeros so that the length is a multiple of
3510     blocksize.
3511     """
3512     # after much testing, this algorithm was deemed to be the fastest
3513     s = b''
3514     n = int(n)
3515     while n > 0:
3516         s = compat_struct_pack('>I', n & 0xffffffff) + s
3517         n = n >> 32
3518     # strip off leading zeros
3519     for i in range(len(s)):
3520         if s[i] != b'\000'[0]:
3521             break
3522     else:
3523         # only happens when n == 0
3524         s = b'\000'
3525         i = 0
3526     s = s[i:]
3527     # add back some pad bytes.  this could be done more efficiently w.r.t. the
3528     # de-padding being done above, but sigh...
3529     if blocksize > 0 and len(s) % blocksize:
3530         s = (blocksize - len(s) % blocksize) * b'\000' + s
3531     return s
3532
3533
3534 def bytes_to_long(s):
3535     """bytes_to_long(string) : long
3536     Convert a byte string to a long integer.
3537
3538     This is (essentially) the inverse of long_to_bytes().
3539     """
3540     acc = 0
3541     length = len(s)
3542     if length % 4:
3543         extra = (4 - length % 4)
3544         s = b'\000' * extra + s
3545         length = length + extra
3546     for i in range(0, length, 4):
3547         acc = (acc << 32) + compat_struct_unpack('>I', s[i:i + 4])[0]
3548     return acc
3549
3550
3551 def ohdave_rsa_encrypt(data, exponent, modulus):
3552     '''
3553     Implement OHDave's RSA algorithm. See http://www.ohdave.com/rsa/
3554
3555     Input:
3556         data: data to encrypt, bytes-like object
3557         exponent, modulus: parameter e and N of RSA algorithm, both integer
3558     Output: hex string of encrypted data
3559
3560     Limitation: supports one block encryption only
3561     '''
3562
3563     payload = int(binascii.hexlify(data[::-1]), 16)
3564     encrypted = pow(payload, exponent, modulus)
3565     return '%x' % encrypted
3566
3567
3568 def pkcs1pad(data, length):
3569     """
3570     Padding input data with PKCS#1 scheme
3571
3572     @param {int[]} data        input data
3573     @param {int}   length      target length
3574     @returns {int[]}           padded data
3575     """
3576     if len(data) > length - 11:
3577         raise ValueError('Input data too long for PKCS#1 padding')
3578
3579     pseudo_random = [random.randint(0, 254) for _ in range(length - len(data) - 3)]
3580     return [0, 2] + pseudo_random + [0] + data
3581
3582
3583 def encode_base_n(num, n, table=None):
3584     FULL_TABLE = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
3585     if not table:
3586         table = FULL_TABLE[:n]
3587
3588     if n > len(table):
3589         raise ValueError('base %d exceeds table length %d' % (n, len(table)))
3590
3591     if num == 0:
3592         return table[0]
3593
3594     ret = ''
3595     while num:
3596         ret = table[num % n] + ret
3597         num = num // n
3598     return ret
3599
3600
3601 def decode_packed_codes(code):
3602     mobj = re.search(PACKED_CODES_RE, code)
3603     obfucasted_code, base, count, symbols = mobj.groups()
3604     base = int(base)
3605     count = int(count)
3606     symbols = symbols.split('|')
3607     symbol_table = {}
3608
3609     while count:
3610         count -= 1
3611         base_n_count = encode_base_n(count, base)
3612         symbol_table[base_n_count] = symbols[count] or base_n_count
3613
3614     return re.sub(
3615         r'\b(\w+)\b', lambda mobj: symbol_table[mobj.group(0)],
3616         obfucasted_code)
3617
3618
3619 def parse_m3u8_attributes(attrib):
3620     info = {}
3621     for (key, val) in re.findall(r'(?P<key>[A-Z0-9-]+)=(?P<val>"[^"]+"|[^",]+)(?:,|$)', attrib):
3622         if val.startswith('"'):
3623             val = val[1:-1]
3624         info[key] = val
3625     return info
3626
3627
3628 def urshift(val, n):
3629     return val >> n if val >= 0 else (val + 0x100000000) >> n
3630
3631
3632 # Based on png2str() written by @gdkchan and improved by @yokrysty
3633 # Originally posted at https://github.com/rg3/youtube-dl/issues/9706
3634 def decode_png(png_data):
3635     # Reference: https://www.w3.org/TR/PNG/
3636     header = png_data[8:]
3637
3638     if png_data[:8] != b'\x89PNG\x0d\x0a\x1a\x0a' or header[4:8] != b'IHDR':
3639         raise IOError('Not a valid PNG file.')
3640
3641     int_map = {1: '>B', 2: '>H', 4: '>I'}
3642     unpack_integer = lambda x: compat_struct_unpack(int_map[len(x)], x)[0]
3643
3644     chunks = []
3645
3646     while header:
3647         length = unpack_integer(header[:4])
3648         header = header[4:]
3649
3650         chunk_type = header[:4]
3651         header = header[4:]
3652
3653         chunk_data = header[:length]
3654         header = header[length:]
3655
3656         header = header[4:]  # Skip CRC
3657
3658         chunks.append({
3659             'type': chunk_type,
3660             'length': length,
3661             'data': chunk_data
3662         })
3663
3664     ihdr = chunks[0]['data']
3665
3666     width = unpack_integer(ihdr[:4])
3667     height = unpack_integer(ihdr[4:8])
3668
3669     idat = b''
3670
3671     for chunk in chunks:
3672         if chunk['type'] == b'IDAT':
3673             idat += chunk['data']
3674
3675     if not idat:
3676         raise IOError('Unable to read PNG data.')
3677
3678     decompressed_data = bytearray(zlib.decompress(idat))
3679
3680     stride = width * 3
3681     pixels = []
3682
3683     def _get_pixel(idx):
3684         x = idx % stride
3685         y = idx // stride
3686         return pixels[y][x]
3687
3688     for y in range(height):
3689         basePos = y * (1 + stride)
3690         filter_type = decompressed_data[basePos]
3691
3692         current_row = []
3693
3694         pixels.append(current_row)
3695
3696         for x in range(stride):
3697             color = decompressed_data[1 + basePos + x]
3698             basex = y * stride + x
3699             left = 0
3700             up = 0
3701
3702             if x > 2:
3703                 left = _get_pixel(basex - 3)
3704             if y > 0:
3705                 up = _get_pixel(basex - stride)
3706
3707             if filter_type == 1:  # Sub
3708                 color = (color + left) & 0xff
3709             elif filter_type == 2:  # Up
3710                 color = (color + up) & 0xff
3711             elif filter_type == 3:  # Average
3712                 color = (color + ((left + up) >> 1)) & 0xff
3713             elif filter_type == 4:  # Paeth
3714                 a = left
3715                 b = up
3716                 c = 0
3717
3718                 if x > 2 and y > 0:
3719                     c = _get_pixel(basex - stride - 3)
3720
3721                 p = a + b - c
3722
3723                 pa = abs(p - a)
3724                 pb = abs(p - b)
3725                 pc = abs(p - c)
3726
3727                 if pa <= pb and pa <= pc:
3728                     color = (color + a) & 0xff
3729                 elif pb <= pc:
3730                     color = (color + b) & 0xff
3731                 else:
3732                     color = (color + c) & 0xff
3733
3734             current_row.append(color)
3735
3736     return width, height, pixels
3737
3738
3739 def write_xattr(path, key, value):
3740     # This mess below finds the best xattr tool for the job
3741     try:
3742         # try the pyxattr module...
3743         import xattr
3744
3745         if hasattr(xattr, 'set'):  # pyxattr
3746             # Unicode arguments are not supported in python-pyxattr until
3747             # version 0.5.0
3748             # See https://github.com/rg3/youtube-dl/issues/5498
3749             pyxattr_required_version = '0.5.0'
3750             if version_tuple(xattr.__version__) < version_tuple(pyxattr_required_version):
3751                 # TODO: fallback to CLI tools
3752                 raise XAttrUnavailableError(
3753                     'python-pyxattr is detected but is too old. '
3754                     'youtube-dl requires %s or above while your version is %s. '
3755                     'Falling back to other xattr implementations' % (
3756                         pyxattr_required_version, xattr.__version__))
3757
3758             setxattr = xattr.set
3759         else:  # xattr
3760             setxattr = xattr.setxattr
3761
3762         try:
3763             setxattr(path, key, value)
3764         except EnvironmentError as e:
3765             raise XAttrMetadataError(e.errno, e.strerror)
3766
3767     except ImportError:
3768         if compat_os_name == 'nt':
3769             # Write xattrs to NTFS Alternate Data Streams:
3770             # http://en.wikipedia.org/wiki/NTFS#Alternate_data_streams_.28ADS.29
3771             assert ':' not in key
3772             assert os.path.exists(path)
3773
3774             ads_fn = path + ':' + key
3775             try:
3776                 with open(ads_fn, 'wb') as f:
3777                     f.write(value)
3778             except EnvironmentError as e:
3779                 raise XAttrMetadataError(e.errno, e.strerror)
3780         else:
3781             user_has_setfattr = check_executable('setfattr', ['--version'])
3782             user_has_xattr = check_executable('xattr', ['-h'])
3783
3784             if user_has_setfattr or user_has_xattr:
3785
3786                 value = value.decode('utf-8')
3787                 if user_has_setfattr:
3788                     executable = 'setfattr'
3789                     opts = ['-n', key, '-v', value]
3790                 elif user_has_xattr:
3791                     executable = 'xattr'
3792                     opts = ['-w', key, value]
3793
3794                 cmd = ([encodeFilename(executable, True)] +
3795                        [encodeArgument(o) for o in opts] +
3796                        [encodeFilename(path, True)])
3797
3798                 try:
3799                     p = subprocess.Popen(
3800                         cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
3801                 except EnvironmentError as e:
3802                     raise XAttrMetadataError(e.errno, e.strerror)
3803                 stdout, stderr = p.communicate()
3804                 stderr = stderr.decode('utf-8', 'replace')
3805                 if p.returncode != 0:
3806                     raise XAttrMetadataError(p.returncode, stderr)
3807
3808             else:
3809                 # On Unix, and can't find pyxattr, setfattr, or xattr.
3810                 if sys.platform.startswith('linux'):
3811                     raise XAttrUnavailableError(
3812                         "Couldn't find a tool to set the xattrs. "
3813                         "Install either the python 'pyxattr' or 'xattr' "
3814                         "modules, or the GNU 'attr' package "
3815                         "(which contains the 'setfattr' tool).")
3816                 else:
3817                     raise XAttrUnavailableError(
3818                         "Couldn't find a tool to set the xattrs. "
3819                         "Install either the python 'xattr' module, "
3820                         "or the 'xattr' binary.")
3821
3822
3823 def random_birthday(year_field, month_field, day_field):
3824     return {
3825         year_field: str(random.randint(1950, 1995)),
3826         month_field: str(random.randint(1, 12)),
3827         day_field: str(random.randint(1, 31)),
3828     }