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