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