[utils] Do not fail in int_or_none on non-numeric data (Closes #7175)
[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 calendar
7 import codecs
8 import contextlib
9 import ctypes
10 import datetime
11 import email.utils
12 import errno
13 import functools
14 import gzip
15 import itertools
16 import io
17 import json
18 import locale
19 import math
20 import operator
21 import os
22 import pipes
23 import platform
24 import re
25 import ssl
26 import socket
27 import struct
28 import subprocess
29 import sys
30 import tempfile
31 import traceback
32 import xml.etree.ElementTree
33 import zlib
34
35 from .compat import (
36     compat_basestring,
37     compat_chr,
38     compat_html_entities,
39     compat_http_client,
40     compat_kwargs,
41     compat_parse_qs,
42     compat_socket_create_connection,
43     compat_str,
44     compat_urllib_error,
45     compat_urllib_parse,
46     compat_urllib_parse_urlparse,
47     compat_urllib_request,
48     compat_urlparse,
49     shlex_quote,
50 )
51
52
53 # This is not clearly defined otherwise
54 compiled_regex_type = type(re.compile(''))
55
56 std_headers = {
57     'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/20.0 (Chrome)',
58     'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
59     'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
60     'Accept-Encoding': 'gzip, deflate',
61     'Accept-Language': 'en-us,en;q=0.5',
62 }
63
64
65 NO_DEFAULT = object()
66
67 ENGLISH_MONTH_NAMES = [
68     'January', 'February', 'March', 'April', 'May', 'June',
69     'July', 'August', 'September', 'October', 'November', 'December']
70
71
72 def preferredencoding():
73     """Get preferred encoding.
74
75     Returns the best encoding scheme for the system, based on
76     locale.getpreferredencoding() and some further tweaks.
77     """
78     try:
79         pref = locale.getpreferredencoding()
80         'TEST'.encode(pref)
81     except Exception:
82         pref = 'UTF-8'
83
84     return pref
85
86
87 def write_json_file(obj, fn):
88     """ Encode obj as JSON and write it to fn, atomically if possible """
89
90     fn = encodeFilename(fn)
91     if sys.version_info < (3, 0) and sys.platform != 'win32':
92         encoding = get_filesystem_encoding()
93         # os.path.basename returns a bytes object, but NamedTemporaryFile
94         # will fail if the filename contains non ascii characters unless we
95         # use a unicode object
96         path_basename = lambda f: os.path.basename(fn).decode(encoding)
97         # the same for os.path.dirname
98         path_dirname = lambda f: os.path.dirname(fn).decode(encoding)
99     else:
100         path_basename = os.path.basename
101         path_dirname = os.path.dirname
102
103     args = {
104         'suffix': '.tmp',
105         'prefix': path_basename(fn) + '.',
106         'dir': path_dirname(fn),
107         'delete': False,
108     }
109
110     # In Python 2.x, json.dump expects a bytestream.
111     # In Python 3.x, it writes to a character stream
112     if sys.version_info < (3, 0):
113         args['mode'] = 'wb'
114     else:
115         args.update({
116             'mode': 'w',
117             'encoding': 'utf-8',
118         })
119
120     tf = tempfile.NamedTemporaryFile(**compat_kwargs(args))
121
122     try:
123         with tf:
124             json.dump(obj, tf)
125         if sys.platform == 'win32':
126             # Need to remove existing file on Windows, else os.rename raises
127             # WindowsError or FileExistsError.
128             try:
129                 os.unlink(fn)
130             except OSError:
131                 pass
132         os.rename(tf.name, fn)
133     except Exception:
134         try:
135             os.remove(tf.name)
136         except OSError:
137             pass
138         raise
139
140
141 if sys.version_info >= (2, 7):
142     def find_xpath_attr(node, xpath, key, val=None):
143         """ Find the xpath xpath[@key=val] """
144         assert re.match(r'^[a-zA-Z_-]+$', key)
145         if val:
146             assert re.match(r'^[a-zA-Z0-9@\s:._-]*$', val)
147         expr = xpath + ('[@%s]' % key if val is None else "[@%s='%s']" % (key, val))
148         return node.find(expr)
149 else:
150     def find_xpath_attr(node, xpath, key, val=None):
151         # Here comes the crazy part: In 2.6, if the xpath is a unicode,
152         # .//node does not match if a node is a direct child of . !
153         if isinstance(xpath, compat_str):
154             xpath = xpath.encode('ascii')
155
156         for f in node.findall(xpath):
157             if key not in f.attrib:
158                 continue
159             if val is None or f.attrib.get(key) == val:
160                 return f
161         return None
162
163 # On python2.6 the xml.etree.ElementTree.Element methods don't support
164 # the namespace parameter
165
166
167 def xpath_with_ns(path, ns_map):
168     components = [c.split(':') for c in path.split('/')]
169     replaced = []
170     for c in components:
171         if len(c) == 1:
172             replaced.append(c[0])
173         else:
174             ns, tag = c
175             replaced.append('{%s}%s' % (ns_map[ns], tag))
176     return '/'.join(replaced)
177
178
179 def xpath_element(node, xpath, name=None, fatal=False, default=NO_DEFAULT):
180     if sys.version_info < (2, 7):  # Crazy 2.6
181         xpath = xpath.encode('ascii')
182
183     n = node.find(xpath)
184     if n is None:
185         if default is not NO_DEFAULT:
186             return default
187         elif fatal:
188             name = xpath if name is None else name
189             raise ExtractorError('Could not find XML element %s' % name)
190         else:
191             return None
192     return n
193
194
195 def xpath_text(node, xpath, name=None, fatal=False, default=NO_DEFAULT):
196     n = xpath_element(node, xpath, name, fatal=fatal, default=default)
197     if n is None or n == default:
198         return n
199     if n.text is None:
200         if default is not NO_DEFAULT:
201             return default
202         elif fatal:
203             name = xpath if name is None else name
204             raise ExtractorError('Could not find XML element\'s text %s' % name)
205         else:
206             return None
207     return n.text
208
209
210 def xpath_attr(node, xpath, key, name=None, fatal=False, default=NO_DEFAULT):
211     n = find_xpath_attr(node, xpath, key)
212     if n is None:
213         if default is not NO_DEFAULT:
214             return default
215         elif fatal:
216             name = '%s[@%s]' % (xpath, key) if name is None else name
217             raise ExtractorError('Could not find XML attribute %s' % name)
218         else:
219             return None
220     return n.attrib[key]
221
222
223 def get_element_by_id(id, html):
224     """Return the content of the tag with the specified ID in the passed HTML document"""
225     return get_element_by_attribute("id", id, html)
226
227
228 def get_element_by_attribute(attribute, value, html):
229     """Return the content of the tag with the specified attribute in the passed HTML document"""
230
231     m = re.search(r'''(?xs)
232         <([a-zA-Z0-9:._-]+)
233          (?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]+|="[^"]+"|='[^']+'))*?
234          \s+%s=['"]?%s['"]?
235          (?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]+|="[^"]+"|='[^']+'))*?
236         \s*>
237         (?P<content>.*?)
238         </\1>
239     ''' % (re.escape(attribute), re.escape(value)), html)
240
241     if not m:
242         return None
243     res = m.group('content')
244
245     if res.startswith('"') or res.startswith("'"):
246         res = res[1:-1]
247
248     return unescapeHTML(res)
249
250
251 def clean_html(html):
252     """Clean an HTML snippet into a readable string"""
253
254     if html is None:  # Convenience for sanitizing descriptions etc.
255         return html
256
257     # Newline vs <br />
258     html = html.replace('\n', ' ')
259     html = re.sub(r'\s*<\s*br\s*/?\s*>\s*', '\n', html)
260     html = re.sub(r'<\s*/\s*p\s*>\s*<\s*p[^>]*>', '\n', html)
261     # Strip html tags
262     html = re.sub('<.*?>', '', html)
263     # Replace html entities
264     html = unescapeHTML(html)
265     return html.strip()
266
267
268 def sanitize_open(filename, open_mode):
269     """Try to open the given filename, and slightly tweak it if this fails.
270
271     Attempts to open the given filename. If this fails, it tries to change
272     the filename slightly, step by step, until it's either able to open it
273     or it fails and raises a final exception, like the standard open()
274     function.
275
276     It returns the tuple (stream, definitive_file_name).
277     """
278     try:
279         if filename == '-':
280             if sys.platform == 'win32':
281                 import msvcrt
282                 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
283             return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename)
284         stream = open(encodeFilename(filename), open_mode)
285         return (stream, filename)
286     except (IOError, OSError) as err:
287         if err.errno in (errno.EACCES,):
288             raise
289
290         # In case of error, try to remove win32 forbidden chars
291         alt_filename = sanitize_path(filename)
292         if alt_filename == filename:
293             raise
294         else:
295             # An exception here should be caught in the caller
296             stream = open(encodeFilename(alt_filename), open_mode)
297             return (stream, alt_filename)
298
299
300 def timeconvert(timestr):
301     """Convert RFC 2822 defined time string into system timestamp"""
302     timestamp = None
303     timetuple = email.utils.parsedate_tz(timestr)
304     if timetuple is not None:
305         timestamp = email.utils.mktime_tz(timetuple)
306     return timestamp
307
308
309 def sanitize_filename(s, restricted=False, is_id=False):
310     """Sanitizes a string so it could be used as part of a filename.
311     If restricted is set, use a stricter subset of allowed characters.
312     Set is_id if this is not an arbitrary string, but an ID that should be kept if possible
313     """
314     def replace_insane(char):
315         if char == '?' or ord(char) < 32 or ord(char) == 127:
316             return ''
317         elif char == '"':
318             return '' if restricted else '\''
319         elif char == ':':
320             return '_-' if restricted else ' -'
321         elif char in '\\/|*<>':
322             return '_'
323         if restricted and (char in '!&\'()[]{}$;`^,#' or char.isspace()):
324             return '_'
325         if restricted and ord(char) > 127:
326             return '_'
327         return char
328
329     # Handle timestamps
330     s = re.sub(r'[0-9]+(?::[0-9]+)+', lambda m: m.group(0).replace(':', '_'), s)
331     result = ''.join(map(replace_insane, s))
332     if not is_id:
333         while '__' in result:
334             result = result.replace('__', '_')
335         result = result.strip('_')
336         # Common case of "Foreign band name - English song title"
337         if restricted and result.startswith('-_'):
338             result = result[2:]
339         if result.startswith('-'):
340             result = '_' + result[len('-'):]
341         result = result.lstrip('.')
342         if not result:
343             result = '_'
344     return result
345
346
347 def sanitize_path(s):
348     """Sanitizes and normalizes path on Windows"""
349     if sys.platform != 'win32':
350         return s
351     drive_or_unc, _ = os.path.splitdrive(s)
352     if sys.version_info < (2, 7) and not drive_or_unc:
353         drive_or_unc, _ = os.path.splitunc(s)
354     norm_path = os.path.normpath(remove_start(s, drive_or_unc)).split(os.path.sep)
355     if drive_or_unc:
356         norm_path.pop(0)
357     sanitized_path = [
358         path_part if path_part in ['.', '..'] else re.sub('(?:[/<>:"\\|\\\\?\\*]|\.$)', '#', path_part)
359         for path_part in norm_path]
360     if drive_or_unc:
361         sanitized_path.insert(0, drive_or_unc + os.path.sep)
362     return os.path.join(*sanitized_path)
363
364
365 def orderedSet(iterable):
366     """ Remove all duplicates from the input iterable """
367     res = []
368     for el in iterable:
369         if el not in res:
370             res.append(el)
371     return res
372
373
374 def _htmlentity_transform(entity):
375     """Transforms an HTML entity to a character."""
376     # Known non-numeric HTML entity
377     if entity in compat_html_entities.name2codepoint:
378         return compat_chr(compat_html_entities.name2codepoint[entity])
379
380     mobj = re.match(r'#(x[0-9a-fA-F]+|[0-9]+)', entity)
381     if mobj is not None:
382         numstr = mobj.group(1)
383         if numstr.startswith('x'):
384             base = 16
385             numstr = '0%s' % numstr
386         else:
387             base = 10
388         return compat_chr(int(numstr, base))
389
390     # Unknown entity in name, return its literal representation
391     return ('&%s;' % entity)
392
393
394 def unescapeHTML(s):
395     if s is None:
396         return None
397     assert type(s) == compat_str
398
399     return re.sub(
400         r'&([^;]+);', lambda m: _htmlentity_transform(m.group(1)), s)
401
402
403 def get_subprocess_encoding():
404     if sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
405         # For subprocess calls, encode with locale encoding
406         # Refer to http://stackoverflow.com/a/9951851/35070
407         encoding = preferredencoding()
408     else:
409         encoding = sys.getfilesystemencoding()
410     if encoding is None:
411         encoding = 'utf-8'
412     return encoding
413
414
415 def encodeFilename(s, for_subprocess=False):
416     """
417     @param s The name of the file
418     """
419
420     assert type(s) == compat_str
421
422     # Python 3 has a Unicode API
423     if sys.version_info >= (3, 0):
424         return s
425
426     # Pass '' directly to use Unicode APIs on Windows 2000 and up
427     # (Detecting Windows NT 4 is tricky because 'major >= 4' would
428     # match Windows 9x series as well. Besides, NT 4 is obsolete.)
429     if not for_subprocess and sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
430         return s
431
432     return s.encode(get_subprocess_encoding(), 'ignore')
433
434
435 def decodeFilename(b, for_subprocess=False):
436
437     if sys.version_info >= (3, 0):
438         return b
439
440     if not isinstance(b, bytes):
441         return b
442
443     return b.decode(get_subprocess_encoding(), 'ignore')
444
445
446 def encodeArgument(s):
447     if not isinstance(s, compat_str):
448         # Legacy code that uses byte strings
449         # Uncomment the following line after fixing all post processors
450         # assert False, 'Internal error: %r should be of type %r, is %r' % (s, compat_str, type(s))
451         s = s.decode('ascii')
452     return encodeFilename(s, True)
453
454
455 def decodeArgument(b):
456     return decodeFilename(b, True)
457
458
459 def decodeOption(optval):
460     if optval is None:
461         return optval
462     if isinstance(optval, bytes):
463         optval = optval.decode(preferredencoding())
464
465     assert isinstance(optval, compat_str)
466     return optval
467
468
469 def formatSeconds(secs):
470     if secs > 3600:
471         return '%d:%02d:%02d' % (secs // 3600, (secs % 3600) // 60, secs % 60)
472     elif secs > 60:
473         return '%d:%02d' % (secs // 60, secs % 60)
474     else:
475         return '%d' % secs
476
477
478 def make_HTTPS_handler(params, **kwargs):
479     opts_no_check_certificate = params.get('nocheckcertificate', False)
480     if hasattr(ssl, 'create_default_context'):  # Python >= 3.4 or 2.7.9
481         context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
482         if opts_no_check_certificate:
483             context.check_hostname = False
484             context.verify_mode = ssl.CERT_NONE
485         try:
486             return YoutubeDLHTTPSHandler(params, context=context, **kwargs)
487         except TypeError:
488             # Python 2.7.8
489             # (create_default_context present but HTTPSHandler has no context=)
490             pass
491
492     if sys.version_info < (3, 2):
493         return YoutubeDLHTTPSHandler(params, **kwargs)
494     else:  # Python < 3.4
495         context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
496         context.verify_mode = (ssl.CERT_NONE
497                                if opts_no_check_certificate
498                                else ssl.CERT_REQUIRED)
499         context.set_default_verify_paths()
500         return YoutubeDLHTTPSHandler(params, context=context, **kwargs)
501
502
503 def bug_reports_message():
504     if ytdl_is_updateable():
505         update_cmd = 'type  youtube-dl -U  to update'
506     else:
507         update_cmd = 'see  https://yt-dl.org/update  on how to update'
508     msg = '; please report this issue on https://yt-dl.org/bug .'
509     msg += ' Make sure you are using the latest version; %s.' % update_cmd
510     msg += ' Be sure to call youtube-dl with the --verbose flag and include its complete output.'
511     return msg
512
513
514 class ExtractorError(Exception):
515     """Error during info extraction."""
516
517     def __init__(self, msg, tb=None, expected=False, cause=None, video_id=None):
518         """ tb, if given, is the original traceback (so that it can be printed out).
519         If expected is set, this is a normal error message and most likely not a bug in youtube-dl.
520         """
521
522         if sys.exc_info()[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError):
523             expected = True
524         if video_id is not None:
525             msg = video_id + ': ' + msg
526         if cause:
527             msg += ' (caused by %r)' % cause
528         if not expected:
529             msg += bug_reports_message()
530         super(ExtractorError, self).__init__(msg)
531
532         self.traceback = tb
533         self.exc_info = sys.exc_info()  # preserve original exception
534         self.cause = cause
535         self.video_id = video_id
536
537     def format_traceback(self):
538         if self.traceback is None:
539             return None
540         return ''.join(traceback.format_tb(self.traceback))
541
542
543 class UnsupportedError(ExtractorError):
544     def __init__(self, url):
545         super(UnsupportedError, self).__init__(
546             'Unsupported URL: %s' % url, expected=True)
547         self.url = url
548
549
550 class RegexNotFoundError(ExtractorError):
551     """Error when a regex didn't match"""
552     pass
553
554
555 class DownloadError(Exception):
556     """Download Error exception.
557
558     This exception may be thrown by FileDownloader objects if they are not
559     configured to continue on errors. They will contain the appropriate
560     error message.
561     """
562
563     def __init__(self, msg, exc_info=None):
564         """ exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """
565         super(DownloadError, self).__init__(msg)
566         self.exc_info = exc_info
567
568
569 class SameFileError(Exception):
570     """Same File exception.
571
572     This exception will be thrown by FileDownloader objects if they detect
573     multiple files would have to be downloaded to the same file on disk.
574     """
575     pass
576
577
578 class PostProcessingError(Exception):
579     """Post Processing exception.
580
581     This exception may be raised by PostProcessor's .run() method to
582     indicate an error in the postprocessing task.
583     """
584
585     def __init__(self, msg):
586         self.msg = msg
587
588
589 class MaxDownloadsReached(Exception):
590     """ --max-downloads limit has been reached. """
591     pass
592
593
594 class UnavailableVideoError(Exception):
595     """Unavailable Format exception.
596
597     This exception will be thrown when a video is requested
598     in a format that is not available for that video.
599     """
600     pass
601
602
603 class ContentTooShortError(Exception):
604     """Content Too Short exception.
605
606     This exception may be raised by FileDownloader objects when a file they
607     download is too small for what the server announced first, indicating
608     the connection was probably interrupted.
609     """
610
611     def __init__(self, downloaded, expected):
612         # Both in bytes
613         self.downloaded = downloaded
614         self.expected = expected
615
616
617 def _create_http_connection(ydl_handler, http_class, is_https, *args, **kwargs):
618     # Working around python 2 bug (see http://bugs.python.org/issue17849) by limiting
619     # expected HTTP responses to meet HTTP/1.0 or later (see also
620     # https://github.com/rg3/youtube-dl/issues/6727)
621     if sys.version_info < (3, 0):
622         kwargs[b'strict'] = True
623     hc = http_class(*args, **kwargs)
624     source_address = ydl_handler._params.get('source_address')
625     if source_address is not None:
626         sa = (source_address, 0)
627         if hasattr(hc, 'source_address'):  # Python 2.7+
628             hc.source_address = sa
629         else:  # Python 2.6
630             def _hc_connect(self, *args, **kwargs):
631                 sock = compat_socket_create_connection(
632                     (self.host, self.port), self.timeout, sa)
633                 if is_https:
634                     self.sock = ssl.wrap_socket(
635                         sock, self.key_file, self.cert_file,
636                         ssl_version=ssl.PROTOCOL_TLSv1)
637                 else:
638                     self.sock = sock
639             hc.connect = functools.partial(_hc_connect, hc)
640
641     return hc
642
643
644 class YoutubeDLHandler(compat_urllib_request.HTTPHandler):
645     """Handler for HTTP requests and responses.
646
647     This class, when installed with an OpenerDirector, automatically adds
648     the standard headers to every HTTP request and handles gzipped and
649     deflated responses from web servers. If compression is to be avoided in
650     a particular request, the original request in the program code only has
651     to include the HTTP header "Youtubedl-No-Compression", which will be
652     removed before making the real request.
653
654     Part of this code was copied from:
655
656     http://techknack.net/python-urllib2-handlers/
657
658     Andrew Rowls, the author of that code, agreed to release it to the
659     public domain.
660     """
661
662     def __init__(self, params, *args, **kwargs):
663         compat_urllib_request.HTTPHandler.__init__(self, *args, **kwargs)
664         self._params = params
665
666     def http_open(self, req):
667         return self.do_open(functools.partial(
668             _create_http_connection, self, compat_http_client.HTTPConnection, False),
669             req)
670
671     @staticmethod
672     def deflate(data):
673         try:
674             return zlib.decompress(data, -zlib.MAX_WBITS)
675         except zlib.error:
676             return zlib.decompress(data)
677
678     @staticmethod
679     def addinfourl_wrapper(stream, headers, url, code):
680         if hasattr(compat_urllib_request.addinfourl, 'getcode'):
681             return compat_urllib_request.addinfourl(stream, headers, url, code)
682         ret = compat_urllib_request.addinfourl(stream, headers, url)
683         ret.code = code
684         return ret
685
686     def http_request(self, req):
687         # According to RFC 3986, URLs can not contain non-ASCII characters, however this is not
688         # always respected by websites, some tend to give out URLs with non percent-encoded
689         # non-ASCII characters (see telemb.py, ard.py [#3412])
690         # urllib chokes on URLs with non-ASCII characters (see http://bugs.python.org/issue3991)
691         # To work around aforementioned issue we will replace request's original URL with
692         # percent-encoded one
693         # Since redirects are also affected (e.g. http://www.southpark.de/alle-episoden/s18e09)
694         # the code of this workaround has been moved here from YoutubeDL.urlopen()
695         url = req.get_full_url()
696         url_escaped = escape_url(url)
697
698         # Substitute URL if any change after escaping
699         if url != url_escaped:
700             req_type = HEADRequest if req.get_method() == 'HEAD' else compat_urllib_request.Request
701             new_req = req_type(
702                 url_escaped, data=req.data, headers=req.headers,
703                 origin_req_host=req.origin_req_host, unverifiable=req.unverifiable)
704             new_req.timeout = req.timeout
705             req = new_req
706
707         for h, v in std_headers.items():
708             # Capitalize is needed because of Python bug 2275: http://bugs.python.org/issue2275
709             # The dict keys are capitalized because of this bug by urllib
710             if h.capitalize() not in req.headers:
711                 req.add_header(h, v)
712         if 'Youtubedl-no-compression' in req.headers:
713             if 'Accept-encoding' in req.headers:
714                 del req.headers['Accept-encoding']
715             del req.headers['Youtubedl-no-compression']
716
717         if sys.version_info < (2, 7) and '#' in req.get_full_url():
718             # Python 2.6 is brain-dead when it comes to fragments
719             req._Request__original = req._Request__original.partition('#')[0]
720             req._Request__r_type = req._Request__r_type.partition('#')[0]
721
722         return req
723
724     def http_response(self, req, resp):
725         old_resp = resp
726         # gzip
727         if resp.headers.get('Content-encoding', '') == 'gzip':
728             content = resp.read()
729             gz = gzip.GzipFile(fileobj=io.BytesIO(content), mode='rb')
730             try:
731                 uncompressed = io.BytesIO(gz.read())
732             except IOError as original_ioerror:
733                 # There may be junk add the end of the file
734                 # See http://stackoverflow.com/q/4928560/35070 for details
735                 for i in range(1, 1024):
736                     try:
737                         gz = gzip.GzipFile(fileobj=io.BytesIO(content[:-i]), mode='rb')
738                         uncompressed = io.BytesIO(gz.read())
739                     except IOError:
740                         continue
741                     break
742                 else:
743                     raise original_ioerror
744             resp = self.addinfourl_wrapper(uncompressed, old_resp.headers, old_resp.url, old_resp.code)
745             resp.msg = old_resp.msg
746         # deflate
747         if resp.headers.get('Content-encoding', '') == 'deflate':
748             gz = io.BytesIO(self.deflate(resp.read()))
749             resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code)
750             resp.msg = old_resp.msg
751         # Percent-encode redirect URL of Location HTTP header to satisfy RFC 3986 (see
752         # https://github.com/rg3/youtube-dl/issues/6457).
753         if 300 <= resp.code < 400:
754             location = resp.headers.get('Location')
755             if location:
756                 # As of RFC 2616 default charset is iso-8859-1 that is respected by python 3
757                 if sys.version_info >= (3, 0):
758                     location = location.encode('iso-8859-1').decode('utf-8')
759                 location_escaped = escape_url(location)
760                 if location != location_escaped:
761                     del resp.headers['Location']
762                     resp.headers['Location'] = location_escaped
763         return resp
764
765     https_request = http_request
766     https_response = http_response
767
768
769 class YoutubeDLHTTPSHandler(compat_urllib_request.HTTPSHandler):
770     def __init__(self, params, https_conn_class=None, *args, **kwargs):
771         compat_urllib_request.HTTPSHandler.__init__(self, *args, **kwargs)
772         self._https_conn_class = https_conn_class or compat_http_client.HTTPSConnection
773         self._params = params
774
775     def https_open(self, req):
776         kwargs = {}
777         if hasattr(self, '_context'):  # python > 2.6
778             kwargs['context'] = self._context
779         if hasattr(self, '_check_hostname'):  # python 3.x
780             kwargs['check_hostname'] = self._check_hostname
781         return self.do_open(functools.partial(
782             _create_http_connection, self, self._https_conn_class, True),
783             req, **kwargs)
784
785
786 class YoutubeDLCookieProcessor(compat_urllib_request.HTTPCookieProcessor):
787     def __init__(self, cookiejar=None):
788         compat_urllib_request.HTTPCookieProcessor.__init__(self, cookiejar)
789
790     def http_response(self, request, response):
791         # Python 2 will choke on next HTTP request in row if there are non-ASCII
792         # characters in Set-Cookie HTTP header of last response (see
793         # https://github.com/rg3/youtube-dl/issues/6769).
794         # In order to at least prevent crashing we will percent encode Set-Cookie
795         # header before HTTPCookieProcessor starts processing it.
796         # if sys.version_info < (3, 0) and response.headers:
797         #     for set_cookie_header in ('Set-Cookie', 'Set-Cookie2'):
798         #         set_cookie = response.headers.get(set_cookie_header)
799         #         if set_cookie:
800         #             set_cookie_escaped = compat_urllib_parse.quote(set_cookie, b"%/;:@&=+$,!~*'()?#[] ")
801         #             if set_cookie != set_cookie_escaped:
802         #                 del response.headers[set_cookie_header]
803         #                 response.headers[set_cookie_header] = set_cookie_escaped
804         return compat_urllib_request.HTTPCookieProcessor.http_response(self, request, response)
805
806     https_request = compat_urllib_request.HTTPCookieProcessor.http_request
807     https_response = http_response
808
809
810 def parse_iso8601(date_str, delimiter='T', timezone=None):
811     """ Return a UNIX timestamp from the given date """
812
813     if date_str is None:
814         return None
815
816     if timezone is None:
817         m = re.search(
818             r'(\.[0-9]+)?(?:Z$| ?(?P<sign>\+|-)(?P<hours>[0-9]{2}):?(?P<minutes>[0-9]{2})$)',
819             date_str)
820         if not m:
821             timezone = datetime.timedelta()
822         else:
823             date_str = date_str[:-len(m.group(0))]
824             if not m.group('sign'):
825                 timezone = datetime.timedelta()
826             else:
827                 sign = 1 if m.group('sign') == '+' else -1
828                 timezone = datetime.timedelta(
829                     hours=sign * int(m.group('hours')),
830                     minutes=sign * int(m.group('minutes')))
831     date_format = '%Y-%m-%d{0}%H:%M:%S'.format(delimiter)
832     dt = datetime.datetime.strptime(date_str, date_format) - timezone
833     return calendar.timegm(dt.timetuple())
834
835
836 def unified_strdate(date_str, day_first=True):
837     """Return a string with the date in the format YYYYMMDD"""
838
839     if date_str is None:
840         return None
841     upload_date = None
842     # Replace commas
843     date_str = date_str.replace(',', ' ')
844     # %z (UTC offset) is only supported in python>=3.2
845     if not re.match(r'^[0-9]{1,2}-[0-9]{1,2}-[0-9]{4}$', date_str):
846         date_str = re.sub(r' ?(\+|-)[0-9]{2}:?[0-9]{2}$', '', date_str)
847     # Remove AM/PM + timezone
848     date_str = re.sub(r'(?i)\s*(?:AM|PM)(?:\s+[A-Z]+)?', '', date_str)
849
850     format_expressions = [
851         '%d %B %Y',
852         '%d %b %Y',
853         '%B %d %Y',
854         '%b %d %Y',
855         '%b %dst %Y %I:%M%p',
856         '%b %dnd %Y %I:%M%p',
857         '%b %dth %Y %I:%M%p',
858         '%Y %m %d',
859         '%Y-%m-%d',
860         '%Y/%m/%d',
861         '%Y/%m/%d %H:%M:%S',
862         '%Y-%m-%d %H:%M:%S',
863         '%Y-%m-%d %H:%M:%S.%f',
864         '%d.%m.%Y %H:%M',
865         '%d.%m.%Y %H.%M',
866         '%Y-%m-%dT%H:%M:%SZ',
867         '%Y-%m-%dT%H:%M:%S.%fZ',
868         '%Y-%m-%dT%H:%M:%S.%f0Z',
869         '%Y-%m-%dT%H:%M:%S',
870         '%Y-%m-%dT%H:%M:%S.%f',
871         '%Y-%m-%dT%H:%M',
872     ]
873     if day_first:
874         format_expressions.extend([
875             '%d-%m-%Y',
876             '%d.%m.%Y',
877             '%d/%m/%Y',
878             '%d/%m/%y',
879             '%d/%m/%Y %H:%M:%S',
880         ])
881     else:
882         format_expressions.extend([
883             '%m-%d-%Y',
884             '%m.%d.%Y',
885             '%m/%d/%Y',
886             '%m/%d/%y',
887             '%m/%d/%Y %H:%M:%S',
888         ])
889     for expression in format_expressions:
890         try:
891             upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
892         except ValueError:
893             pass
894     if upload_date is None:
895         timetuple = email.utils.parsedate_tz(date_str)
896         if timetuple:
897             upload_date = datetime.datetime(*timetuple[:6]).strftime('%Y%m%d')
898     return upload_date
899
900
901 def determine_ext(url, default_ext='unknown_video'):
902     if url is None:
903         return default_ext
904     guess = url.partition('?')[0].rpartition('.')[2]
905     if re.match(r'^[A-Za-z0-9]+$', guess):
906         return guess
907     else:
908         return default_ext
909
910
911 def subtitles_filename(filename, sub_lang, sub_format):
912     return filename.rsplit('.', 1)[0] + '.' + sub_lang + '.' + sub_format
913
914
915 def date_from_str(date_str):
916     """
917     Return a datetime object from a string in the format YYYYMMDD or
918     (now|today)[+-][0-9](day|week|month|year)(s)?"""
919     today = datetime.date.today()
920     if date_str in ('now', 'today'):
921         return today
922     if date_str == 'yesterday':
923         return today - datetime.timedelta(days=1)
924     match = re.match('(now|today)(?P<sign>[+-])(?P<time>\d+)(?P<unit>day|week|month|year)(s)?', date_str)
925     if match is not None:
926         sign = match.group('sign')
927         time = int(match.group('time'))
928         if sign == '-':
929             time = -time
930         unit = match.group('unit')
931         # A bad aproximation?
932         if unit == 'month':
933             unit = 'day'
934             time *= 30
935         elif unit == 'year':
936             unit = 'day'
937             time *= 365
938         unit += 's'
939         delta = datetime.timedelta(**{unit: time})
940         return today + delta
941     return datetime.datetime.strptime(date_str, "%Y%m%d").date()
942
943
944 def hyphenate_date(date_str):
945     """
946     Convert a date in 'YYYYMMDD' format to 'YYYY-MM-DD' format"""
947     match = re.match(r'^(\d\d\d\d)(\d\d)(\d\d)$', date_str)
948     if match is not None:
949         return '-'.join(match.groups())
950     else:
951         return date_str
952
953
954 class DateRange(object):
955     """Represents a time interval between two dates"""
956
957     def __init__(self, start=None, end=None):
958         """start and end must be strings in the format accepted by date"""
959         if start is not None:
960             self.start = date_from_str(start)
961         else:
962             self.start = datetime.datetime.min.date()
963         if end is not None:
964             self.end = date_from_str(end)
965         else:
966             self.end = datetime.datetime.max.date()
967         if self.start > self.end:
968             raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
969
970     @classmethod
971     def day(cls, day):
972         """Returns a range that only contains the given day"""
973         return cls(day, day)
974
975     def __contains__(self, date):
976         """Check if the date is in the range"""
977         if not isinstance(date, datetime.date):
978             date = date_from_str(date)
979         return self.start <= date <= self.end
980
981     def __str__(self):
982         return '%s - %s' % (self.start.isoformat(), self.end.isoformat())
983
984
985 def platform_name():
986     """ Returns the platform name as a compat_str """
987     res = platform.platform()
988     if isinstance(res, bytes):
989         res = res.decode(preferredencoding())
990
991     assert isinstance(res, compat_str)
992     return res
993
994
995 def _windows_write_string(s, out):
996     """ Returns True if the string was written using special methods,
997     False if it has yet to be written out."""
998     # Adapted from http://stackoverflow.com/a/3259271/35070
999
1000     import ctypes
1001     import ctypes.wintypes
1002
1003     WIN_OUTPUT_IDS = {
1004         1: -11,
1005         2: -12,
1006     }
1007
1008     try:
1009         fileno = out.fileno()
1010     except AttributeError:
1011         # If the output stream doesn't have a fileno, it's virtual
1012         return False
1013     except io.UnsupportedOperation:
1014         # Some strange Windows pseudo files?
1015         return False
1016     if fileno not in WIN_OUTPUT_IDS:
1017         return False
1018
1019     GetStdHandle = ctypes.WINFUNCTYPE(
1020         ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD)(
1021         (b"GetStdHandle", ctypes.windll.kernel32))
1022     h = GetStdHandle(WIN_OUTPUT_IDS[fileno])
1023
1024     WriteConsoleW = ctypes.WINFUNCTYPE(
1025         ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE, ctypes.wintypes.LPWSTR,
1026         ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD),
1027         ctypes.wintypes.LPVOID)((b"WriteConsoleW", ctypes.windll.kernel32))
1028     written = ctypes.wintypes.DWORD(0)
1029
1030     GetFileType = ctypes.WINFUNCTYPE(ctypes.wintypes.DWORD, ctypes.wintypes.DWORD)((b"GetFileType", ctypes.windll.kernel32))
1031     FILE_TYPE_CHAR = 0x0002
1032     FILE_TYPE_REMOTE = 0x8000
1033     GetConsoleMode = ctypes.WINFUNCTYPE(
1034         ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE,
1035         ctypes.POINTER(ctypes.wintypes.DWORD))(
1036         (b"GetConsoleMode", ctypes.windll.kernel32))
1037     INVALID_HANDLE_VALUE = ctypes.wintypes.DWORD(-1).value
1038
1039     def not_a_console(handle):
1040         if handle == INVALID_HANDLE_VALUE or handle is None:
1041             return True
1042         return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR or
1043                 GetConsoleMode(handle, ctypes.byref(ctypes.wintypes.DWORD())) == 0)
1044
1045     if not_a_console(h):
1046         return False
1047
1048     def next_nonbmp_pos(s):
1049         try:
1050             return next(i for i, c in enumerate(s) if ord(c) > 0xffff)
1051         except StopIteration:
1052             return len(s)
1053
1054     while s:
1055         count = min(next_nonbmp_pos(s), 1024)
1056
1057         ret = WriteConsoleW(
1058             h, s, count if count else 2, ctypes.byref(written), None)
1059         if ret == 0:
1060             raise OSError('Failed to write string')
1061         if not count:  # We just wrote a non-BMP character
1062             assert written.value == 2
1063             s = s[1:]
1064         else:
1065             assert written.value > 0
1066             s = s[written.value:]
1067     return True
1068
1069
1070 def write_string(s, out=None, encoding=None):
1071     if out is None:
1072         out = sys.stderr
1073     assert type(s) == compat_str
1074
1075     if sys.platform == 'win32' and encoding is None and hasattr(out, 'fileno'):
1076         if _windows_write_string(s, out):
1077             return
1078
1079     if ('b' in getattr(out, 'mode', '') or
1080             sys.version_info[0] < 3):  # Python 2 lies about mode of sys.stderr
1081         byt = s.encode(encoding or preferredencoding(), 'ignore')
1082         out.write(byt)
1083     elif hasattr(out, 'buffer'):
1084         enc = encoding or getattr(out, 'encoding', None) or preferredencoding()
1085         byt = s.encode(enc, 'ignore')
1086         out.buffer.write(byt)
1087     else:
1088         out.write(s)
1089     out.flush()
1090
1091
1092 def bytes_to_intlist(bs):
1093     if not bs:
1094         return []
1095     if isinstance(bs[0], int):  # Python 3
1096         return list(bs)
1097     else:
1098         return [ord(c) for c in bs]
1099
1100
1101 def intlist_to_bytes(xs):
1102     if not xs:
1103         return b''
1104     return struct_pack('%dB' % len(xs), *xs)
1105
1106
1107 # Cross-platform file locking
1108 if sys.platform == 'win32':
1109     import ctypes.wintypes
1110     import msvcrt
1111
1112     class OVERLAPPED(ctypes.Structure):
1113         _fields_ = [
1114             ('Internal', ctypes.wintypes.LPVOID),
1115             ('InternalHigh', ctypes.wintypes.LPVOID),
1116             ('Offset', ctypes.wintypes.DWORD),
1117             ('OffsetHigh', ctypes.wintypes.DWORD),
1118             ('hEvent', ctypes.wintypes.HANDLE),
1119         ]
1120
1121     kernel32 = ctypes.windll.kernel32
1122     LockFileEx = kernel32.LockFileEx
1123     LockFileEx.argtypes = [
1124         ctypes.wintypes.HANDLE,     # hFile
1125         ctypes.wintypes.DWORD,      # dwFlags
1126         ctypes.wintypes.DWORD,      # dwReserved
1127         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
1128         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
1129         ctypes.POINTER(OVERLAPPED)  # Overlapped
1130     ]
1131     LockFileEx.restype = ctypes.wintypes.BOOL
1132     UnlockFileEx = kernel32.UnlockFileEx
1133     UnlockFileEx.argtypes = [
1134         ctypes.wintypes.HANDLE,     # hFile
1135         ctypes.wintypes.DWORD,      # dwReserved
1136         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
1137         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
1138         ctypes.POINTER(OVERLAPPED)  # Overlapped
1139     ]
1140     UnlockFileEx.restype = ctypes.wintypes.BOOL
1141     whole_low = 0xffffffff
1142     whole_high = 0x7fffffff
1143
1144     def _lock_file(f, exclusive):
1145         overlapped = OVERLAPPED()
1146         overlapped.Offset = 0
1147         overlapped.OffsetHigh = 0
1148         overlapped.hEvent = 0
1149         f._lock_file_overlapped_p = ctypes.pointer(overlapped)
1150         handle = msvcrt.get_osfhandle(f.fileno())
1151         if not LockFileEx(handle, 0x2 if exclusive else 0x0, 0,
1152                           whole_low, whole_high, f._lock_file_overlapped_p):
1153             raise OSError('Locking file failed: %r' % ctypes.FormatError())
1154
1155     def _unlock_file(f):
1156         assert f._lock_file_overlapped_p
1157         handle = msvcrt.get_osfhandle(f.fileno())
1158         if not UnlockFileEx(handle, 0,
1159                             whole_low, whole_high, f._lock_file_overlapped_p):
1160             raise OSError('Unlocking file failed: %r' % ctypes.FormatError())
1161
1162 else:
1163     import fcntl
1164
1165     def _lock_file(f, exclusive):
1166         fcntl.flock(f, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
1167
1168     def _unlock_file(f):
1169         fcntl.flock(f, fcntl.LOCK_UN)
1170
1171
1172 class locked_file(object):
1173     def __init__(self, filename, mode, encoding=None):
1174         assert mode in ['r', 'a', 'w']
1175         self.f = io.open(filename, mode, encoding=encoding)
1176         self.mode = mode
1177
1178     def __enter__(self):
1179         exclusive = self.mode != 'r'
1180         try:
1181             _lock_file(self.f, exclusive)
1182         except IOError:
1183             self.f.close()
1184             raise
1185         return self
1186
1187     def __exit__(self, etype, value, traceback):
1188         try:
1189             _unlock_file(self.f)
1190         finally:
1191             self.f.close()
1192
1193     def __iter__(self):
1194         return iter(self.f)
1195
1196     def write(self, *args):
1197         return self.f.write(*args)
1198
1199     def read(self, *args):
1200         return self.f.read(*args)
1201
1202
1203 def get_filesystem_encoding():
1204     encoding = sys.getfilesystemencoding()
1205     return encoding if encoding is not None else 'utf-8'
1206
1207
1208 def shell_quote(args):
1209     quoted_args = []
1210     encoding = get_filesystem_encoding()
1211     for a in args:
1212         if isinstance(a, bytes):
1213             # We may get a filename encoded with 'encodeFilename'
1214             a = a.decode(encoding)
1215         quoted_args.append(pipes.quote(a))
1216     return ' '.join(quoted_args)
1217
1218
1219 def smuggle_url(url, data):
1220     """ Pass additional data in a URL for internal use. """
1221
1222     sdata = compat_urllib_parse.urlencode(
1223         {'__youtubedl_smuggle': json.dumps(data)})
1224     return url + '#' + sdata
1225
1226
1227 def unsmuggle_url(smug_url, default=None):
1228     if '#__youtubedl_smuggle' not in smug_url:
1229         return smug_url, default
1230     url, _, sdata = smug_url.rpartition('#')
1231     jsond = compat_parse_qs(sdata)['__youtubedl_smuggle'][0]
1232     data = json.loads(jsond)
1233     return url, data
1234
1235
1236 def format_bytes(bytes):
1237     if bytes is None:
1238         return 'N/A'
1239     if type(bytes) is str:
1240         bytes = float(bytes)
1241     if bytes == 0.0:
1242         exponent = 0
1243     else:
1244         exponent = int(math.log(bytes, 1024.0))
1245     suffix = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'][exponent]
1246     converted = float(bytes) / float(1024 ** exponent)
1247     return '%.2f%s' % (converted, suffix)
1248
1249
1250 def parse_filesize(s):
1251     if s is None:
1252         return None
1253
1254     # The lower-case forms are of course incorrect and inofficial,
1255     # but we support those too
1256     _UNIT_TABLE = {
1257         'B': 1,
1258         'b': 1,
1259         'KiB': 1024,
1260         'KB': 1000,
1261         'kB': 1024,
1262         'Kb': 1000,
1263         'MiB': 1024 ** 2,
1264         'MB': 1000 ** 2,
1265         'mB': 1024 ** 2,
1266         'Mb': 1000 ** 2,
1267         'GiB': 1024 ** 3,
1268         'GB': 1000 ** 3,
1269         'gB': 1024 ** 3,
1270         'Gb': 1000 ** 3,
1271         'TiB': 1024 ** 4,
1272         'TB': 1000 ** 4,
1273         'tB': 1024 ** 4,
1274         'Tb': 1000 ** 4,
1275         'PiB': 1024 ** 5,
1276         'PB': 1000 ** 5,
1277         'pB': 1024 ** 5,
1278         'Pb': 1000 ** 5,
1279         'EiB': 1024 ** 6,
1280         'EB': 1000 ** 6,
1281         'eB': 1024 ** 6,
1282         'Eb': 1000 ** 6,
1283         'ZiB': 1024 ** 7,
1284         'ZB': 1000 ** 7,
1285         'zB': 1024 ** 7,
1286         'Zb': 1000 ** 7,
1287         'YiB': 1024 ** 8,
1288         'YB': 1000 ** 8,
1289         'yB': 1024 ** 8,
1290         'Yb': 1000 ** 8,
1291     }
1292
1293     units_re = '|'.join(re.escape(u) for u in _UNIT_TABLE)
1294     m = re.match(
1295         r'(?P<num>[0-9]+(?:[,.][0-9]*)?)\s*(?P<unit>%s)' % units_re, s)
1296     if not m:
1297         return None
1298
1299     num_str = m.group('num').replace(',', '.')
1300     mult = _UNIT_TABLE[m.group('unit')]
1301     return int(float(num_str) * mult)
1302
1303
1304 def month_by_name(name):
1305     """ Return the number of a month by (locale-independently) English name """
1306
1307     try:
1308         return ENGLISH_MONTH_NAMES.index(name) + 1
1309     except ValueError:
1310         return None
1311
1312
1313 def month_by_abbreviation(abbrev):
1314     """ Return the number of a month by (locale-independently) English
1315         abbreviations """
1316
1317     try:
1318         return [s[:3] for s in ENGLISH_MONTH_NAMES].index(abbrev) + 1
1319     except ValueError:
1320         return None
1321
1322
1323 def fix_xml_ampersands(xml_str):
1324     """Replace all the '&' by '&amp;' in XML"""
1325     return re.sub(
1326         r'&(?!amp;|lt;|gt;|apos;|quot;|#x[0-9a-fA-F]{,4};|#[0-9]{,4};)',
1327         '&amp;',
1328         xml_str)
1329
1330
1331 def setproctitle(title):
1332     assert isinstance(title, compat_str)
1333     try:
1334         libc = ctypes.cdll.LoadLibrary("libc.so.6")
1335     except OSError:
1336         return
1337     title_bytes = title.encode('utf-8')
1338     buf = ctypes.create_string_buffer(len(title_bytes))
1339     buf.value = title_bytes
1340     try:
1341         libc.prctl(15, buf, 0, 0, 0)
1342     except AttributeError:
1343         return  # Strange libc, just skip this
1344
1345
1346 def remove_start(s, start):
1347     if s.startswith(start):
1348         return s[len(start):]
1349     return s
1350
1351
1352 def remove_end(s, end):
1353     if s.endswith(end):
1354         return s[:-len(end)]
1355     return s
1356
1357
1358 def url_basename(url):
1359     path = compat_urlparse.urlparse(url).path
1360     return path.strip('/').split('/')[-1]
1361
1362
1363 class HEADRequest(compat_urllib_request.Request):
1364     def get_method(self):
1365         return "HEAD"
1366
1367
1368 def int_or_none(v, scale=1, default=None, get_attr=None, invscale=1):
1369     if get_attr:
1370         if v is not None:
1371             v = getattr(v, get_attr, None)
1372     if v == '':
1373         v = None
1374     if v is None:
1375         return default
1376     try:
1377         return int(v) * invscale // scale
1378     except ValueError:
1379         pass
1380
1381
1382 def str_or_none(v, default=None):
1383     return default if v is None else compat_str(v)
1384
1385
1386 def str_to_int(int_str):
1387     """ A more relaxed version of int_or_none """
1388     if int_str is None:
1389         return None
1390     int_str = re.sub(r'[,\.\+]', '', int_str)
1391     return int(int_str)
1392
1393
1394 def float_or_none(v, scale=1, invscale=1, default=None):
1395     return default if v is None else (float(v) * invscale / scale)
1396
1397
1398 def parse_duration(s):
1399     if not isinstance(s, compat_basestring):
1400         return None
1401
1402     s = s.strip()
1403
1404     m = re.match(
1405         r'''(?ix)(?:P?T)?
1406         (?:
1407             (?P<only_mins>[0-9.]+)\s*(?:mins?\.?|minutes?)\s*|
1408             (?P<only_hours>[0-9.]+)\s*(?:hours?)|
1409
1410             \s*(?P<hours_reversed>[0-9]+)\s*(?:[:h]|hours?)\s*(?P<mins_reversed>[0-9]+)\s*(?:[:m]|mins?\.?|minutes?)\s*|
1411             (?:
1412                 (?:
1413                     (?:(?P<days>[0-9]+)\s*(?:[:d]|days?)\s*)?
1414                     (?P<hours>[0-9]+)\s*(?:[:h]|hours?)\s*
1415                 )?
1416                 (?P<mins>[0-9]+)\s*(?:[:m]|mins?|minutes?)\s*
1417             )?
1418             (?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?\s*(?:s|secs?|seconds?)?
1419         )$''', s)
1420     if not m:
1421         return None
1422     res = 0
1423     if m.group('only_mins'):
1424         return float_or_none(m.group('only_mins'), invscale=60)
1425     if m.group('only_hours'):
1426         return float_or_none(m.group('only_hours'), invscale=60 * 60)
1427     if m.group('secs'):
1428         res += int(m.group('secs'))
1429     if m.group('mins_reversed'):
1430         res += int(m.group('mins_reversed')) * 60
1431     if m.group('mins'):
1432         res += int(m.group('mins')) * 60
1433     if m.group('hours'):
1434         res += int(m.group('hours')) * 60 * 60
1435     if m.group('hours_reversed'):
1436         res += int(m.group('hours_reversed')) * 60 * 60
1437     if m.group('days'):
1438         res += int(m.group('days')) * 24 * 60 * 60
1439     if m.group('ms'):
1440         res += float(m.group('ms'))
1441     return res
1442
1443
1444 def prepend_extension(filename, ext, expected_real_ext=None):
1445     name, real_ext = os.path.splitext(filename)
1446     return (
1447         '{0}.{1}{2}'.format(name, ext, real_ext)
1448         if not expected_real_ext or real_ext[1:] == expected_real_ext
1449         else '{0}.{1}'.format(filename, ext))
1450
1451
1452 def replace_extension(filename, ext, expected_real_ext=None):
1453     name, real_ext = os.path.splitext(filename)
1454     return '{0}.{1}'.format(
1455         name if not expected_real_ext or real_ext[1:] == expected_real_ext else filename,
1456         ext)
1457
1458
1459 def check_executable(exe, args=[]):
1460     """ Checks if the given binary is installed somewhere in PATH, and returns its name.
1461     args can be a list of arguments for a short output (like -version) """
1462     try:
1463         subprocess.Popen([exe] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
1464     except OSError:
1465         return False
1466     return exe
1467
1468
1469 def get_exe_version(exe, args=['--version'],
1470                     version_re=None, unrecognized='present'):
1471     """ Returns the version of the specified executable,
1472     or False if the executable is not present """
1473     try:
1474         out, _ = subprocess.Popen(
1475             [encodeArgument(exe)] + args,
1476             stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()
1477     except OSError:
1478         return False
1479     if isinstance(out, bytes):  # Python 2.x
1480         out = out.decode('ascii', 'ignore')
1481     return detect_exe_version(out, version_re, unrecognized)
1482
1483
1484 def detect_exe_version(output, version_re=None, unrecognized='present'):
1485     assert isinstance(output, compat_str)
1486     if version_re is None:
1487         version_re = r'version\s+([-0-9._a-zA-Z]+)'
1488     m = re.search(version_re, output)
1489     if m:
1490         return m.group(1)
1491     else:
1492         return unrecognized
1493
1494
1495 class PagedList(object):
1496     def __len__(self):
1497         # This is only useful for tests
1498         return len(self.getslice())
1499
1500
1501 class OnDemandPagedList(PagedList):
1502     def __init__(self, pagefunc, pagesize):
1503         self._pagefunc = pagefunc
1504         self._pagesize = pagesize
1505
1506     def getslice(self, start=0, end=None):
1507         res = []
1508         for pagenum in itertools.count(start // self._pagesize):
1509             firstid = pagenum * self._pagesize
1510             nextfirstid = pagenum * self._pagesize + self._pagesize
1511             if start >= nextfirstid:
1512                 continue
1513
1514             page_results = list(self._pagefunc(pagenum))
1515
1516             startv = (
1517                 start % self._pagesize
1518                 if firstid <= start < nextfirstid
1519                 else 0)
1520
1521             endv = (
1522                 ((end - 1) % self._pagesize) + 1
1523                 if (end is not None and firstid <= end <= nextfirstid)
1524                 else None)
1525
1526             if startv != 0 or endv is not None:
1527                 page_results = page_results[startv:endv]
1528             res.extend(page_results)
1529
1530             # A little optimization - if current page is not "full", ie. does
1531             # not contain page_size videos then we can assume that this page
1532             # is the last one - there are no more ids on further pages -
1533             # i.e. no need to query again.
1534             if len(page_results) + startv < self._pagesize:
1535                 break
1536
1537             # If we got the whole page, but the next page is not interesting,
1538             # break out early as well
1539             if end == nextfirstid:
1540                 break
1541         return res
1542
1543
1544 class InAdvancePagedList(PagedList):
1545     def __init__(self, pagefunc, pagecount, pagesize):
1546         self._pagefunc = pagefunc
1547         self._pagecount = pagecount
1548         self._pagesize = pagesize
1549
1550     def getslice(self, start=0, end=None):
1551         res = []
1552         start_page = start // self._pagesize
1553         end_page = (
1554             self._pagecount if end is None else (end // self._pagesize + 1))
1555         skip_elems = start - start_page * self._pagesize
1556         only_more = None if end is None else end - start
1557         for pagenum in range(start_page, end_page):
1558             page = list(self._pagefunc(pagenum))
1559             if skip_elems:
1560                 page = page[skip_elems:]
1561                 skip_elems = None
1562             if only_more is not None:
1563                 if len(page) < only_more:
1564                     only_more -= len(page)
1565                 else:
1566                     page = page[:only_more]
1567                     res.extend(page)
1568                     break
1569             res.extend(page)
1570         return res
1571
1572
1573 def uppercase_escape(s):
1574     unicode_escape = codecs.getdecoder('unicode_escape')
1575     return re.sub(
1576         r'\\U[0-9a-fA-F]{8}',
1577         lambda m: unicode_escape(m.group(0))[0],
1578         s)
1579
1580
1581 def lowercase_escape(s):
1582     unicode_escape = codecs.getdecoder('unicode_escape')
1583     return re.sub(
1584         r'\\u[0-9a-fA-F]{4}',
1585         lambda m: unicode_escape(m.group(0))[0],
1586         s)
1587
1588
1589 def escape_rfc3986(s):
1590     """Escape non-ASCII characters as suggested by RFC 3986"""
1591     if sys.version_info < (3, 0) and isinstance(s, compat_str):
1592         s = s.encode('utf-8')
1593     return compat_urllib_parse.quote(s, b"%/;:@&=+$,!~*'()?#[]")
1594
1595
1596 def escape_url(url):
1597     """Escape URL as suggested by RFC 3986"""
1598     url_parsed = compat_urllib_parse_urlparse(url)
1599     return url_parsed._replace(
1600         path=escape_rfc3986(url_parsed.path),
1601         params=escape_rfc3986(url_parsed.params),
1602         query=escape_rfc3986(url_parsed.query),
1603         fragment=escape_rfc3986(url_parsed.fragment)
1604     ).geturl()
1605
1606 try:
1607     struct.pack('!I', 0)
1608 except TypeError:
1609     # In Python 2.6 (and some 2.7 versions), struct requires a bytes argument
1610     def struct_pack(spec, *args):
1611         if isinstance(spec, compat_str):
1612             spec = spec.encode('ascii')
1613         return struct.pack(spec, *args)
1614
1615     def struct_unpack(spec, *args):
1616         if isinstance(spec, compat_str):
1617             spec = spec.encode('ascii')
1618         return struct.unpack(spec, *args)
1619 else:
1620     struct_pack = struct.pack
1621     struct_unpack = struct.unpack
1622
1623
1624 def read_batch_urls(batch_fd):
1625     def fixup(url):
1626         if not isinstance(url, compat_str):
1627             url = url.decode('utf-8', 'replace')
1628         BOM_UTF8 = '\xef\xbb\xbf'
1629         if url.startswith(BOM_UTF8):
1630             url = url[len(BOM_UTF8):]
1631         url = url.strip()
1632         if url.startswith(('#', ';', ']')):
1633             return False
1634         return url
1635
1636     with contextlib.closing(batch_fd) as fd:
1637         return [url for url in map(fixup, fd) if url]
1638
1639
1640 def urlencode_postdata(*args, **kargs):
1641     return compat_urllib_parse.urlencode(*args, **kargs).encode('ascii')
1642
1643
1644 def encode_dict(d, encoding='utf-8'):
1645     return dict((k.encode(encoding), v.encode(encoding)) for k, v in d.items())
1646
1647
1648 try:
1649     etree_iter = xml.etree.ElementTree.Element.iter
1650 except AttributeError:  # Python <=2.6
1651     etree_iter = lambda n: n.findall('.//*')
1652
1653
1654 def parse_xml(s):
1655     class TreeBuilder(xml.etree.ElementTree.TreeBuilder):
1656         def doctype(self, name, pubid, system):
1657             pass  # Ignore doctypes
1658
1659     parser = xml.etree.ElementTree.XMLParser(target=TreeBuilder())
1660     kwargs = {'parser': parser} if sys.version_info >= (2, 7) else {}
1661     tree = xml.etree.ElementTree.XML(s.encode('utf-8'), **kwargs)
1662     # Fix up XML parser in Python 2.x
1663     if sys.version_info < (3, 0):
1664         for n in etree_iter(tree):
1665             if n.text is not None:
1666                 if not isinstance(n.text, compat_str):
1667                     n.text = n.text.decode('utf-8')
1668     return tree
1669
1670
1671 US_RATINGS = {
1672     'G': 0,
1673     'PG': 10,
1674     'PG-13': 13,
1675     'R': 16,
1676     'NC': 18,
1677 }
1678
1679
1680 def parse_age_limit(s):
1681     if s is None:
1682         return None
1683     m = re.match(r'^(?P<age>\d{1,2})\+?$', s)
1684     return int(m.group('age')) if m else US_RATINGS.get(s, None)
1685
1686
1687 def strip_jsonp(code):
1688     return re.sub(
1689         r'(?s)^[a-zA-Z0-9_]+\s*\(\s*(.*)\);?\s*?(?://[^\n]*)*$', r'\1', code)
1690
1691
1692 def js_to_json(code):
1693     def fix_kv(m):
1694         v = m.group(0)
1695         if v in ('true', 'false', 'null'):
1696             return v
1697         if v.startswith('"'):
1698             return v
1699         if v.startswith("'"):
1700             v = v[1:-1]
1701             v = re.sub(r"\\\\|\\'|\"", lambda m: {
1702                 '\\\\': '\\\\',
1703                 "\\'": "'",
1704                 '"': '\\"',
1705             }[m.group(0)], v)
1706         return '"%s"' % v
1707
1708     res = re.sub(r'''(?x)
1709         "(?:[^"\\]*(?:\\\\|\\['"nu]))*[^"\\]*"|
1710         '(?:[^'\\]*(?:\\\\|\\['"nu]))*[^'\\]*'|
1711         [a-zA-Z_][.a-zA-Z_0-9]*
1712         ''', fix_kv, code)
1713     res = re.sub(r',(\s*[\]}])', lambda m: m.group(1), res)
1714     return res
1715
1716
1717 def qualities(quality_ids):
1718     """ Get a numeric quality value out of a list of possible values """
1719     def q(qid):
1720         try:
1721             return quality_ids.index(qid)
1722         except ValueError:
1723             return -1
1724     return q
1725
1726
1727 DEFAULT_OUTTMPL = '%(title)s-%(id)s.%(ext)s'
1728
1729
1730 def limit_length(s, length):
1731     """ Add ellipses to overly long strings """
1732     if s is None:
1733         return None
1734     ELLIPSES = '...'
1735     if len(s) > length:
1736         return s[:length - len(ELLIPSES)] + ELLIPSES
1737     return s
1738
1739
1740 def version_tuple(v):
1741     return tuple(int(e) for e in re.split(r'[-.]', v))
1742
1743
1744 def is_outdated_version(version, limit, assume_new=True):
1745     if not version:
1746         return not assume_new
1747     try:
1748         return version_tuple(version) < version_tuple(limit)
1749     except ValueError:
1750         return not assume_new
1751
1752
1753 def ytdl_is_updateable():
1754     """ Returns if youtube-dl can be updated with -U """
1755     from zipimport import zipimporter
1756
1757     return isinstance(globals().get('__loader__'), zipimporter) or hasattr(sys, 'frozen')
1758
1759
1760 def args_to_str(args):
1761     # Get a short string representation for a subprocess command
1762     return ' '.join(shlex_quote(a) for a in args)
1763
1764
1765 def mimetype2ext(mt):
1766     _, _, res = mt.rpartition('/')
1767
1768     return {
1769         'x-ms-wmv': 'wmv',
1770         'x-mp4-fragmented': 'mp4',
1771         'ttml+xml': 'ttml',
1772     }.get(res, res)
1773
1774
1775 def urlhandle_detect_ext(url_handle):
1776     try:
1777         url_handle.headers
1778         getheader = lambda h: url_handle.headers[h]
1779     except AttributeError:  # Python < 3
1780         getheader = url_handle.info().getheader
1781
1782     cd = getheader('Content-Disposition')
1783     if cd:
1784         m = re.match(r'attachment;\s*filename="(?P<filename>[^"]+)"', cd)
1785         if m:
1786             e = determine_ext(m.group('filename'), default_ext=None)
1787             if e:
1788                 return e
1789
1790     return mimetype2ext(getheader('Content-Type'))
1791
1792
1793 def age_restricted(content_limit, age_limit):
1794     """ Returns True iff the content should be blocked """
1795
1796     if age_limit is None:  # No limit set
1797         return False
1798     if content_limit is None:
1799         return False  # Content available for everyone
1800     return age_limit < content_limit
1801
1802
1803 def is_html(first_bytes):
1804     """ Detect whether a file contains HTML by examining its first bytes. """
1805
1806     BOMS = [
1807         (b'\xef\xbb\xbf', 'utf-8'),
1808         (b'\x00\x00\xfe\xff', 'utf-32-be'),
1809         (b'\xff\xfe\x00\x00', 'utf-32-le'),
1810         (b'\xff\xfe', 'utf-16-le'),
1811         (b'\xfe\xff', 'utf-16-be'),
1812     ]
1813     for bom, enc in BOMS:
1814         if first_bytes.startswith(bom):
1815             s = first_bytes[len(bom):].decode(enc, 'replace')
1816             break
1817     else:
1818         s = first_bytes.decode('utf-8', 'replace')
1819
1820     return re.match(r'^\s*<', s)
1821
1822
1823 def determine_protocol(info_dict):
1824     protocol = info_dict.get('protocol')
1825     if protocol is not None:
1826         return protocol
1827
1828     url = info_dict['url']
1829     if url.startswith('rtmp'):
1830         return 'rtmp'
1831     elif url.startswith('mms'):
1832         return 'mms'
1833     elif url.startswith('rtsp'):
1834         return 'rtsp'
1835
1836     ext = determine_ext(url)
1837     if ext == 'm3u8':
1838         return 'm3u8'
1839     elif ext == 'f4m':
1840         return 'f4m'
1841
1842     return compat_urllib_parse_urlparse(url).scheme
1843
1844
1845 def render_table(header_row, data):
1846     """ Render a list of rows, each as a list of values """
1847     table = [header_row] + data
1848     max_lens = [max(len(compat_str(v)) for v in col) for col in zip(*table)]
1849     format_str = ' '.join('%-' + compat_str(ml + 1) + 's' for ml in max_lens[:-1]) + '%s'
1850     return '\n'.join(format_str % tuple(row) for row in table)
1851
1852
1853 def _match_one(filter_part, dct):
1854     COMPARISON_OPERATORS = {
1855         '<': operator.lt,
1856         '<=': operator.le,
1857         '>': operator.gt,
1858         '>=': operator.ge,
1859         '=': operator.eq,
1860         '!=': operator.ne,
1861     }
1862     operator_rex = re.compile(r'''(?x)\s*
1863         (?P<key>[a-z_]+)
1864         \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
1865         (?:
1866             (?P<intval>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)|
1867             (?P<strval>(?![0-9.])[a-z0-9A-Z]*)
1868         )
1869         \s*$
1870         ''' % '|'.join(map(re.escape, COMPARISON_OPERATORS.keys())))
1871     m = operator_rex.search(filter_part)
1872     if m:
1873         op = COMPARISON_OPERATORS[m.group('op')]
1874         if m.group('strval') is not None:
1875             if m.group('op') not in ('=', '!='):
1876                 raise ValueError(
1877                     'Operator %s does not support string values!' % m.group('op'))
1878             comparison_value = m.group('strval')
1879         else:
1880             try:
1881                 comparison_value = int(m.group('intval'))
1882             except ValueError:
1883                 comparison_value = parse_filesize(m.group('intval'))
1884                 if comparison_value is None:
1885                     comparison_value = parse_filesize(m.group('intval') + 'B')
1886                 if comparison_value is None:
1887                     raise ValueError(
1888                         'Invalid integer value %r in filter part %r' % (
1889                             m.group('intval'), filter_part))
1890         actual_value = dct.get(m.group('key'))
1891         if actual_value is None:
1892             return m.group('none_inclusive')
1893         return op(actual_value, comparison_value)
1894
1895     UNARY_OPERATORS = {
1896         '': lambda v: v is not None,
1897         '!': lambda v: v is None,
1898     }
1899     operator_rex = re.compile(r'''(?x)\s*
1900         (?P<op>%s)\s*(?P<key>[a-z_]+)
1901         \s*$
1902         ''' % '|'.join(map(re.escape, UNARY_OPERATORS.keys())))
1903     m = operator_rex.search(filter_part)
1904     if m:
1905         op = UNARY_OPERATORS[m.group('op')]
1906         actual_value = dct.get(m.group('key'))
1907         return op(actual_value)
1908
1909     raise ValueError('Invalid filter part %r' % filter_part)
1910
1911
1912 def match_str(filter_str, dct):
1913     """ Filter a dictionary with a simple string syntax. Returns True (=passes filter) or false """
1914
1915     return all(
1916         _match_one(filter_part, dct) for filter_part in filter_str.split('&'))
1917
1918
1919 def match_filter_func(filter_str):
1920     def _match_func(info_dict):
1921         if match_str(filter_str, info_dict):
1922             return None
1923         else:
1924             video_title = info_dict.get('title', info_dict.get('id', 'video'))
1925             return '%s does not pass filter %s, skipping ..' % (video_title, filter_str)
1926     return _match_func
1927
1928
1929 def parse_dfxp_time_expr(time_expr):
1930     if not time_expr:
1931         return 0.0
1932
1933     mobj = re.match(r'^(?P<time_offset>\d+(?:\.\d+)?)s?$', time_expr)
1934     if mobj:
1935         return float(mobj.group('time_offset'))
1936
1937     mobj = re.match(r'^(\d+):(\d\d):(\d\d(?:\.\d+)?)$', time_expr)
1938     if mobj:
1939         return 3600 * int(mobj.group(1)) + 60 * int(mobj.group(2)) + float(mobj.group(3))
1940
1941
1942 def srt_subtitles_timecode(seconds):
1943     return '%02d:%02d:%02d,%03d' % (seconds / 3600, (seconds % 3600) / 60, seconds % 60, (seconds % 1) * 1000)
1944
1945
1946 def dfxp2srt(dfxp_data):
1947     _x = functools.partial(xpath_with_ns, ns_map={
1948         'ttml': 'http://www.w3.org/ns/ttml',
1949         'ttaf1': 'http://www.w3.org/2006/10/ttaf1',
1950     })
1951
1952     def parse_node(node):
1953         str_or_empty = functools.partial(str_or_none, default='')
1954
1955         out = str_or_empty(node.text)
1956
1957         for child in node:
1958             if child.tag in (_x('ttml:br'), _x('ttaf1:br'), 'br'):
1959                 out += '\n' + str_or_empty(child.tail)
1960             elif child.tag in (_x('ttml:span'), _x('ttaf1:span'), 'span'):
1961                 out += str_or_empty(parse_node(child))
1962             else:
1963                 out += str_or_empty(xml.etree.ElementTree.tostring(child))
1964
1965         return out
1966
1967     dfxp = xml.etree.ElementTree.fromstring(dfxp_data.encode('utf-8'))
1968     out = []
1969     paras = dfxp.findall(_x('.//ttml:p')) or dfxp.findall(_x('.//ttaf1:p')) or dfxp.findall('.//p')
1970
1971     if not paras:
1972         raise ValueError('Invalid dfxp/TTML subtitle')
1973
1974     for para, index in zip(paras, itertools.count(1)):
1975         begin_time = parse_dfxp_time_expr(para.attrib['begin'])
1976         end_time = parse_dfxp_time_expr(para.attrib.get('end'))
1977         if not end_time:
1978             end_time = begin_time + parse_dfxp_time_expr(para.attrib['dur'])
1979         out.append('%d\n%s --> %s\n%s\n\n' % (
1980             index,
1981             srt_subtitles_timecode(begin_time),
1982             srt_subtitles_timecode(end_time),
1983             parse_node(para)))
1984
1985     return ''.join(out)
1986
1987
1988 def cli_option(params, command_option, param):
1989     param = params.get(param)
1990     return [command_option, param] if param is not None else []
1991
1992
1993 def cli_bool_option(params, command_option, param, true_value='true', false_value='false', separator=None):
1994     param = params.get(param)
1995     assert isinstance(param, bool)
1996     if separator:
1997         return [command_option + separator + (true_value if param else false_value)]
1998     return [command_option, true_value if param else false_value]
1999
2000
2001 def cli_valueless_option(params, command_option, param, expected_value=True):
2002     param = params.get(param)
2003     return [command_option] if param == expected_value else []
2004
2005
2006 def cli_configuration_args(params, param, default=[]):
2007     ex_args = params.get(param)
2008     if ex_args is None:
2009         return default
2010     assert isinstance(ex_args, list)
2011     return ex_args
2012
2013
2014 class ISO639Utils(object):
2015     # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
2016     _lang_map = {
2017         'aa': 'aar',
2018         'ab': 'abk',
2019         'ae': 'ave',
2020         'af': 'afr',
2021         'ak': 'aka',
2022         'am': 'amh',
2023         'an': 'arg',
2024         'ar': 'ara',
2025         'as': 'asm',
2026         'av': 'ava',
2027         'ay': 'aym',
2028         'az': 'aze',
2029         'ba': 'bak',
2030         'be': 'bel',
2031         'bg': 'bul',
2032         'bh': 'bih',
2033         'bi': 'bis',
2034         'bm': 'bam',
2035         'bn': 'ben',
2036         'bo': 'bod',
2037         'br': 'bre',
2038         'bs': 'bos',
2039         'ca': 'cat',
2040         'ce': 'che',
2041         'ch': 'cha',
2042         'co': 'cos',
2043         'cr': 'cre',
2044         'cs': 'ces',
2045         'cu': 'chu',
2046         'cv': 'chv',
2047         'cy': 'cym',
2048         'da': 'dan',
2049         'de': 'deu',
2050         'dv': 'div',
2051         'dz': 'dzo',
2052         'ee': 'ewe',
2053         'el': 'ell',
2054         'en': 'eng',
2055         'eo': 'epo',
2056         'es': 'spa',
2057         'et': 'est',
2058         'eu': 'eus',
2059         'fa': 'fas',
2060         'ff': 'ful',
2061         'fi': 'fin',
2062         'fj': 'fij',
2063         'fo': 'fao',
2064         'fr': 'fra',
2065         'fy': 'fry',
2066         'ga': 'gle',
2067         'gd': 'gla',
2068         'gl': 'glg',
2069         'gn': 'grn',
2070         'gu': 'guj',
2071         'gv': 'glv',
2072         'ha': 'hau',
2073         'he': 'heb',
2074         'hi': 'hin',
2075         'ho': 'hmo',
2076         'hr': 'hrv',
2077         'ht': 'hat',
2078         'hu': 'hun',
2079         'hy': 'hye',
2080         'hz': 'her',
2081         'ia': 'ina',
2082         'id': 'ind',
2083         'ie': 'ile',
2084         'ig': 'ibo',
2085         'ii': 'iii',
2086         'ik': 'ipk',
2087         'io': 'ido',
2088         'is': 'isl',
2089         'it': 'ita',
2090         'iu': 'iku',
2091         'ja': 'jpn',
2092         'jv': 'jav',
2093         'ka': 'kat',
2094         'kg': 'kon',
2095         'ki': 'kik',
2096         'kj': 'kua',
2097         'kk': 'kaz',
2098         'kl': 'kal',
2099         'km': 'khm',
2100         'kn': 'kan',
2101         'ko': 'kor',
2102         'kr': 'kau',
2103         'ks': 'kas',
2104         'ku': 'kur',
2105         'kv': 'kom',
2106         'kw': 'cor',
2107         'ky': 'kir',
2108         'la': 'lat',
2109         'lb': 'ltz',
2110         'lg': 'lug',
2111         'li': 'lim',
2112         'ln': 'lin',
2113         'lo': 'lao',
2114         'lt': 'lit',
2115         'lu': 'lub',
2116         'lv': 'lav',
2117         'mg': 'mlg',
2118         'mh': 'mah',
2119         'mi': 'mri',
2120         'mk': 'mkd',
2121         'ml': 'mal',
2122         'mn': 'mon',
2123         'mr': 'mar',
2124         'ms': 'msa',
2125         'mt': 'mlt',
2126         'my': 'mya',
2127         'na': 'nau',
2128         'nb': 'nob',
2129         'nd': 'nde',
2130         'ne': 'nep',
2131         'ng': 'ndo',
2132         'nl': 'nld',
2133         'nn': 'nno',
2134         'no': 'nor',
2135         'nr': 'nbl',
2136         'nv': 'nav',
2137         'ny': 'nya',
2138         'oc': 'oci',
2139         'oj': 'oji',
2140         'om': 'orm',
2141         'or': 'ori',
2142         'os': 'oss',
2143         'pa': 'pan',
2144         'pi': 'pli',
2145         'pl': 'pol',
2146         'ps': 'pus',
2147         'pt': 'por',
2148         'qu': 'que',
2149         'rm': 'roh',
2150         'rn': 'run',
2151         'ro': 'ron',
2152         'ru': 'rus',
2153         'rw': 'kin',
2154         'sa': 'san',
2155         'sc': 'srd',
2156         'sd': 'snd',
2157         'se': 'sme',
2158         'sg': 'sag',
2159         'si': 'sin',
2160         'sk': 'slk',
2161         'sl': 'slv',
2162         'sm': 'smo',
2163         'sn': 'sna',
2164         'so': 'som',
2165         'sq': 'sqi',
2166         'sr': 'srp',
2167         'ss': 'ssw',
2168         'st': 'sot',
2169         'su': 'sun',
2170         'sv': 'swe',
2171         'sw': 'swa',
2172         'ta': 'tam',
2173         'te': 'tel',
2174         'tg': 'tgk',
2175         'th': 'tha',
2176         'ti': 'tir',
2177         'tk': 'tuk',
2178         'tl': 'tgl',
2179         'tn': 'tsn',
2180         'to': 'ton',
2181         'tr': 'tur',
2182         'ts': 'tso',
2183         'tt': 'tat',
2184         'tw': 'twi',
2185         'ty': 'tah',
2186         'ug': 'uig',
2187         'uk': 'ukr',
2188         'ur': 'urd',
2189         'uz': 'uzb',
2190         've': 'ven',
2191         'vi': 'vie',
2192         'vo': 'vol',
2193         'wa': 'wln',
2194         'wo': 'wol',
2195         'xh': 'xho',
2196         'yi': 'yid',
2197         'yo': 'yor',
2198         'za': 'zha',
2199         'zh': 'zho',
2200         'zu': 'zul',
2201     }
2202
2203     @classmethod
2204     def short2long(cls, code):
2205         """Convert language code from ISO 639-1 to ISO 639-2/T"""
2206         return cls._lang_map.get(code[:2])
2207
2208     @classmethod
2209     def long2short(cls, code):
2210         """Convert language code from ISO 639-2/T to ISO 639-1"""
2211         for short_name, long_name in cls._lang_map.items():
2212             if long_name == code:
2213                 return short_name
2214
2215
2216 class ISO3166Utils(object):
2217     # From http://data.okfn.org/data/core/country-list
2218     _country_map = {
2219         'AF': 'Afghanistan',
2220         'AX': 'Åland Islands',
2221         'AL': 'Albania',
2222         'DZ': 'Algeria',
2223         'AS': 'American Samoa',
2224         'AD': 'Andorra',
2225         'AO': 'Angola',
2226         'AI': 'Anguilla',
2227         'AQ': 'Antarctica',
2228         'AG': 'Antigua and Barbuda',
2229         'AR': 'Argentina',
2230         'AM': 'Armenia',
2231         'AW': 'Aruba',
2232         'AU': 'Australia',
2233         'AT': 'Austria',
2234         'AZ': 'Azerbaijan',
2235         'BS': 'Bahamas',
2236         'BH': 'Bahrain',
2237         'BD': 'Bangladesh',
2238         'BB': 'Barbados',
2239         'BY': 'Belarus',
2240         'BE': 'Belgium',
2241         'BZ': 'Belize',
2242         'BJ': 'Benin',
2243         'BM': 'Bermuda',
2244         'BT': 'Bhutan',
2245         'BO': 'Bolivia, Plurinational State of',
2246         'BQ': 'Bonaire, Sint Eustatius and Saba',
2247         'BA': 'Bosnia and Herzegovina',
2248         'BW': 'Botswana',
2249         'BV': 'Bouvet Island',
2250         'BR': 'Brazil',
2251         'IO': 'British Indian Ocean Territory',
2252         'BN': 'Brunei Darussalam',
2253         'BG': 'Bulgaria',
2254         'BF': 'Burkina Faso',
2255         'BI': 'Burundi',
2256         'KH': 'Cambodia',
2257         'CM': 'Cameroon',
2258         'CA': 'Canada',
2259         'CV': 'Cape Verde',
2260         'KY': 'Cayman Islands',
2261         'CF': 'Central African Republic',
2262         'TD': 'Chad',
2263         'CL': 'Chile',
2264         'CN': 'China',
2265         'CX': 'Christmas Island',
2266         'CC': 'Cocos (Keeling) Islands',
2267         'CO': 'Colombia',
2268         'KM': 'Comoros',
2269         'CG': 'Congo',
2270         'CD': 'Congo, the Democratic Republic of the',
2271         'CK': 'Cook Islands',
2272         'CR': 'Costa Rica',
2273         'CI': 'Côte d\'Ivoire',
2274         'HR': 'Croatia',
2275         'CU': 'Cuba',
2276         'CW': 'Curaçao',
2277         'CY': 'Cyprus',
2278         'CZ': 'Czech Republic',
2279         'DK': 'Denmark',
2280         'DJ': 'Djibouti',
2281         'DM': 'Dominica',
2282         'DO': 'Dominican Republic',
2283         'EC': 'Ecuador',
2284         'EG': 'Egypt',
2285         'SV': 'El Salvador',
2286         'GQ': 'Equatorial Guinea',
2287         'ER': 'Eritrea',
2288         'EE': 'Estonia',
2289         'ET': 'Ethiopia',
2290         'FK': 'Falkland Islands (Malvinas)',
2291         'FO': 'Faroe Islands',
2292         'FJ': 'Fiji',
2293         'FI': 'Finland',
2294         'FR': 'France',
2295         'GF': 'French Guiana',
2296         'PF': 'French Polynesia',
2297         'TF': 'French Southern Territories',
2298         'GA': 'Gabon',
2299         'GM': 'Gambia',
2300         'GE': 'Georgia',
2301         'DE': 'Germany',
2302         'GH': 'Ghana',
2303         'GI': 'Gibraltar',
2304         'GR': 'Greece',
2305         'GL': 'Greenland',
2306         'GD': 'Grenada',
2307         'GP': 'Guadeloupe',
2308         'GU': 'Guam',
2309         'GT': 'Guatemala',
2310         'GG': 'Guernsey',
2311         'GN': 'Guinea',
2312         'GW': 'Guinea-Bissau',
2313         'GY': 'Guyana',
2314         'HT': 'Haiti',
2315         'HM': 'Heard Island and McDonald Islands',
2316         'VA': 'Holy See (Vatican City State)',
2317         'HN': 'Honduras',
2318         'HK': 'Hong Kong',
2319         'HU': 'Hungary',
2320         'IS': 'Iceland',
2321         'IN': 'India',
2322         'ID': 'Indonesia',
2323         'IR': 'Iran, Islamic Republic of',
2324         'IQ': 'Iraq',
2325         'IE': 'Ireland',
2326         'IM': 'Isle of Man',
2327         'IL': 'Israel',
2328         'IT': 'Italy',
2329         'JM': 'Jamaica',
2330         'JP': 'Japan',
2331         'JE': 'Jersey',
2332         'JO': 'Jordan',
2333         'KZ': 'Kazakhstan',
2334         'KE': 'Kenya',
2335         'KI': 'Kiribati',
2336         'KP': 'Korea, Democratic People\'s Republic of',
2337         'KR': 'Korea, Republic of',
2338         'KW': 'Kuwait',
2339         'KG': 'Kyrgyzstan',
2340         'LA': 'Lao People\'s Democratic Republic',
2341         'LV': 'Latvia',
2342         'LB': 'Lebanon',
2343         'LS': 'Lesotho',
2344         'LR': 'Liberia',
2345         'LY': 'Libya',
2346         'LI': 'Liechtenstein',
2347         'LT': 'Lithuania',
2348         'LU': 'Luxembourg',
2349         'MO': 'Macao',
2350         'MK': 'Macedonia, the Former Yugoslav Republic of',
2351         'MG': 'Madagascar',
2352         'MW': 'Malawi',
2353         'MY': 'Malaysia',
2354         'MV': 'Maldives',
2355         'ML': 'Mali',
2356         'MT': 'Malta',
2357         'MH': 'Marshall Islands',
2358         'MQ': 'Martinique',
2359         'MR': 'Mauritania',
2360         'MU': 'Mauritius',
2361         'YT': 'Mayotte',
2362         'MX': 'Mexico',
2363         'FM': 'Micronesia, Federated States of',
2364         'MD': 'Moldova, Republic of',
2365         'MC': 'Monaco',
2366         'MN': 'Mongolia',
2367         'ME': 'Montenegro',
2368         'MS': 'Montserrat',
2369         'MA': 'Morocco',
2370         'MZ': 'Mozambique',
2371         'MM': 'Myanmar',
2372         'NA': 'Namibia',
2373         'NR': 'Nauru',
2374         'NP': 'Nepal',
2375         'NL': 'Netherlands',
2376         'NC': 'New Caledonia',
2377         'NZ': 'New Zealand',
2378         'NI': 'Nicaragua',
2379         'NE': 'Niger',
2380         'NG': 'Nigeria',
2381         'NU': 'Niue',
2382         'NF': 'Norfolk Island',
2383         'MP': 'Northern Mariana Islands',
2384         'NO': 'Norway',
2385         'OM': 'Oman',
2386         'PK': 'Pakistan',
2387         'PW': 'Palau',
2388         'PS': 'Palestine, State of',
2389         'PA': 'Panama',
2390         'PG': 'Papua New Guinea',
2391         'PY': 'Paraguay',
2392         'PE': 'Peru',
2393         'PH': 'Philippines',
2394         'PN': 'Pitcairn',
2395         'PL': 'Poland',
2396         'PT': 'Portugal',
2397         'PR': 'Puerto Rico',
2398         'QA': 'Qatar',
2399         'RE': 'Réunion',
2400         'RO': 'Romania',
2401         'RU': 'Russian Federation',
2402         'RW': 'Rwanda',
2403         'BL': 'Saint Barthélemy',
2404         'SH': 'Saint Helena, Ascension and Tristan da Cunha',
2405         'KN': 'Saint Kitts and Nevis',
2406         'LC': 'Saint Lucia',
2407         'MF': 'Saint Martin (French part)',
2408         'PM': 'Saint Pierre and Miquelon',
2409         'VC': 'Saint Vincent and the Grenadines',
2410         'WS': 'Samoa',
2411         'SM': 'San Marino',
2412         'ST': 'Sao Tome and Principe',
2413         'SA': 'Saudi Arabia',
2414         'SN': 'Senegal',
2415         'RS': 'Serbia',
2416         'SC': 'Seychelles',
2417         'SL': 'Sierra Leone',
2418         'SG': 'Singapore',
2419         'SX': 'Sint Maarten (Dutch part)',
2420         'SK': 'Slovakia',
2421         'SI': 'Slovenia',
2422         'SB': 'Solomon Islands',
2423         'SO': 'Somalia',
2424         'ZA': 'South Africa',
2425         'GS': 'South Georgia and the South Sandwich Islands',
2426         'SS': 'South Sudan',
2427         'ES': 'Spain',
2428         'LK': 'Sri Lanka',
2429         'SD': 'Sudan',
2430         'SR': 'Suriname',
2431         'SJ': 'Svalbard and Jan Mayen',
2432         'SZ': 'Swaziland',
2433         'SE': 'Sweden',
2434         'CH': 'Switzerland',
2435         'SY': 'Syrian Arab Republic',
2436         'TW': 'Taiwan, Province of China',
2437         'TJ': 'Tajikistan',
2438         'TZ': 'Tanzania, United Republic of',
2439         'TH': 'Thailand',
2440         'TL': 'Timor-Leste',
2441         'TG': 'Togo',
2442         'TK': 'Tokelau',
2443         'TO': 'Tonga',
2444         'TT': 'Trinidad and Tobago',
2445         'TN': 'Tunisia',
2446         'TR': 'Turkey',
2447         'TM': 'Turkmenistan',
2448         'TC': 'Turks and Caicos Islands',
2449         'TV': 'Tuvalu',
2450         'UG': 'Uganda',
2451         'UA': 'Ukraine',
2452         'AE': 'United Arab Emirates',
2453         'GB': 'United Kingdom',
2454         'US': 'United States',
2455         'UM': 'United States Minor Outlying Islands',
2456         'UY': 'Uruguay',
2457         'UZ': 'Uzbekistan',
2458         'VU': 'Vanuatu',
2459         'VE': 'Venezuela, Bolivarian Republic of',
2460         'VN': 'Viet Nam',
2461         'VG': 'Virgin Islands, British',
2462         'VI': 'Virgin Islands, U.S.',
2463         'WF': 'Wallis and Futuna',
2464         'EH': 'Western Sahara',
2465         'YE': 'Yemen',
2466         'ZM': 'Zambia',
2467         'ZW': 'Zimbabwe',
2468     }
2469
2470     @classmethod
2471     def short2full(cls, code):
2472         """Convert an ISO 3166-2 country code to the corresponding full name"""
2473         return cls._country_map.get(code.upper())
2474
2475
2476 class PerRequestProxyHandler(compat_urllib_request.ProxyHandler):
2477     def __init__(self, proxies=None):
2478         # Set default handlers
2479         for type in ('http', 'https'):
2480             setattr(self, '%s_open' % type,
2481                     lambda r, proxy='__noproxy__', type=type, meth=self.proxy_open:
2482                         meth(r, proxy, type))
2483         return compat_urllib_request.ProxyHandler.__init__(self, proxies)
2484
2485     def proxy_open(self, req, proxy, type):
2486         req_proxy = req.headers.get('Ytdl-request-proxy')
2487         if req_proxy is not None:
2488             proxy = req_proxy
2489             del req.headers['Ytdl-request-proxy']
2490
2491         if proxy == '__noproxy__':
2492             return None  # No Proxy
2493         return compat_urllib_request.ProxyHandler.proxy_open(
2494             self, req, proxy, type)