[utils] Add encode_dict
[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['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 = '; '.join([
801                         escape_rfc3986(cookie_attr.strip())
802                         for cookie_attr in set_cookie.decode('iso-8859-1').split(';')]).encode('iso-8859-1')
803                     if set_cookie != set_cookie_escaped:
804                         del response.headers[set_cookie_header]
805                         response.headers[set_cookie_header] = set_cookie_escaped
806         return compat_urllib_request.HTTPCookieProcessor.http_response(self, request, response)
807
808     https_request = compat_urllib_request.HTTPCookieProcessor.http_request
809     https_response = http_response
810
811
812 def parse_iso8601(date_str, delimiter='T', timezone=None):
813     """ Return a UNIX timestamp from the given date """
814
815     if date_str is None:
816         return None
817
818     if timezone is None:
819         m = re.search(
820             r'(\.[0-9]+)?(?:Z$| ?(?P<sign>\+|-)(?P<hours>[0-9]{2}):?(?P<minutes>[0-9]{2})$)',
821             date_str)
822         if not m:
823             timezone = datetime.timedelta()
824         else:
825             date_str = date_str[:-len(m.group(0))]
826             if not m.group('sign'):
827                 timezone = datetime.timedelta()
828             else:
829                 sign = 1 if m.group('sign') == '+' else -1
830                 timezone = datetime.timedelta(
831                     hours=sign * int(m.group('hours')),
832                     minutes=sign * int(m.group('minutes')))
833     date_format = '%Y-%m-%d{0}%H:%M:%S'.format(delimiter)
834     dt = datetime.datetime.strptime(date_str, date_format) - timezone
835     return calendar.timegm(dt.timetuple())
836
837
838 def unified_strdate(date_str, day_first=True):
839     """Return a string with the date in the format YYYYMMDD"""
840
841     if date_str is None:
842         return None
843     upload_date = None
844     # Replace commas
845     date_str = date_str.replace(',', ' ')
846     # %z (UTC offset) is only supported in python>=3.2
847     if not re.match(r'^[0-9]{1,2}-[0-9]{1,2}-[0-9]{4}$', date_str):
848         date_str = re.sub(r' ?(\+|-)[0-9]{2}:?[0-9]{2}$', '', date_str)
849     # Remove AM/PM + timezone
850     date_str = re.sub(r'(?i)\s*(?:AM|PM)(?:\s+[A-Z]+)?', '', date_str)
851
852     format_expressions = [
853         '%d %B %Y',
854         '%d %b %Y',
855         '%B %d %Y',
856         '%b %d %Y',
857         '%b %dst %Y %I:%M%p',
858         '%b %dnd %Y %I:%M%p',
859         '%b %dth %Y %I:%M%p',
860         '%Y %m %d',
861         '%Y-%m-%d',
862         '%Y/%m/%d',
863         '%Y/%m/%d %H:%M:%S',
864         '%Y-%m-%d %H:%M:%S',
865         '%Y-%m-%d %H:%M:%S.%f',
866         '%d.%m.%Y %H:%M',
867         '%d.%m.%Y %H.%M',
868         '%Y-%m-%dT%H:%M:%SZ',
869         '%Y-%m-%dT%H:%M:%S.%fZ',
870         '%Y-%m-%dT%H:%M:%S.%f0Z',
871         '%Y-%m-%dT%H:%M:%S',
872         '%Y-%m-%dT%H:%M:%S.%f',
873         '%Y-%m-%dT%H:%M',
874     ]
875     if day_first:
876         format_expressions.extend([
877             '%d-%m-%Y',
878             '%d.%m.%Y',
879             '%d/%m/%Y',
880             '%d/%m/%y',
881             '%d/%m/%Y %H:%M:%S',
882         ])
883     else:
884         format_expressions.extend([
885             '%m-%d-%Y',
886             '%m.%d.%Y',
887             '%m/%d/%Y',
888             '%m/%d/%y',
889             '%m/%d/%Y %H:%M:%S',
890         ])
891     for expression in format_expressions:
892         try:
893             upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
894         except ValueError:
895             pass
896     if upload_date is None:
897         timetuple = email.utils.parsedate_tz(date_str)
898         if timetuple:
899             upload_date = datetime.datetime(*timetuple[:6]).strftime('%Y%m%d')
900     return upload_date
901
902
903 def determine_ext(url, default_ext='unknown_video'):
904     if url is None:
905         return default_ext
906     guess = url.partition('?')[0].rpartition('.')[2]
907     if re.match(r'^[A-Za-z0-9]+$', guess):
908         return guess
909     else:
910         return default_ext
911
912
913 def subtitles_filename(filename, sub_lang, sub_format):
914     return filename.rsplit('.', 1)[0] + '.' + sub_lang + '.' + sub_format
915
916
917 def date_from_str(date_str):
918     """
919     Return a datetime object from a string in the format YYYYMMDD or
920     (now|today)[+-][0-9](day|week|month|year)(s)?"""
921     today = datetime.date.today()
922     if date_str in ('now', 'today'):
923         return today
924     if date_str == 'yesterday':
925         return today - datetime.timedelta(days=1)
926     match = re.match('(now|today)(?P<sign>[+-])(?P<time>\d+)(?P<unit>day|week|month|year)(s)?', date_str)
927     if match is not None:
928         sign = match.group('sign')
929         time = int(match.group('time'))
930         if sign == '-':
931             time = -time
932         unit = match.group('unit')
933         # A bad aproximation?
934         if unit == 'month':
935             unit = 'day'
936             time *= 30
937         elif unit == 'year':
938             unit = 'day'
939             time *= 365
940         unit += 's'
941         delta = datetime.timedelta(**{unit: time})
942         return today + delta
943     return datetime.datetime.strptime(date_str, "%Y%m%d").date()
944
945
946 def hyphenate_date(date_str):
947     """
948     Convert a date in 'YYYYMMDD' format to 'YYYY-MM-DD' format"""
949     match = re.match(r'^(\d\d\d\d)(\d\d)(\d\d)$', date_str)
950     if match is not None:
951         return '-'.join(match.groups())
952     else:
953         return date_str
954
955
956 class DateRange(object):
957     """Represents a time interval between two dates"""
958
959     def __init__(self, start=None, end=None):
960         """start and end must be strings in the format accepted by date"""
961         if start is not None:
962             self.start = date_from_str(start)
963         else:
964             self.start = datetime.datetime.min.date()
965         if end is not None:
966             self.end = date_from_str(end)
967         else:
968             self.end = datetime.datetime.max.date()
969         if self.start > self.end:
970             raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
971
972     @classmethod
973     def day(cls, day):
974         """Returns a range that only contains the given day"""
975         return cls(day, day)
976
977     def __contains__(self, date):
978         """Check if the date is in the range"""
979         if not isinstance(date, datetime.date):
980             date = date_from_str(date)
981         return self.start <= date <= self.end
982
983     def __str__(self):
984         return '%s - %s' % (self.start.isoformat(), self.end.isoformat())
985
986
987 def platform_name():
988     """ Returns the platform name as a compat_str """
989     res = platform.platform()
990     if isinstance(res, bytes):
991         res = res.decode(preferredencoding())
992
993     assert isinstance(res, compat_str)
994     return res
995
996
997 def _windows_write_string(s, out):
998     """ Returns True if the string was written using special methods,
999     False if it has yet to be written out."""
1000     # Adapted from http://stackoverflow.com/a/3259271/35070
1001
1002     import ctypes
1003     import ctypes.wintypes
1004
1005     WIN_OUTPUT_IDS = {
1006         1: -11,
1007         2: -12,
1008     }
1009
1010     try:
1011         fileno = out.fileno()
1012     except AttributeError:
1013         # If the output stream doesn't have a fileno, it's virtual
1014         return False
1015     except io.UnsupportedOperation:
1016         # Some strange Windows pseudo files?
1017         return False
1018     if fileno not in WIN_OUTPUT_IDS:
1019         return False
1020
1021     GetStdHandle = ctypes.WINFUNCTYPE(
1022         ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD)(
1023         (b"GetStdHandle", ctypes.windll.kernel32))
1024     h = GetStdHandle(WIN_OUTPUT_IDS[fileno])
1025
1026     WriteConsoleW = ctypes.WINFUNCTYPE(
1027         ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE, ctypes.wintypes.LPWSTR,
1028         ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD),
1029         ctypes.wintypes.LPVOID)((b"WriteConsoleW", ctypes.windll.kernel32))
1030     written = ctypes.wintypes.DWORD(0)
1031
1032     GetFileType = ctypes.WINFUNCTYPE(ctypes.wintypes.DWORD, ctypes.wintypes.DWORD)((b"GetFileType", ctypes.windll.kernel32))
1033     FILE_TYPE_CHAR = 0x0002
1034     FILE_TYPE_REMOTE = 0x8000
1035     GetConsoleMode = ctypes.WINFUNCTYPE(
1036         ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE,
1037         ctypes.POINTER(ctypes.wintypes.DWORD))(
1038         (b"GetConsoleMode", ctypes.windll.kernel32))
1039     INVALID_HANDLE_VALUE = ctypes.wintypes.DWORD(-1).value
1040
1041     def not_a_console(handle):
1042         if handle == INVALID_HANDLE_VALUE or handle is None:
1043             return True
1044         return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR or
1045                 GetConsoleMode(handle, ctypes.byref(ctypes.wintypes.DWORD())) == 0)
1046
1047     if not_a_console(h):
1048         return False
1049
1050     def next_nonbmp_pos(s):
1051         try:
1052             return next(i for i, c in enumerate(s) if ord(c) > 0xffff)
1053         except StopIteration:
1054             return len(s)
1055
1056     while s:
1057         count = min(next_nonbmp_pos(s), 1024)
1058
1059         ret = WriteConsoleW(
1060             h, s, count if count else 2, ctypes.byref(written), None)
1061         if ret == 0:
1062             raise OSError('Failed to write string')
1063         if not count:  # We just wrote a non-BMP character
1064             assert written.value == 2
1065             s = s[1:]
1066         else:
1067             assert written.value > 0
1068             s = s[written.value:]
1069     return True
1070
1071
1072 def write_string(s, out=None, encoding=None):
1073     if out is None:
1074         out = sys.stderr
1075     assert type(s) == compat_str
1076
1077     if sys.platform == 'win32' and encoding is None and hasattr(out, 'fileno'):
1078         if _windows_write_string(s, out):
1079             return
1080
1081     if ('b' in getattr(out, 'mode', '') or
1082             sys.version_info[0] < 3):  # Python 2 lies about mode of sys.stderr
1083         byt = s.encode(encoding or preferredencoding(), 'ignore')
1084         out.write(byt)
1085     elif hasattr(out, 'buffer'):
1086         enc = encoding or getattr(out, 'encoding', None) or preferredencoding()
1087         byt = s.encode(enc, 'ignore')
1088         out.buffer.write(byt)
1089     else:
1090         out.write(s)
1091     out.flush()
1092
1093
1094 def bytes_to_intlist(bs):
1095     if not bs:
1096         return []
1097     if isinstance(bs[0], int):  # Python 3
1098         return list(bs)
1099     else:
1100         return [ord(c) for c in bs]
1101
1102
1103 def intlist_to_bytes(xs):
1104     if not xs:
1105         return b''
1106     return struct_pack('%dB' % len(xs), *xs)
1107
1108
1109 # Cross-platform file locking
1110 if sys.platform == 'win32':
1111     import ctypes.wintypes
1112     import msvcrt
1113
1114     class OVERLAPPED(ctypes.Structure):
1115         _fields_ = [
1116             ('Internal', ctypes.wintypes.LPVOID),
1117             ('InternalHigh', ctypes.wintypes.LPVOID),
1118             ('Offset', ctypes.wintypes.DWORD),
1119             ('OffsetHigh', ctypes.wintypes.DWORD),
1120             ('hEvent', ctypes.wintypes.HANDLE),
1121         ]
1122
1123     kernel32 = ctypes.windll.kernel32
1124     LockFileEx = kernel32.LockFileEx
1125     LockFileEx.argtypes = [
1126         ctypes.wintypes.HANDLE,     # hFile
1127         ctypes.wintypes.DWORD,      # dwFlags
1128         ctypes.wintypes.DWORD,      # dwReserved
1129         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
1130         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
1131         ctypes.POINTER(OVERLAPPED)  # Overlapped
1132     ]
1133     LockFileEx.restype = ctypes.wintypes.BOOL
1134     UnlockFileEx = kernel32.UnlockFileEx
1135     UnlockFileEx.argtypes = [
1136         ctypes.wintypes.HANDLE,     # hFile
1137         ctypes.wintypes.DWORD,      # dwReserved
1138         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
1139         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
1140         ctypes.POINTER(OVERLAPPED)  # Overlapped
1141     ]
1142     UnlockFileEx.restype = ctypes.wintypes.BOOL
1143     whole_low = 0xffffffff
1144     whole_high = 0x7fffffff
1145
1146     def _lock_file(f, exclusive):
1147         overlapped = OVERLAPPED()
1148         overlapped.Offset = 0
1149         overlapped.OffsetHigh = 0
1150         overlapped.hEvent = 0
1151         f._lock_file_overlapped_p = ctypes.pointer(overlapped)
1152         handle = msvcrt.get_osfhandle(f.fileno())
1153         if not LockFileEx(handle, 0x2 if exclusive else 0x0, 0,
1154                           whole_low, whole_high, f._lock_file_overlapped_p):
1155             raise OSError('Locking file failed: %r' % ctypes.FormatError())
1156
1157     def _unlock_file(f):
1158         assert f._lock_file_overlapped_p
1159         handle = msvcrt.get_osfhandle(f.fileno())
1160         if not UnlockFileEx(handle, 0,
1161                             whole_low, whole_high, f._lock_file_overlapped_p):
1162             raise OSError('Unlocking file failed: %r' % ctypes.FormatError())
1163
1164 else:
1165     import fcntl
1166
1167     def _lock_file(f, exclusive):
1168         fcntl.flock(f, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
1169
1170     def _unlock_file(f):
1171         fcntl.flock(f, fcntl.LOCK_UN)
1172
1173
1174 class locked_file(object):
1175     def __init__(self, filename, mode, encoding=None):
1176         assert mode in ['r', 'a', 'w']
1177         self.f = io.open(filename, mode, encoding=encoding)
1178         self.mode = mode
1179
1180     def __enter__(self):
1181         exclusive = self.mode != 'r'
1182         try:
1183             _lock_file(self.f, exclusive)
1184         except IOError:
1185             self.f.close()
1186             raise
1187         return self
1188
1189     def __exit__(self, etype, value, traceback):
1190         try:
1191             _unlock_file(self.f)
1192         finally:
1193             self.f.close()
1194
1195     def __iter__(self):
1196         return iter(self.f)
1197
1198     def write(self, *args):
1199         return self.f.write(*args)
1200
1201     def read(self, *args):
1202         return self.f.read(*args)
1203
1204
1205 def get_filesystem_encoding():
1206     encoding = sys.getfilesystemencoding()
1207     return encoding if encoding is not None else 'utf-8'
1208
1209
1210 def shell_quote(args):
1211     quoted_args = []
1212     encoding = get_filesystem_encoding()
1213     for a in args:
1214         if isinstance(a, bytes):
1215             # We may get a filename encoded with 'encodeFilename'
1216             a = a.decode(encoding)
1217         quoted_args.append(pipes.quote(a))
1218     return ' '.join(quoted_args)
1219
1220
1221 def smuggle_url(url, data):
1222     """ Pass additional data in a URL for internal use. """
1223
1224     sdata = compat_urllib_parse.urlencode(
1225         {'__youtubedl_smuggle': json.dumps(data)})
1226     return url + '#' + sdata
1227
1228
1229 def unsmuggle_url(smug_url, default=None):
1230     if '#__youtubedl_smuggle' not in smug_url:
1231         return smug_url, default
1232     url, _, sdata = smug_url.rpartition('#')
1233     jsond = compat_parse_qs(sdata)['__youtubedl_smuggle'][0]
1234     data = json.loads(jsond)
1235     return url, data
1236
1237
1238 def format_bytes(bytes):
1239     if bytes is None:
1240         return 'N/A'
1241     if type(bytes) is str:
1242         bytes = float(bytes)
1243     if bytes == 0.0:
1244         exponent = 0
1245     else:
1246         exponent = int(math.log(bytes, 1024.0))
1247     suffix = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'][exponent]
1248     converted = float(bytes) / float(1024 ** exponent)
1249     return '%.2f%s' % (converted, suffix)
1250
1251
1252 def parse_filesize(s):
1253     if s is None:
1254         return None
1255
1256     # The lower-case forms are of course incorrect and inofficial,
1257     # but we support those too
1258     _UNIT_TABLE = {
1259         'B': 1,
1260         'b': 1,
1261         'KiB': 1024,
1262         'KB': 1000,
1263         'kB': 1024,
1264         'Kb': 1000,
1265         'MiB': 1024 ** 2,
1266         'MB': 1000 ** 2,
1267         'mB': 1024 ** 2,
1268         'Mb': 1000 ** 2,
1269         'GiB': 1024 ** 3,
1270         'GB': 1000 ** 3,
1271         'gB': 1024 ** 3,
1272         'Gb': 1000 ** 3,
1273         'TiB': 1024 ** 4,
1274         'TB': 1000 ** 4,
1275         'tB': 1024 ** 4,
1276         'Tb': 1000 ** 4,
1277         'PiB': 1024 ** 5,
1278         'PB': 1000 ** 5,
1279         'pB': 1024 ** 5,
1280         'Pb': 1000 ** 5,
1281         'EiB': 1024 ** 6,
1282         'EB': 1000 ** 6,
1283         'eB': 1024 ** 6,
1284         'Eb': 1000 ** 6,
1285         'ZiB': 1024 ** 7,
1286         'ZB': 1000 ** 7,
1287         'zB': 1024 ** 7,
1288         'Zb': 1000 ** 7,
1289         'YiB': 1024 ** 8,
1290         'YB': 1000 ** 8,
1291         'yB': 1024 ** 8,
1292         'Yb': 1000 ** 8,
1293     }
1294
1295     units_re = '|'.join(re.escape(u) for u in _UNIT_TABLE)
1296     m = re.match(
1297         r'(?P<num>[0-9]+(?:[,.][0-9]*)?)\s*(?P<unit>%s)' % units_re, s)
1298     if not m:
1299         return None
1300
1301     num_str = m.group('num').replace(',', '.')
1302     mult = _UNIT_TABLE[m.group('unit')]
1303     return int(float(num_str) * mult)
1304
1305
1306 def month_by_name(name):
1307     """ Return the number of a month by (locale-independently) English name """
1308
1309     try:
1310         return ENGLISH_MONTH_NAMES.index(name) + 1
1311     except ValueError:
1312         return None
1313
1314
1315 def month_by_abbreviation(abbrev):
1316     """ Return the number of a month by (locale-independently) English
1317         abbreviations """
1318
1319     try:
1320         return [s[:3] for s in ENGLISH_MONTH_NAMES].index(abbrev) + 1
1321     except ValueError:
1322         return None
1323
1324
1325 def fix_xml_ampersands(xml_str):
1326     """Replace all the '&' by '&amp;' in XML"""
1327     return re.sub(
1328         r'&(?!amp;|lt;|gt;|apos;|quot;|#x[0-9a-fA-F]{,4};|#[0-9]{,4};)',
1329         '&amp;',
1330         xml_str)
1331
1332
1333 def setproctitle(title):
1334     assert isinstance(title, compat_str)
1335     try:
1336         libc = ctypes.cdll.LoadLibrary("libc.so.6")
1337     except OSError:
1338         return
1339     title_bytes = title.encode('utf-8')
1340     buf = ctypes.create_string_buffer(len(title_bytes))
1341     buf.value = title_bytes
1342     try:
1343         libc.prctl(15, buf, 0, 0, 0)
1344     except AttributeError:
1345         return  # Strange libc, just skip this
1346
1347
1348 def remove_start(s, start):
1349     if s.startswith(start):
1350         return s[len(start):]
1351     return s
1352
1353
1354 def remove_end(s, end):
1355     if s.endswith(end):
1356         return s[:-len(end)]
1357     return s
1358
1359
1360 def url_basename(url):
1361     path = compat_urlparse.urlparse(url).path
1362     return path.strip('/').split('/')[-1]
1363
1364
1365 class HEADRequest(compat_urllib_request.Request):
1366     def get_method(self):
1367         return "HEAD"
1368
1369
1370 def int_or_none(v, scale=1, default=None, get_attr=None, invscale=1):
1371     if get_attr:
1372         if v is not None:
1373             v = getattr(v, get_attr, None)
1374     if v == '':
1375         v = None
1376     return default if v is None else (int(v) * invscale // scale)
1377
1378
1379 def str_or_none(v, default=None):
1380     return default if v is None else compat_str(v)
1381
1382
1383 def str_to_int(int_str):
1384     """ A more relaxed version of int_or_none """
1385     if int_str is None:
1386         return None
1387     int_str = re.sub(r'[,\.\+]', '', int_str)
1388     return int(int_str)
1389
1390
1391 def float_or_none(v, scale=1, invscale=1, default=None):
1392     return default if v is None else (float(v) * invscale / scale)
1393
1394
1395 def parse_duration(s):
1396     if not isinstance(s, compat_basestring):
1397         return None
1398
1399     s = s.strip()
1400
1401     m = re.match(
1402         r'''(?ix)(?:P?T)?
1403         (?:
1404             (?P<only_mins>[0-9.]+)\s*(?:mins?\.?|minutes?)\s*|
1405             (?P<only_hours>[0-9.]+)\s*(?:hours?)|
1406
1407             \s*(?P<hours_reversed>[0-9]+)\s*(?:[:h]|hours?)\s*(?P<mins_reversed>[0-9]+)\s*(?:[:m]|mins?\.?|minutes?)\s*|
1408             (?:
1409                 (?:
1410                     (?:(?P<days>[0-9]+)\s*(?:[:d]|days?)\s*)?
1411                     (?P<hours>[0-9]+)\s*(?:[:h]|hours?)\s*
1412                 )?
1413                 (?P<mins>[0-9]+)\s*(?:[:m]|mins?|minutes?)\s*
1414             )?
1415             (?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?\s*(?:s|secs?|seconds?)?
1416         )$''', s)
1417     if not m:
1418         return None
1419     res = 0
1420     if m.group('only_mins'):
1421         return float_or_none(m.group('only_mins'), invscale=60)
1422     if m.group('only_hours'):
1423         return float_or_none(m.group('only_hours'), invscale=60 * 60)
1424     if m.group('secs'):
1425         res += int(m.group('secs'))
1426     if m.group('mins_reversed'):
1427         res += int(m.group('mins_reversed')) * 60
1428     if m.group('mins'):
1429         res += int(m.group('mins')) * 60
1430     if m.group('hours'):
1431         res += int(m.group('hours')) * 60 * 60
1432     if m.group('hours_reversed'):
1433         res += int(m.group('hours_reversed')) * 60 * 60
1434     if m.group('days'):
1435         res += int(m.group('days')) * 24 * 60 * 60
1436     if m.group('ms'):
1437         res += float(m.group('ms'))
1438     return res
1439
1440
1441 def prepend_extension(filename, ext, expected_real_ext=None):
1442     name, real_ext = os.path.splitext(filename)
1443     return (
1444         '{0}.{1}{2}'.format(name, ext, real_ext)
1445         if not expected_real_ext or real_ext[1:] == expected_real_ext
1446         else '{0}.{1}'.format(filename, ext))
1447
1448
1449 def replace_extension(filename, ext, expected_real_ext=None):
1450     name, real_ext = os.path.splitext(filename)
1451     return '{0}.{1}'.format(
1452         name if not expected_real_ext or real_ext[1:] == expected_real_ext else filename,
1453         ext)
1454
1455
1456 def check_executable(exe, args=[]):
1457     """ Checks if the given binary is installed somewhere in PATH, and returns its name.
1458     args can be a list of arguments for a short output (like -version) """
1459     try:
1460         subprocess.Popen([exe] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
1461     except OSError:
1462         return False
1463     return exe
1464
1465
1466 def get_exe_version(exe, args=['--version'],
1467                     version_re=None, unrecognized='present'):
1468     """ Returns the version of the specified executable,
1469     or False if the executable is not present """
1470     try:
1471         out, _ = subprocess.Popen(
1472             [encodeArgument(exe)] + args,
1473             stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()
1474     except OSError:
1475         return False
1476     if isinstance(out, bytes):  # Python 2.x
1477         out = out.decode('ascii', 'ignore')
1478     return detect_exe_version(out, version_re, unrecognized)
1479
1480
1481 def detect_exe_version(output, version_re=None, unrecognized='present'):
1482     assert isinstance(output, compat_str)
1483     if version_re is None:
1484         version_re = r'version\s+([-0-9._a-zA-Z]+)'
1485     m = re.search(version_re, output)
1486     if m:
1487         return m.group(1)
1488     else:
1489         return unrecognized
1490
1491
1492 class PagedList(object):
1493     def __len__(self):
1494         # This is only useful for tests
1495         return len(self.getslice())
1496
1497
1498 class OnDemandPagedList(PagedList):
1499     def __init__(self, pagefunc, pagesize):
1500         self._pagefunc = pagefunc
1501         self._pagesize = pagesize
1502
1503     def getslice(self, start=0, end=None):
1504         res = []
1505         for pagenum in itertools.count(start // self._pagesize):
1506             firstid = pagenum * self._pagesize
1507             nextfirstid = pagenum * self._pagesize + self._pagesize
1508             if start >= nextfirstid:
1509                 continue
1510
1511             page_results = list(self._pagefunc(pagenum))
1512
1513             startv = (
1514                 start % self._pagesize
1515                 if firstid <= start < nextfirstid
1516                 else 0)
1517
1518             endv = (
1519                 ((end - 1) % self._pagesize) + 1
1520                 if (end is not None and firstid <= end <= nextfirstid)
1521                 else None)
1522
1523             if startv != 0 or endv is not None:
1524                 page_results = page_results[startv:endv]
1525             res.extend(page_results)
1526
1527             # A little optimization - if current page is not "full", ie. does
1528             # not contain page_size videos then we can assume that this page
1529             # is the last one - there are no more ids on further pages -
1530             # i.e. no need to query again.
1531             if len(page_results) + startv < self._pagesize:
1532                 break
1533
1534             # If we got the whole page, but the next page is not interesting,
1535             # break out early as well
1536             if end == nextfirstid:
1537                 break
1538         return res
1539
1540
1541 class InAdvancePagedList(PagedList):
1542     def __init__(self, pagefunc, pagecount, pagesize):
1543         self._pagefunc = pagefunc
1544         self._pagecount = pagecount
1545         self._pagesize = pagesize
1546
1547     def getslice(self, start=0, end=None):
1548         res = []
1549         start_page = start // self._pagesize
1550         end_page = (
1551             self._pagecount if end is None else (end // self._pagesize + 1))
1552         skip_elems = start - start_page * self._pagesize
1553         only_more = None if end is None else end - start
1554         for pagenum in range(start_page, end_page):
1555             page = list(self._pagefunc(pagenum))
1556             if skip_elems:
1557                 page = page[skip_elems:]
1558                 skip_elems = None
1559             if only_more is not None:
1560                 if len(page) < only_more:
1561                     only_more -= len(page)
1562                 else:
1563                     page = page[:only_more]
1564                     res.extend(page)
1565                     break
1566             res.extend(page)
1567         return res
1568
1569
1570 def uppercase_escape(s):
1571     unicode_escape = codecs.getdecoder('unicode_escape')
1572     return re.sub(
1573         r'\\U[0-9a-fA-F]{8}',
1574         lambda m: unicode_escape(m.group(0))[0],
1575         s)
1576
1577
1578 def lowercase_escape(s):
1579     unicode_escape = codecs.getdecoder('unicode_escape')
1580     return re.sub(
1581         r'\\u[0-9a-fA-F]{4}',
1582         lambda m: unicode_escape(m.group(0))[0],
1583         s)
1584
1585
1586 def escape_rfc3986(s):
1587     """Escape non-ASCII characters as suggested by RFC 3986"""
1588     if sys.version_info < (3, 0) and isinstance(s, compat_str):
1589         s = s.encode('utf-8')
1590     return compat_urllib_parse.quote(s, b"%/;:@&=+$,!~*'()?#[]")
1591
1592
1593 def escape_url(url):
1594     """Escape URL as suggested by RFC 3986"""
1595     url_parsed = compat_urllib_parse_urlparse(url)
1596     return url_parsed._replace(
1597         path=escape_rfc3986(url_parsed.path),
1598         params=escape_rfc3986(url_parsed.params),
1599         query=escape_rfc3986(url_parsed.query),
1600         fragment=escape_rfc3986(url_parsed.fragment)
1601     ).geturl()
1602
1603 try:
1604     struct.pack('!I', 0)
1605 except TypeError:
1606     # In Python 2.6 (and some 2.7 versions), struct requires a bytes argument
1607     def struct_pack(spec, *args):
1608         if isinstance(spec, compat_str):
1609             spec = spec.encode('ascii')
1610         return struct.pack(spec, *args)
1611
1612     def struct_unpack(spec, *args):
1613         if isinstance(spec, compat_str):
1614             spec = spec.encode('ascii')
1615         return struct.unpack(spec, *args)
1616 else:
1617     struct_pack = struct.pack
1618     struct_unpack = struct.unpack
1619
1620
1621 def read_batch_urls(batch_fd):
1622     def fixup(url):
1623         if not isinstance(url, compat_str):
1624             url = url.decode('utf-8', 'replace')
1625         BOM_UTF8 = '\xef\xbb\xbf'
1626         if url.startswith(BOM_UTF8):
1627             url = url[len(BOM_UTF8):]
1628         url = url.strip()
1629         if url.startswith(('#', ';', ']')):
1630             return False
1631         return url
1632
1633     with contextlib.closing(batch_fd) as fd:
1634         return [url for url in map(fixup, fd) if url]
1635
1636
1637 def urlencode_postdata(*args, **kargs):
1638     return compat_urllib_parse.urlencode(*args, **kargs).encode('ascii')
1639
1640
1641 def encode_dict(d, encoding='utf-8'):
1642     return dict((k.encode(encoding), v.encode(encoding)) for k, v in d.items())
1643
1644
1645 try:
1646     etree_iter = xml.etree.ElementTree.Element.iter
1647 except AttributeError:  # Python <=2.6
1648     etree_iter = lambda n: n.findall('.//*')
1649
1650
1651 def parse_xml(s):
1652     class TreeBuilder(xml.etree.ElementTree.TreeBuilder):
1653         def doctype(self, name, pubid, system):
1654             pass  # Ignore doctypes
1655
1656     parser = xml.etree.ElementTree.XMLParser(target=TreeBuilder())
1657     kwargs = {'parser': parser} if sys.version_info >= (2, 7) else {}
1658     tree = xml.etree.ElementTree.XML(s.encode('utf-8'), **kwargs)
1659     # Fix up XML parser in Python 2.x
1660     if sys.version_info < (3, 0):
1661         for n in etree_iter(tree):
1662             if n.text is not None:
1663                 if not isinstance(n.text, compat_str):
1664                     n.text = n.text.decode('utf-8')
1665     return tree
1666
1667
1668 US_RATINGS = {
1669     'G': 0,
1670     'PG': 10,
1671     'PG-13': 13,
1672     'R': 16,
1673     'NC': 18,
1674 }
1675
1676
1677 def parse_age_limit(s):
1678     if s is None:
1679         return None
1680     m = re.match(r'^(?P<age>\d{1,2})\+?$', s)
1681     return int(m.group('age')) if m else US_RATINGS.get(s, None)
1682
1683
1684 def strip_jsonp(code):
1685     return re.sub(
1686         r'(?s)^[a-zA-Z0-9_]+\s*\(\s*(.*)\);?\s*?(?://[^\n]*)*$', r'\1', code)
1687
1688
1689 def js_to_json(code):
1690     def fix_kv(m):
1691         v = m.group(0)
1692         if v in ('true', 'false', 'null'):
1693             return v
1694         if v.startswith('"'):
1695             return v
1696         if v.startswith("'"):
1697             v = v[1:-1]
1698             v = re.sub(r"\\\\|\\'|\"", lambda m: {
1699                 '\\\\': '\\\\',
1700                 "\\'": "'",
1701                 '"': '\\"',
1702             }[m.group(0)], v)
1703         return '"%s"' % v
1704
1705     res = re.sub(r'''(?x)
1706         "(?:[^"\\]*(?:\\\\|\\['"nu]))*[^"\\]*"|
1707         '(?:[^'\\]*(?:\\\\|\\['"nu]))*[^'\\]*'|
1708         [a-zA-Z_][.a-zA-Z_0-9]*
1709         ''', fix_kv, code)
1710     res = re.sub(r',(\s*[\]}])', lambda m: m.group(1), res)
1711     return res
1712
1713
1714 def qualities(quality_ids):
1715     """ Get a numeric quality value out of a list of possible values """
1716     def q(qid):
1717         try:
1718             return quality_ids.index(qid)
1719         except ValueError:
1720             return -1
1721     return q
1722
1723
1724 DEFAULT_OUTTMPL = '%(title)s-%(id)s.%(ext)s'
1725
1726
1727 def limit_length(s, length):
1728     """ Add ellipses to overly long strings """
1729     if s is None:
1730         return None
1731     ELLIPSES = '...'
1732     if len(s) > length:
1733         return s[:length - len(ELLIPSES)] + ELLIPSES
1734     return s
1735
1736
1737 def version_tuple(v):
1738     return tuple(int(e) for e in re.split(r'[-.]', v))
1739
1740
1741 def is_outdated_version(version, limit, assume_new=True):
1742     if not version:
1743         return not assume_new
1744     try:
1745         return version_tuple(version) < version_tuple(limit)
1746     except ValueError:
1747         return not assume_new
1748
1749
1750 def ytdl_is_updateable():
1751     """ Returns if youtube-dl can be updated with -U """
1752     from zipimport import zipimporter
1753
1754     return isinstance(globals().get('__loader__'), zipimporter) or hasattr(sys, 'frozen')
1755
1756
1757 def args_to_str(args):
1758     # Get a short string representation for a subprocess command
1759     return ' '.join(shlex_quote(a) for a in args)
1760
1761
1762 def mimetype2ext(mt):
1763     _, _, res = mt.rpartition('/')
1764
1765     return {
1766         'x-ms-wmv': 'wmv',
1767         'x-mp4-fragmented': 'mp4',
1768         'ttml+xml': 'ttml',
1769     }.get(res, res)
1770
1771
1772 def urlhandle_detect_ext(url_handle):
1773     try:
1774         url_handle.headers
1775         getheader = lambda h: url_handle.headers[h]
1776     except AttributeError:  # Python < 3
1777         getheader = url_handle.info().getheader
1778
1779     cd = getheader('Content-Disposition')
1780     if cd:
1781         m = re.match(r'attachment;\s*filename="(?P<filename>[^"]+)"', cd)
1782         if m:
1783             e = determine_ext(m.group('filename'), default_ext=None)
1784             if e:
1785                 return e
1786
1787     return mimetype2ext(getheader('Content-Type'))
1788
1789
1790 def age_restricted(content_limit, age_limit):
1791     """ Returns True iff the content should be blocked """
1792
1793     if age_limit is None:  # No limit set
1794         return False
1795     if content_limit is None:
1796         return False  # Content available for everyone
1797     return age_limit < content_limit
1798
1799
1800 def is_html(first_bytes):
1801     """ Detect whether a file contains HTML by examining its first bytes. """
1802
1803     BOMS = [
1804         (b'\xef\xbb\xbf', 'utf-8'),
1805         (b'\x00\x00\xfe\xff', 'utf-32-be'),
1806         (b'\xff\xfe\x00\x00', 'utf-32-le'),
1807         (b'\xff\xfe', 'utf-16-le'),
1808         (b'\xfe\xff', 'utf-16-be'),
1809     ]
1810     for bom, enc in BOMS:
1811         if first_bytes.startswith(bom):
1812             s = first_bytes[len(bom):].decode(enc, 'replace')
1813             break
1814     else:
1815         s = first_bytes.decode('utf-8', 'replace')
1816
1817     return re.match(r'^\s*<', s)
1818
1819
1820 def determine_protocol(info_dict):
1821     protocol = info_dict.get('protocol')
1822     if protocol is not None:
1823         return protocol
1824
1825     url = info_dict['url']
1826     if url.startswith('rtmp'):
1827         return 'rtmp'
1828     elif url.startswith('mms'):
1829         return 'mms'
1830     elif url.startswith('rtsp'):
1831         return 'rtsp'
1832
1833     ext = determine_ext(url)
1834     if ext == 'm3u8':
1835         return 'm3u8'
1836     elif ext == 'f4m':
1837         return 'f4m'
1838
1839     return compat_urllib_parse_urlparse(url).scheme
1840
1841
1842 def render_table(header_row, data):
1843     """ Render a list of rows, each as a list of values """
1844     table = [header_row] + data
1845     max_lens = [max(len(compat_str(v)) for v in col) for col in zip(*table)]
1846     format_str = ' '.join('%-' + compat_str(ml + 1) + 's' for ml in max_lens[:-1]) + '%s'
1847     return '\n'.join(format_str % tuple(row) for row in table)
1848
1849
1850 def _match_one(filter_part, dct):
1851     COMPARISON_OPERATORS = {
1852         '<': operator.lt,
1853         '<=': operator.le,
1854         '>': operator.gt,
1855         '>=': operator.ge,
1856         '=': operator.eq,
1857         '!=': operator.ne,
1858     }
1859     operator_rex = re.compile(r'''(?x)\s*
1860         (?P<key>[a-z_]+)
1861         \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
1862         (?:
1863             (?P<intval>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)|
1864             (?P<strval>(?![0-9.])[a-z0-9A-Z]*)
1865         )
1866         \s*$
1867         ''' % '|'.join(map(re.escape, COMPARISON_OPERATORS.keys())))
1868     m = operator_rex.search(filter_part)
1869     if m:
1870         op = COMPARISON_OPERATORS[m.group('op')]
1871         if m.group('strval') is not None:
1872             if m.group('op') not in ('=', '!='):
1873                 raise ValueError(
1874                     'Operator %s does not support string values!' % m.group('op'))
1875             comparison_value = m.group('strval')
1876         else:
1877             try:
1878                 comparison_value = int(m.group('intval'))
1879             except ValueError:
1880                 comparison_value = parse_filesize(m.group('intval'))
1881                 if comparison_value is None:
1882                     comparison_value = parse_filesize(m.group('intval') + 'B')
1883                 if comparison_value is None:
1884                     raise ValueError(
1885                         'Invalid integer value %r in filter part %r' % (
1886                             m.group('intval'), filter_part))
1887         actual_value = dct.get(m.group('key'))
1888         if actual_value is None:
1889             return m.group('none_inclusive')
1890         return op(actual_value, comparison_value)
1891
1892     UNARY_OPERATORS = {
1893         '': lambda v: v is not None,
1894         '!': lambda v: v is None,
1895     }
1896     operator_rex = re.compile(r'''(?x)\s*
1897         (?P<op>%s)\s*(?P<key>[a-z_]+)
1898         \s*$
1899         ''' % '|'.join(map(re.escape, UNARY_OPERATORS.keys())))
1900     m = operator_rex.search(filter_part)
1901     if m:
1902         op = UNARY_OPERATORS[m.group('op')]
1903         actual_value = dct.get(m.group('key'))
1904         return op(actual_value)
1905
1906     raise ValueError('Invalid filter part %r' % filter_part)
1907
1908
1909 def match_str(filter_str, dct):
1910     """ Filter a dictionary with a simple string syntax. Returns True (=passes filter) or false """
1911
1912     return all(
1913         _match_one(filter_part, dct) for filter_part in filter_str.split('&'))
1914
1915
1916 def match_filter_func(filter_str):
1917     def _match_func(info_dict):
1918         if match_str(filter_str, info_dict):
1919             return None
1920         else:
1921             video_title = info_dict.get('title', info_dict.get('id', 'video'))
1922             return '%s does not pass filter %s, skipping ..' % (video_title, filter_str)
1923     return _match_func
1924
1925
1926 def parse_dfxp_time_expr(time_expr):
1927     if not time_expr:
1928         return 0.0
1929
1930     mobj = re.match(r'^(?P<time_offset>\d+(?:\.\d+)?)s?$', time_expr)
1931     if mobj:
1932         return float(mobj.group('time_offset'))
1933
1934     mobj = re.match(r'^(\d+):(\d\d):(\d\d(?:\.\d+)?)$', time_expr)
1935     if mobj:
1936         return 3600 * int(mobj.group(1)) + 60 * int(mobj.group(2)) + float(mobj.group(3))
1937
1938
1939 def srt_subtitles_timecode(seconds):
1940     return '%02d:%02d:%02d,%03d' % (seconds / 3600, (seconds % 3600) / 60, seconds % 60, (seconds % 1) * 1000)
1941
1942
1943 def dfxp2srt(dfxp_data):
1944     _x = functools.partial(xpath_with_ns, ns_map={
1945         'ttml': 'http://www.w3.org/ns/ttml',
1946         'ttaf1': 'http://www.w3.org/2006/10/ttaf1',
1947     })
1948
1949     def parse_node(node):
1950         str_or_empty = functools.partial(str_or_none, default='')
1951
1952         out = str_or_empty(node.text)
1953
1954         for child in node:
1955             if child.tag in (_x('ttml:br'), _x('ttaf1:br'), 'br'):
1956                 out += '\n' + str_or_empty(child.tail)
1957             elif child.tag in (_x('ttml:span'), _x('ttaf1:span'), 'span'):
1958                 out += str_or_empty(parse_node(child))
1959             else:
1960                 out += str_or_empty(xml.etree.ElementTree.tostring(child))
1961
1962         return out
1963
1964     dfxp = xml.etree.ElementTree.fromstring(dfxp_data.encode('utf-8'))
1965     out = []
1966     paras = dfxp.findall(_x('.//ttml:p')) or dfxp.findall(_x('.//ttaf1:p')) or dfxp.findall('.//p')
1967
1968     if not paras:
1969         raise ValueError('Invalid dfxp/TTML subtitle')
1970
1971     for para, index in zip(paras, itertools.count(1)):
1972         begin_time = parse_dfxp_time_expr(para.attrib['begin'])
1973         end_time = parse_dfxp_time_expr(para.attrib.get('end'))
1974         if not end_time:
1975             end_time = begin_time + parse_dfxp_time_expr(para.attrib['dur'])
1976         out.append('%d\n%s --> %s\n%s\n\n' % (
1977             index,
1978             srt_subtitles_timecode(begin_time),
1979             srt_subtitles_timecode(end_time),
1980             parse_node(para)))
1981
1982     return ''.join(out)
1983
1984
1985 def cli_option(params, command_option, param):
1986     param = params.get(param)
1987     return [command_option, param] if param is not None else []
1988
1989
1990 def cli_bool_option(params, command_option, param, true_value='true', false_value='false', separator=None):
1991     param = params.get(param)
1992     assert isinstance(param, bool)
1993     if separator:
1994         return [command_option + separator + (true_value if param else false_value)]
1995     return [command_option, true_value if param else false_value]
1996
1997
1998 def cli_valueless_option(params, command_option, param, expected_value=True):
1999     param = params.get(param)
2000     return [command_option] if param == expected_value else []
2001
2002
2003 def cli_configuration_args(params, param, default=[]):
2004     ex_args = params.get(param)
2005     if ex_args is None:
2006         return default
2007     assert isinstance(ex_args, list)
2008     return ex_args
2009
2010
2011 class ISO639Utils(object):
2012     # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
2013     _lang_map = {
2014         'aa': 'aar',
2015         'ab': 'abk',
2016         'ae': 'ave',
2017         'af': 'afr',
2018         'ak': 'aka',
2019         'am': 'amh',
2020         'an': 'arg',
2021         'ar': 'ara',
2022         'as': 'asm',
2023         'av': 'ava',
2024         'ay': 'aym',
2025         'az': 'aze',
2026         'ba': 'bak',
2027         'be': 'bel',
2028         'bg': 'bul',
2029         'bh': 'bih',
2030         'bi': 'bis',
2031         'bm': 'bam',
2032         'bn': 'ben',
2033         'bo': 'bod',
2034         'br': 'bre',
2035         'bs': 'bos',
2036         'ca': 'cat',
2037         'ce': 'che',
2038         'ch': 'cha',
2039         'co': 'cos',
2040         'cr': 'cre',
2041         'cs': 'ces',
2042         'cu': 'chu',
2043         'cv': 'chv',
2044         'cy': 'cym',
2045         'da': 'dan',
2046         'de': 'deu',
2047         'dv': 'div',
2048         'dz': 'dzo',
2049         'ee': 'ewe',
2050         'el': 'ell',
2051         'en': 'eng',
2052         'eo': 'epo',
2053         'es': 'spa',
2054         'et': 'est',
2055         'eu': 'eus',
2056         'fa': 'fas',
2057         'ff': 'ful',
2058         'fi': 'fin',
2059         'fj': 'fij',
2060         'fo': 'fao',
2061         'fr': 'fra',
2062         'fy': 'fry',
2063         'ga': 'gle',
2064         'gd': 'gla',
2065         'gl': 'glg',
2066         'gn': 'grn',
2067         'gu': 'guj',
2068         'gv': 'glv',
2069         'ha': 'hau',
2070         'he': 'heb',
2071         'hi': 'hin',
2072         'ho': 'hmo',
2073         'hr': 'hrv',
2074         'ht': 'hat',
2075         'hu': 'hun',
2076         'hy': 'hye',
2077         'hz': 'her',
2078         'ia': 'ina',
2079         'id': 'ind',
2080         'ie': 'ile',
2081         'ig': 'ibo',
2082         'ii': 'iii',
2083         'ik': 'ipk',
2084         'io': 'ido',
2085         'is': 'isl',
2086         'it': 'ita',
2087         'iu': 'iku',
2088         'ja': 'jpn',
2089         'jv': 'jav',
2090         'ka': 'kat',
2091         'kg': 'kon',
2092         'ki': 'kik',
2093         'kj': 'kua',
2094         'kk': 'kaz',
2095         'kl': 'kal',
2096         'km': 'khm',
2097         'kn': 'kan',
2098         'ko': 'kor',
2099         'kr': 'kau',
2100         'ks': 'kas',
2101         'ku': 'kur',
2102         'kv': 'kom',
2103         'kw': 'cor',
2104         'ky': 'kir',
2105         'la': 'lat',
2106         'lb': 'ltz',
2107         'lg': 'lug',
2108         'li': 'lim',
2109         'ln': 'lin',
2110         'lo': 'lao',
2111         'lt': 'lit',
2112         'lu': 'lub',
2113         'lv': 'lav',
2114         'mg': 'mlg',
2115         'mh': 'mah',
2116         'mi': 'mri',
2117         'mk': 'mkd',
2118         'ml': 'mal',
2119         'mn': 'mon',
2120         'mr': 'mar',
2121         'ms': 'msa',
2122         'mt': 'mlt',
2123         'my': 'mya',
2124         'na': 'nau',
2125         'nb': 'nob',
2126         'nd': 'nde',
2127         'ne': 'nep',
2128         'ng': 'ndo',
2129         'nl': 'nld',
2130         'nn': 'nno',
2131         'no': 'nor',
2132         'nr': 'nbl',
2133         'nv': 'nav',
2134         'ny': 'nya',
2135         'oc': 'oci',
2136         'oj': 'oji',
2137         'om': 'orm',
2138         'or': 'ori',
2139         'os': 'oss',
2140         'pa': 'pan',
2141         'pi': 'pli',
2142         'pl': 'pol',
2143         'ps': 'pus',
2144         'pt': 'por',
2145         'qu': 'que',
2146         'rm': 'roh',
2147         'rn': 'run',
2148         'ro': 'ron',
2149         'ru': 'rus',
2150         'rw': 'kin',
2151         'sa': 'san',
2152         'sc': 'srd',
2153         'sd': 'snd',
2154         'se': 'sme',
2155         'sg': 'sag',
2156         'si': 'sin',
2157         'sk': 'slk',
2158         'sl': 'slv',
2159         'sm': 'smo',
2160         'sn': 'sna',
2161         'so': 'som',
2162         'sq': 'sqi',
2163         'sr': 'srp',
2164         'ss': 'ssw',
2165         'st': 'sot',
2166         'su': 'sun',
2167         'sv': 'swe',
2168         'sw': 'swa',
2169         'ta': 'tam',
2170         'te': 'tel',
2171         'tg': 'tgk',
2172         'th': 'tha',
2173         'ti': 'tir',
2174         'tk': 'tuk',
2175         'tl': 'tgl',
2176         'tn': 'tsn',
2177         'to': 'ton',
2178         'tr': 'tur',
2179         'ts': 'tso',
2180         'tt': 'tat',
2181         'tw': 'twi',
2182         'ty': 'tah',
2183         'ug': 'uig',
2184         'uk': 'ukr',
2185         'ur': 'urd',
2186         'uz': 'uzb',
2187         've': 'ven',
2188         'vi': 'vie',
2189         'vo': 'vol',
2190         'wa': 'wln',
2191         'wo': 'wol',
2192         'xh': 'xho',
2193         'yi': 'yid',
2194         'yo': 'yor',
2195         'za': 'zha',
2196         'zh': 'zho',
2197         'zu': 'zul',
2198     }
2199
2200     @classmethod
2201     def short2long(cls, code):
2202         """Convert language code from ISO 639-1 to ISO 639-2/T"""
2203         return cls._lang_map.get(code[:2])
2204
2205     @classmethod
2206     def long2short(cls, code):
2207         """Convert language code from ISO 639-2/T to ISO 639-1"""
2208         for short_name, long_name in cls._lang_map.items():
2209             if long_name == code:
2210                 return short_name
2211
2212
2213 class ISO3166Utils(object):
2214     # From http://data.okfn.org/data/core/country-list
2215     _country_map = {
2216         'AF': 'Afghanistan',
2217         'AX': 'Åland Islands',
2218         'AL': 'Albania',
2219         'DZ': 'Algeria',
2220         'AS': 'American Samoa',
2221         'AD': 'Andorra',
2222         'AO': 'Angola',
2223         'AI': 'Anguilla',
2224         'AQ': 'Antarctica',
2225         'AG': 'Antigua and Barbuda',
2226         'AR': 'Argentina',
2227         'AM': 'Armenia',
2228         'AW': 'Aruba',
2229         'AU': 'Australia',
2230         'AT': 'Austria',
2231         'AZ': 'Azerbaijan',
2232         'BS': 'Bahamas',
2233         'BH': 'Bahrain',
2234         'BD': 'Bangladesh',
2235         'BB': 'Barbados',
2236         'BY': 'Belarus',
2237         'BE': 'Belgium',
2238         'BZ': 'Belize',
2239         'BJ': 'Benin',
2240         'BM': 'Bermuda',
2241         'BT': 'Bhutan',
2242         'BO': 'Bolivia, Plurinational State of',
2243         'BQ': 'Bonaire, Sint Eustatius and Saba',
2244         'BA': 'Bosnia and Herzegovina',
2245         'BW': 'Botswana',
2246         'BV': 'Bouvet Island',
2247         'BR': 'Brazil',
2248         'IO': 'British Indian Ocean Territory',
2249         'BN': 'Brunei Darussalam',
2250         'BG': 'Bulgaria',
2251         'BF': 'Burkina Faso',
2252         'BI': 'Burundi',
2253         'KH': 'Cambodia',
2254         'CM': 'Cameroon',
2255         'CA': 'Canada',
2256         'CV': 'Cape Verde',
2257         'KY': 'Cayman Islands',
2258         'CF': 'Central African Republic',
2259         'TD': 'Chad',
2260         'CL': 'Chile',
2261         'CN': 'China',
2262         'CX': 'Christmas Island',
2263         'CC': 'Cocos (Keeling) Islands',
2264         'CO': 'Colombia',
2265         'KM': 'Comoros',
2266         'CG': 'Congo',
2267         'CD': 'Congo, the Democratic Republic of the',
2268         'CK': 'Cook Islands',
2269         'CR': 'Costa Rica',
2270         'CI': 'Côte d\'Ivoire',
2271         'HR': 'Croatia',
2272         'CU': 'Cuba',
2273         'CW': 'Curaçao',
2274         'CY': 'Cyprus',
2275         'CZ': 'Czech Republic',
2276         'DK': 'Denmark',
2277         'DJ': 'Djibouti',
2278         'DM': 'Dominica',
2279         'DO': 'Dominican Republic',
2280         'EC': 'Ecuador',
2281         'EG': 'Egypt',
2282         'SV': 'El Salvador',
2283         'GQ': 'Equatorial Guinea',
2284         'ER': 'Eritrea',
2285         'EE': 'Estonia',
2286         'ET': 'Ethiopia',
2287         'FK': 'Falkland Islands (Malvinas)',
2288         'FO': 'Faroe Islands',
2289         'FJ': 'Fiji',
2290         'FI': 'Finland',
2291         'FR': 'France',
2292         'GF': 'French Guiana',
2293         'PF': 'French Polynesia',
2294         'TF': 'French Southern Territories',
2295         'GA': 'Gabon',
2296         'GM': 'Gambia',
2297         'GE': 'Georgia',
2298         'DE': 'Germany',
2299         'GH': 'Ghana',
2300         'GI': 'Gibraltar',
2301         'GR': 'Greece',
2302         'GL': 'Greenland',
2303         'GD': 'Grenada',
2304         'GP': 'Guadeloupe',
2305         'GU': 'Guam',
2306         'GT': 'Guatemala',
2307         'GG': 'Guernsey',
2308         'GN': 'Guinea',
2309         'GW': 'Guinea-Bissau',
2310         'GY': 'Guyana',
2311         'HT': 'Haiti',
2312         'HM': 'Heard Island and McDonald Islands',
2313         'VA': 'Holy See (Vatican City State)',
2314         'HN': 'Honduras',
2315         'HK': 'Hong Kong',
2316         'HU': 'Hungary',
2317         'IS': 'Iceland',
2318         'IN': 'India',
2319         'ID': 'Indonesia',
2320         'IR': 'Iran, Islamic Republic of',
2321         'IQ': 'Iraq',
2322         'IE': 'Ireland',
2323         'IM': 'Isle of Man',
2324         'IL': 'Israel',
2325         'IT': 'Italy',
2326         'JM': 'Jamaica',
2327         'JP': 'Japan',
2328         'JE': 'Jersey',
2329         'JO': 'Jordan',
2330         'KZ': 'Kazakhstan',
2331         'KE': 'Kenya',
2332         'KI': 'Kiribati',
2333         'KP': 'Korea, Democratic People\'s Republic of',
2334         'KR': 'Korea, Republic of',
2335         'KW': 'Kuwait',
2336         'KG': 'Kyrgyzstan',
2337         'LA': 'Lao People\'s Democratic Republic',
2338         'LV': 'Latvia',
2339         'LB': 'Lebanon',
2340         'LS': 'Lesotho',
2341         'LR': 'Liberia',
2342         'LY': 'Libya',
2343         'LI': 'Liechtenstein',
2344         'LT': 'Lithuania',
2345         'LU': 'Luxembourg',
2346         'MO': 'Macao',
2347         'MK': 'Macedonia, the Former Yugoslav Republic of',
2348         'MG': 'Madagascar',
2349         'MW': 'Malawi',
2350         'MY': 'Malaysia',
2351         'MV': 'Maldives',
2352         'ML': 'Mali',
2353         'MT': 'Malta',
2354         'MH': 'Marshall Islands',
2355         'MQ': 'Martinique',
2356         'MR': 'Mauritania',
2357         'MU': 'Mauritius',
2358         'YT': 'Mayotte',
2359         'MX': 'Mexico',
2360         'FM': 'Micronesia, Federated States of',
2361         'MD': 'Moldova, Republic of',
2362         'MC': 'Monaco',
2363         'MN': 'Mongolia',
2364         'ME': 'Montenegro',
2365         'MS': 'Montserrat',
2366         'MA': 'Morocco',
2367         'MZ': 'Mozambique',
2368         'MM': 'Myanmar',
2369         'NA': 'Namibia',
2370         'NR': 'Nauru',
2371         'NP': 'Nepal',
2372         'NL': 'Netherlands',
2373         'NC': 'New Caledonia',
2374         'NZ': 'New Zealand',
2375         'NI': 'Nicaragua',
2376         'NE': 'Niger',
2377         'NG': 'Nigeria',
2378         'NU': 'Niue',
2379         'NF': 'Norfolk Island',
2380         'MP': 'Northern Mariana Islands',
2381         'NO': 'Norway',
2382         'OM': 'Oman',
2383         'PK': 'Pakistan',
2384         'PW': 'Palau',
2385         'PS': 'Palestine, State of',
2386         'PA': 'Panama',
2387         'PG': 'Papua New Guinea',
2388         'PY': 'Paraguay',
2389         'PE': 'Peru',
2390         'PH': 'Philippines',
2391         'PN': 'Pitcairn',
2392         'PL': 'Poland',
2393         'PT': 'Portugal',
2394         'PR': 'Puerto Rico',
2395         'QA': 'Qatar',
2396         'RE': 'Réunion',
2397         'RO': 'Romania',
2398         'RU': 'Russian Federation',
2399         'RW': 'Rwanda',
2400         'BL': 'Saint Barthélemy',
2401         'SH': 'Saint Helena, Ascension and Tristan da Cunha',
2402         'KN': 'Saint Kitts and Nevis',
2403         'LC': 'Saint Lucia',
2404         'MF': 'Saint Martin (French part)',
2405         'PM': 'Saint Pierre and Miquelon',
2406         'VC': 'Saint Vincent and the Grenadines',
2407         'WS': 'Samoa',
2408         'SM': 'San Marino',
2409         'ST': 'Sao Tome and Principe',
2410         'SA': 'Saudi Arabia',
2411         'SN': 'Senegal',
2412         'RS': 'Serbia',
2413         'SC': 'Seychelles',
2414         'SL': 'Sierra Leone',
2415         'SG': 'Singapore',
2416         'SX': 'Sint Maarten (Dutch part)',
2417         'SK': 'Slovakia',
2418         'SI': 'Slovenia',
2419         'SB': 'Solomon Islands',
2420         'SO': 'Somalia',
2421         'ZA': 'South Africa',
2422         'GS': 'South Georgia and the South Sandwich Islands',
2423         'SS': 'South Sudan',
2424         'ES': 'Spain',
2425         'LK': 'Sri Lanka',
2426         'SD': 'Sudan',
2427         'SR': 'Suriname',
2428         'SJ': 'Svalbard and Jan Mayen',
2429         'SZ': 'Swaziland',
2430         'SE': 'Sweden',
2431         'CH': 'Switzerland',
2432         'SY': 'Syrian Arab Republic',
2433         'TW': 'Taiwan, Province of China',
2434         'TJ': 'Tajikistan',
2435         'TZ': 'Tanzania, United Republic of',
2436         'TH': 'Thailand',
2437         'TL': 'Timor-Leste',
2438         'TG': 'Togo',
2439         'TK': 'Tokelau',
2440         'TO': 'Tonga',
2441         'TT': 'Trinidad and Tobago',
2442         'TN': 'Tunisia',
2443         'TR': 'Turkey',
2444         'TM': 'Turkmenistan',
2445         'TC': 'Turks and Caicos Islands',
2446         'TV': 'Tuvalu',
2447         'UG': 'Uganda',
2448         'UA': 'Ukraine',
2449         'AE': 'United Arab Emirates',
2450         'GB': 'United Kingdom',
2451         'US': 'United States',
2452         'UM': 'United States Minor Outlying Islands',
2453         'UY': 'Uruguay',
2454         'UZ': 'Uzbekistan',
2455         'VU': 'Vanuatu',
2456         'VE': 'Venezuela, Bolivarian Republic of',
2457         'VN': 'Viet Nam',
2458         'VG': 'Virgin Islands, British',
2459         'VI': 'Virgin Islands, U.S.',
2460         'WF': 'Wallis and Futuna',
2461         'EH': 'Western Sahara',
2462         'YE': 'Yemen',
2463         'ZM': 'Zambia',
2464         'ZW': 'Zimbabwe',
2465     }
2466
2467     @classmethod
2468     def short2full(cls, code):
2469         """Convert an ISO 3166-2 country code to the corresponding full name"""
2470         return cls._country_map.get(code.upper())
2471
2472
2473 class PerRequestProxyHandler(compat_urllib_request.ProxyHandler):
2474     def __init__(self, proxies=None):
2475         # Set default handlers
2476         for type in ('http', 'https'):
2477             setattr(self, '%s_open' % type,
2478                     lambda r, proxy='__noproxy__', type=type, meth=self.proxy_open:
2479                         meth(r, proxy, type))
2480         return compat_urllib_request.ProxyHandler.__init__(self, proxies)
2481
2482     def proxy_open(self, req, proxy, type):
2483         req_proxy = req.headers.get('Ytdl-request-proxy')
2484         if req_proxy is not None:
2485             proxy = req_proxy
2486             del req.headers['Ytdl-request-proxy']
2487
2488         if proxy == '__noproxy__':
2489             return None  # No Proxy
2490         return compat_urllib_request.ProxyHandler.proxy_open(
2491             self, req, proxy, type)