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