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