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