[bliptv] remove extractor and add support for site replacement(makertv)
[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     if v is None:
1375         return default
1376     try:
1377         return int(v) * invscale // scale
1378     except ValueError:
1379         return default
1380
1381
1382 def str_or_none(v, default=None):
1383     return default if v is None else compat_str(v)
1384
1385
1386 def str_to_int(int_str):
1387     """ A more relaxed version of int_or_none """
1388     if int_str is None:
1389         return None
1390     int_str = re.sub(r'[,\.\+]', '', int_str)
1391     return int(int_str)
1392
1393
1394 def float_or_none(v, scale=1, invscale=1, default=None):
1395     if v is None:
1396         return default
1397     try:
1398         return float(v) * invscale / scale
1399     except ValueError:
1400         return default
1401
1402
1403 def parse_duration(s):
1404     if not isinstance(s, compat_basestring):
1405         return None
1406
1407     s = s.strip()
1408
1409     m = re.match(
1410         r'''(?ix)(?:P?T)?
1411         (?:
1412             (?P<only_mins>[0-9.]+)\s*(?:mins?\.?|minutes?)\s*|
1413             (?P<only_hours>[0-9.]+)\s*(?:hours?)|
1414
1415             \s*(?P<hours_reversed>[0-9]+)\s*(?:[:h]|hours?)\s*(?P<mins_reversed>[0-9]+)\s*(?:[:m]|mins?\.?|minutes?)\s*|
1416             (?:
1417                 (?:
1418                     (?:(?P<days>[0-9]+)\s*(?:[:d]|days?)\s*)?
1419                     (?P<hours>[0-9]+)\s*(?:[:h]|hours?)\s*
1420                 )?
1421                 (?P<mins>[0-9]+)\s*(?:[:m]|mins?|minutes?)\s*
1422             )?
1423             (?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?\s*(?:s|secs?|seconds?)?
1424         )$''', s)
1425     if not m:
1426         return None
1427     res = 0
1428     if m.group('only_mins'):
1429         return float_or_none(m.group('only_mins'), invscale=60)
1430     if m.group('only_hours'):
1431         return float_or_none(m.group('only_hours'), invscale=60 * 60)
1432     if m.group('secs'):
1433         res += int(m.group('secs'))
1434     if m.group('mins_reversed'):
1435         res += int(m.group('mins_reversed')) * 60
1436     if m.group('mins'):
1437         res += int(m.group('mins')) * 60
1438     if m.group('hours'):
1439         res += int(m.group('hours')) * 60 * 60
1440     if m.group('hours_reversed'):
1441         res += int(m.group('hours_reversed')) * 60 * 60
1442     if m.group('days'):
1443         res += int(m.group('days')) * 24 * 60 * 60
1444     if m.group('ms'):
1445         res += float(m.group('ms'))
1446     return res
1447
1448
1449 def prepend_extension(filename, ext, expected_real_ext=None):
1450     name, real_ext = os.path.splitext(filename)
1451     return (
1452         '{0}.{1}{2}'.format(name, ext, real_ext)
1453         if not expected_real_ext or real_ext[1:] == expected_real_ext
1454         else '{0}.{1}'.format(filename, ext))
1455
1456
1457 def replace_extension(filename, ext, expected_real_ext=None):
1458     name, real_ext = os.path.splitext(filename)
1459     return '{0}.{1}'.format(
1460         name if not expected_real_ext or real_ext[1:] == expected_real_ext else filename,
1461         ext)
1462
1463
1464 def check_executable(exe, args=[]):
1465     """ Checks if the given binary is installed somewhere in PATH, and returns its name.
1466     args can be a list of arguments for a short output (like -version) """
1467     try:
1468         subprocess.Popen([exe] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
1469     except OSError:
1470         return False
1471     return exe
1472
1473
1474 def get_exe_version(exe, args=['--version'],
1475                     version_re=None, unrecognized='present'):
1476     """ Returns the version of the specified executable,
1477     or False if the executable is not present """
1478     try:
1479         out, _ = subprocess.Popen(
1480             [encodeArgument(exe)] + args,
1481             stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()
1482     except OSError:
1483         return False
1484     if isinstance(out, bytes):  # Python 2.x
1485         out = out.decode('ascii', 'ignore')
1486     return detect_exe_version(out, version_re, unrecognized)
1487
1488
1489 def detect_exe_version(output, version_re=None, unrecognized='present'):
1490     assert isinstance(output, compat_str)
1491     if version_re is None:
1492         version_re = r'version\s+([-0-9._a-zA-Z]+)'
1493     m = re.search(version_re, output)
1494     if m:
1495         return m.group(1)
1496     else:
1497         return unrecognized
1498
1499
1500 class PagedList(object):
1501     def __len__(self):
1502         # This is only useful for tests
1503         return len(self.getslice())
1504
1505
1506 class OnDemandPagedList(PagedList):
1507     def __init__(self, pagefunc, pagesize):
1508         self._pagefunc = pagefunc
1509         self._pagesize = pagesize
1510
1511     def getslice(self, start=0, end=None):
1512         res = []
1513         for pagenum in itertools.count(start // self._pagesize):
1514             firstid = pagenum * self._pagesize
1515             nextfirstid = pagenum * self._pagesize + self._pagesize
1516             if start >= nextfirstid:
1517                 continue
1518
1519             page_results = list(self._pagefunc(pagenum))
1520
1521             startv = (
1522                 start % self._pagesize
1523                 if firstid <= start < nextfirstid
1524                 else 0)
1525
1526             endv = (
1527                 ((end - 1) % self._pagesize) + 1
1528                 if (end is not None and firstid <= end <= nextfirstid)
1529                 else None)
1530
1531             if startv != 0 or endv is not None:
1532                 page_results = page_results[startv:endv]
1533             res.extend(page_results)
1534
1535             # A little optimization - if current page is not "full", ie. does
1536             # not contain page_size videos then we can assume that this page
1537             # is the last one - there are no more ids on further pages -
1538             # i.e. no need to query again.
1539             if len(page_results) + startv < self._pagesize:
1540                 break
1541
1542             # If we got the whole page, but the next page is not interesting,
1543             # break out early as well
1544             if end == nextfirstid:
1545                 break
1546         return res
1547
1548
1549 class InAdvancePagedList(PagedList):
1550     def __init__(self, pagefunc, pagecount, pagesize):
1551         self._pagefunc = pagefunc
1552         self._pagecount = pagecount
1553         self._pagesize = pagesize
1554
1555     def getslice(self, start=0, end=None):
1556         res = []
1557         start_page = start // self._pagesize
1558         end_page = (
1559             self._pagecount if end is None else (end // self._pagesize + 1))
1560         skip_elems = start - start_page * self._pagesize
1561         only_more = None if end is None else end - start
1562         for pagenum in range(start_page, end_page):
1563             page = list(self._pagefunc(pagenum))
1564             if skip_elems:
1565                 page = page[skip_elems:]
1566                 skip_elems = None
1567             if only_more is not None:
1568                 if len(page) < only_more:
1569                     only_more -= len(page)
1570                 else:
1571                     page = page[:only_more]
1572                     res.extend(page)
1573                     break
1574             res.extend(page)
1575         return res
1576
1577
1578 def uppercase_escape(s):
1579     unicode_escape = codecs.getdecoder('unicode_escape')
1580     return re.sub(
1581         r'\\U[0-9a-fA-F]{8}',
1582         lambda m: unicode_escape(m.group(0))[0],
1583         s)
1584
1585
1586 def lowercase_escape(s):
1587     unicode_escape = codecs.getdecoder('unicode_escape')
1588     return re.sub(
1589         r'\\u[0-9a-fA-F]{4}',
1590         lambda m: unicode_escape(m.group(0))[0],
1591         s)
1592
1593
1594 def escape_rfc3986(s):
1595     """Escape non-ASCII characters as suggested by RFC 3986"""
1596     if sys.version_info < (3, 0) and isinstance(s, compat_str):
1597         s = s.encode('utf-8')
1598     return compat_urllib_parse.quote(s, b"%/;:@&=+$,!~*'()?#[]")
1599
1600
1601 def escape_url(url):
1602     """Escape URL as suggested by RFC 3986"""
1603     url_parsed = compat_urllib_parse_urlparse(url)
1604     return url_parsed._replace(
1605         path=escape_rfc3986(url_parsed.path),
1606         params=escape_rfc3986(url_parsed.params),
1607         query=escape_rfc3986(url_parsed.query),
1608         fragment=escape_rfc3986(url_parsed.fragment)
1609     ).geturl()
1610
1611 try:
1612     struct.pack('!I', 0)
1613 except TypeError:
1614     # In Python 2.6 (and some 2.7 versions), struct requires a bytes argument
1615     def struct_pack(spec, *args):
1616         if isinstance(spec, compat_str):
1617             spec = spec.encode('ascii')
1618         return struct.pack(spec, *args)
1619
1620     def struct_unpack(spec, *args):
1621         if isinstance(spec, compat_str):
1622             spec = spec.encode('ascii')
1623         return struct.unpack(spec, *args)
1624 else:
1625     struct_pack = struct.pack
1626     struct_unpack = struct.unpack
1627
1628
1629 def read_batch_urls(batch_fd):
1630     def fixup(url):
1631         if not isinstance(url, compat_str):
1632             url = url.decode('utf-8', 'replace')
1633         BOM_UTF8 = '\xef\xbb\xbf'
1634         if url.startswith(BOM_UTF8):
1635             url = url[len(BOM_UTF8):]
1636         url = url.strip()
1637         if url.startswith(('#', ';', ']')):
1638             return False
1639         return url
1640
1641     with contextlib.closing(batch_fd) as fd:
1642         return [url for url in map(fixup, fd) if url]
1643
1644
1645 def urlencode_postdata(*args, **kargs):
1646     return compat_urllib_parse.urlencode(*args, **kargs).encode('ascii')
1647
1648
1649 def encode_dict(d, encoding='utf-8'):
1650     return dict((k.encode(encoding), v.encode(encoding)) for k, v in d.items())
1651
1652
1653 try:
1654     etree_iter = xml.etree.ElementTree.Element.iter
1655 except AttributeError:  # Python <=2.6
1656     etree_iter = lambda n: n.findall('.//*')
1657
1658
1659 def parse_xml(s):
1660     class TreeBuilder(xml.etree.ElementTree.TreeBuilder):
1661         def doctype(self, name, pubid, system):
1662             pass  # Ignore doctypes
1663
1664     parser = xml.etree.ElementTree.XMLParser(target=TreeBuilder())
1665     kwargs = {'parser': parser} if sys.version_info >= (2, 7) else {}
1666     tree = xml.etree.ElementTree.XML(s.encode('utf-8'), **kwargs)
1667     # Fix up XML parser in Python 2.x
1668     if sys.version_info < (3, 0):
1669         for n in etree_iter(tree):
1670             if n.text is not None:
1671                 if not isinstance(n.text, compat_str):
1672                     n.text = n.text.decode('utf-8')
1673     return tree
1674
1675
1676 US_RATINGS = {
1677     'G': 0,
1678     'PG': 10,
1679     'PG-13': 13,
1680     'R': 16,
1681     'NC': 18,
1682 }
1683
1684
1685 def parse_age_limit(s):
1686     if s is None:
1687         return None
1688     m = re.match(r'^(?P<age>\d{1,2})\+?$', s)
1689     return int(m.group('age')) if m else US_RATINGS.get(s, None)
1690
1691
1692 def strip_jsonp(code):
1693     return re.sub(
1694         r'(?s)^[a-zA-Z0-9_]+\s*\(\s*(.*)\);?\s*?(?://[^\n]*)*$', r'\1', code)
1695
1696
1697 def js_to_json(code):
1698     def fix_kv(m):
1699         v = m.group(0)
1700         if v in ('true', 'false', 'null'):
1701             return v
1702         if v.startswith('"'):
1703             return v
1704         if v.startswith("'"):
1705             v = v[1:-1]
1706             v = re.sub(r"\\\\|\\'|\"", lambda m: {
1707                 '\\\\': '\\\\',
1708                 "\\'": "'",
1709                 '"': '\\"',
1710             }[m.group(0)], v)
1711         return '"%s"' % v
1712
1713     res = re.sub(r'''(?x)
1714         "(?:[^"\\]*(?:\\\\|\\['"nu]))*[^"\\]*"|
1715         '(?:[^'\\]*(?:\\\\|\\['"nu]))*[^'\\]*'|
1716         [a-zA-Z_][.a-zA-Z_0-9]*
1717         ''', fix_kv, code)
1718     res = re.sub(r',(\s*[\]}])', lambda m: m.group(1), res)
1719     return res
1720
1721
1722 def qualities(quality_ids):
1723     """ Get a numeric quality value out of a list of possible values """
1724     def q(qid):
1725         try:
1726             return quality_ids.index(qid)
1727         except ValueError:
1728             return -1
1729     return q
1730
1731
1732 DEFAULT_OUTTMPL = '%(title)s-%(id)s.%(ext)s'
1733
1734
1735 def limit_length(s, length):
1736     """ Add ellipses to overly long strings """
1737     if s is None:
1738         return None
1739     ELLIPSES = '...'
1740     if len(s) > length:
1741         return s[:length - len(ELLIPSES)] + ELLIPSES
1742     return s
1743
1744
1745 def version_tuple(v):
1746     return tuple(int(e) for e in re.split(r'[-.]', v))
1747
1748
1749 def is_outdated_version(version, limit, assume_new=True):
1750     if not version:
1751         return not assume_new
1752     try:
1753         return version_tuple(version) < version_tuple(limit)
1754     except ValueError:
1755         return not assume_new
1756
1757
1758 def ytdl_is_updateable():
1759     """ Returns if youtube-dl can be updated with -U """
1760     from zipimport import zipimporter
1761
1762     return isinstance(globals().get('__loader__'), zipimporter) or hasattr(sys, 'frozen')
1763
1764
1765 def args_to_str(args):
1766     # Get a short string representation for a subprocess command
1767     return ' '.join(shlex_quote(a) for a in args)
1768
1769
1770 def mimetype2ext(mt):
1771     _, _, res = mt.rpartition('/')
1772
1773     return {
1774         'x-ms-wmv': 'wmv',
1775         'x-mp4-fragmented': 'mp4',
1776         'ttml+xml': 'ttml',
1777     }.get(res, res)
1778
1779
1780 def urlhandle_detect_ext(url_handle):
1781     try:
1782         url_handle.headers
1783         getheader = lambda h: url_handle.headers[h]
1784     except AttributeError:  # Python < 3
1785         getheader = url_handle.info().getheader
1786
1787     cd = getheader('Content-Disposition')
1788     if cd:
1789         m = re.match(r'attachment;\s*filename="(?P<filename>[^"]+)"', cd)
1790         if m:
1791             e = determine_ext(m.group('filename'), default_ext=None)
1792             if e:
1793                 return e
1794
1795     return mimetype2ext(getheader('Content-Type'))
1796
1797
1798 def age_restricted(content_limit, age_limit):
1799     """ Returns True iff the content should be blocked """
1800
1801     if age_limit is None:  # No limit set
1802         return False
1803     if content_limit is None:
1804         return False  # Content available for everyone
1805     return age_limit < content_limit
1806
1807
1808 def is_html(first_bytes):
1809     """ Detect whether a file contains HTML by examining its first bytes. """
1810
1811     BOMS = [
1812         (b'\xef\xbb\xbf', 'utf-8'),
1813         (b'\x00\x00\xfe\xff', 'utf-32-be'),
1814         (b'\xff\xfe\x00\x00', 'utf-32-le'),
1815         (b'\xff\xfe', 'utf-16-le'),
1816         (b'\xfe\xff', 'utf-16-be'),
1817     ]
1818     for bom, enc in BOMS:
1819         if first_bytes.startswith(bom):
1820             s = first_bytes[len(bom):].decode(enc, 'replace')
1821             break
1822     else:
1823         s = first_bytes.decode('utf-8', 'replace')
1824
1825     return re.match(r'^\s*<', s)
1826
1827
1828 def determine_protocol(info_dict):
1829     protocol = info_dict.get('protocol')
1830     if protocol is not None:
1831         return protocol
1832
1833     url = info_dict['url']
1834     if url.startswith('rtmp'):
1835         return 'rtmp'
1836     elif url.startswith('mms'):
1837         return 'mms'
1838     elif url.startswith('rtsp'):
1839         return 'rtsp'
1840
1841     ext = determine_ext(url)
1842     if ext == 'm3u8':
1843         return 'm3u8'
1844     elif ext == 'f4m':
1845         return 'f4m'
1846
1847     return compat_urllib_parse_urlparse(url).scheme
1848
1849
1850 def render_table(header_row, data):
1851     """ Render a list of rows, each as a list of values """
1852     table = [header_row] + data
1853     max_lens = [max(len(compat_str(v)) for v in col) for col in zip(*table)]
1854     format_str = ' '.join('%-' + compat_str(ml + 1) + 's' for ml in max_lens[:-1]) + '%s'
1855     return '\n'.join(format_str % tuple(row) for row in table)
1856
1857
1858 def _match_one(filter_part, dct):
1859     COMPARISON_OPERATORS = {
1860         '<': operator.lt,
1861         '<=': operator.le,
1862         '>': operator.gt,
1863         '>=': operator.ge,
1864         '=': operator.eq,
1865         '!=': operator.ne,
1866     }
1867     operator_rex = re.compile(r'''(?x)\s*
1868         (?P<key>[a-z_]+)
1869         \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
1870         (?:
1871             (?P<intval>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)|
1872             (?P<strval>(?![0-9.])[a-z0-9A-Z]*)
1873         )
1874         \s*$
1875         ''' % '|'.join(map(re.escape, COMPARISON_OPERATORS.keys())))
1876     m = operator_rex.search(filter_part)
1877     if m:
1878         op = COMPARISON_OPERATORS[m.group('op')]
1879         if m.group('strval') is not None:
1880             if m.group('op') not in ('=', '!='):
1881                 raise ValueError(
1882                     'Operator %s does not support string values!' % m.group('op'))
1883             comparison_value = m.group('strval')
1884         else:
1885             try:
1886                 comparison_value = int(m.group('intval'))
1887             except ValueError:
1888                 comparison_value = parse_filesize(m.group('intval'))
1889                 if comparison_value is None:
1890                     comparison_value = parse_filesize(m.group('intval') + 'B')
1891                 if comparison_value is None:
1892                     raise ValueError(
1893                         'Invalid integer value %r in filter part %r' % (
1894                             m.group('intval'), filter_part))
1895         actual_value = dct.get(m.group('key'))
1896         if actual_value is None:
1897             return m.group('none_inclusive')
1898         return op(actual_value, comparison_value)
1899
1900     UNARY_OPERATORS = {
1901         '': lambda v: v is not None,
1902         '!': lambda v: v is None,
1903     }
1904     operator_rex = re.compile(r'''(?x)\s*
1905         (?P<op>%s)\s*(?P<key>[a-z_]+)
1906         \s*$
1907         ''' % '|'.join(map(re.escape, UNARY_OPERATORS.keys())))
1908     m = operator_rex.search(filter_part)
1909     if m:
1910         op = UNARY_OPERATORS[m.group('op')]
1911         actual_value = dct.get(m.group('key'))
1912         return op(actual_value)
1913
1914     raise ValueError('Invalid filter part %r' % filter_part)
1915
1916
1917 def match_str(filter_str, dct):
1918     """ Filter a dictionary with a simple string syntax. Returns True (=passes filter) or false """
1919
1920     return all(
1921         _match_one(filter_part, dct) for filter_part in filter_str.split('&'))
1922
1923
1924 def match_filter_func(filter_str):
1925     def _match_func(info_dict):
1926         if match_str(filter_str, info_dict):
1927             return None
1928         else:
1929             video_title = info_dict.get('title', info_dict.get('id', 'video'))
1930             return '%s does not pass filter %s, skipping ..' % (video_title, filter_str)
1931     return _match_func
1932
1933
1934 def parse_dfxp_time_expr(time_expr):
1935     if not time_expr:
1936         return 0.0
1937
1938     mobj = re.match(r'^(?P<time_offset>\d+(?:\.\d+)?)s?$', time_expr)
1939     if mobj:
1940         return float(mobj.group('time_offset'))
1941
1942     mobj = re.match(r'^(\d+):(\d\d):(\d\d(?:\.\d+)?)$', time_expr)
1943     if mobj:
1944         return 3600 * int(mobj.group(1)) + 60 * int(mobj.group(2)) + float(mobj.group(3))
1945
1946
1947 def srt_subtitles_timecode(seconds):
1948     return '%02d:%02d:%02d,%03d' % (seconds / 3600, (seconds % 3600) / 60, seconds % 60, (seconds % 1) * 1000)
1949
1950
1951 def dfxp2srt(dfxp_data):
1952     _x = functools.partial(xpath_with_ns, ns_map={
1953         'ttml': 'http://www.w3.org/ns/ttml',
1954         'ttaf1': 'http://www.w3.org/2006/10/ttaf1',
1955     })
1956
1957     def parse_node(node):
1958         str_or_empty = functools.partial(str_or_none, default='')
1959
1960         out = str_or_empty(node.text)
1961
1962         for child in node:
1963             if child.tag in (_x('ttml:br'), _x('ttaf1:br'), 'br'):
1964                 out += '\n' + str_or_empty(child.tail)
1965             elif child.tag in (_x('ttml:span'), _x('ttaf1:span'), 'span'):
1966                 out += str_or_empty(parse_node(child))
1967             else:
1968                 out += str_or_empty(xml.etree.ElementTree.tostring(child))
1969
1970         return out
1971
1972     dfxp = xml.etree.ElementTree.fromstring(dfxp_data.encode('utf-8'))
1973     out = []
1974     paras = dfxp.findall(_x('.//ttml:p')) or dfxp.findall(_x('.//ttaf1:p')) or dfxp.findall('.//p')
1975
1976     if not paras:
1977         raise ValueError('Invalid dfxp/TTML subtitle')
1978
1979     for para, index in zip(paras, itertools.count(1)):
1980         begin_time = parse_dfxp_time_expr(para.attrib['begin'])
1981         end_time = parse_dfxp_time_expr(para.attrib.get('end'))
1982         if not end_time:
1983             end_time = begin_time + parse_dfxp_time_expr(para.attrib['dur'])
1984         out.append('%d\n%s --> %s\n%s\n\n' % (
1985             index,
1986             srt_subtitles_timecode(begin_time),
1987             srt_subtitles_timecode(end_time),
1988             parse_node(para)))
1989
1990     return ''.join(out)
1991
1992
1993 def cli_option(params, command_option, param):
1994     param = params.get(param)
1995     return [command_option, param] if param is not None else []
1996
1997
1998 def cli_bool_option(params, command_option, param, true_value='true', false_value='false', separator=None):
1999     param = params.get(param)
2000     assert isinstance(param, bool)
2001     if separator:
2002         return [command_option + separator + (true_value if param else false_value)]
2003     return [command_option, true_value if param else false_value]
2004
2005
2006 def cli_valueless_option(params, command_option, param, expected_value=True):
2007     param = params.get(param)
2008     return [command_option] if param == expected_value else []
2009
2010
2011 def cli_configuration_args(params, param, default=[]):
2012     ex_args = params.get(param)
2013     if ex_args is None:
2014         return default
2015     assert isinstance(ex_args, list)
2016     return ex_args
2017
2018
2019 class ISO639Utils(object):
2020     # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
2021     _lang_map = {
2022         'aa': 'aar',
2023         'ab': 'abk',
2024         'ae': 'ave',
2025         'af': 'afr',
2026         'ak': 'aka',
2027         'am': 'amh',
2028         'an': 'arg',
2029         'ar': 'ara',
2030         'as': 'asm',
2031         'av': 'ava',
2032         'ay': 'aym',
2033         'az': 'aze',
2034         'ba': 'bak',
2035         'be': 'bel',
2036         'bg': 'bul',
2037         'bh': 'bih',
2038         'bi': 'bis',
2039         'bm': 'bam',
2040         'bn': 'ben',
2041         'bo': 'bod',
2042         'br': 'bre',
2043         'bs': 'bos',
2044         'ca': 'cat',
2045         'ce': 'che',
2046         'ch': 'cha',
2047         'co': 'cos',
2048         'cr': 'cre',
2049         'cs': 'ces',
2050         'cu': 'chu',
2051         'cv': 'chv',
2052         'cy': 'cym',
2053         'da': 'dan',
2054         'de': 'deu',
2055         'dv': 'div',
2056         'dz': 'dzo',
2057         'ee': 'ewe',
2058         'el': 'ell',
2059         'en': 'eng',
2060         'eo': 'epo',
2061         'es': 'spa',
2062         'et': 'est',
2063         'eu': 'eus',
2064         'fa': 'fas',
2065         'ff': 'ful',
2066         'fi': 'fin',
2067         'fj': 'fij',
2068         'fo': 'fao',
2069         'fr': 'fra',
2070         'fy': 'fry',
2071         'ga': 'gle',
2072         'gd': 'gla',
2073         'gl': 'glg',
2074         'gn': 'grn',
2075         'gu': 'guj',
2076         'gv': 'glv',
2077         'ha': 'hau',
2078         'he': 'heb',
2079         'hi': 'hin',
2080         'ho': 'hmo',
2081         'hr': 'hrv',
2082         'ht': 'hat',
2083         'hu': 'hun',
2084         'hy': 'hye',
2085         'hz': 'her',
2086         'ia': 'ina',
2087         'id': 'ind',
2088         'ie': 'ile',
2089         'ig': 'ibo',
2090         'ii': 'iii',
2091         'ik': 'ipk',
2092         'io': 'ido',
2093         'is': 'isl',
2094         'it': 'ita',
2095         'iu': 'iku',
2096         'ja': 'jpn',
2097         'jv': 'jav',
2098         'ka': 'kat',
2099         'kg': 'kon',
2100         'ki': 'kik',
2101         'kj': 'kua',
2102         'kk': 'kaz',
2103         'kl': 'kal',
2104         'km': 'khm',
2105         'kn': 'kan',
2106         'ko': 'kor',
2107         'kr': 'kau',
2108         'ks': 'kas',
2109         'ku': 'kur',
2110         'kv': 'kom',
2111         'kw': 'cor',
2112         'ky': 'kir',
2113         'la': 'lat',
2114         'lb': 'ltz',
2115         'lg': 'lug',
2116         'li': 'lim',
2117         'ln': 'lin',
2118         'lo': 'lao',
2119         'lt': 'lit',
2120         'lu': 'lub',
2121         'lv': 'lav',
2122         'mg': 'mlg',
2123         'mh': 'mah',
2124         'mi': 'mri',
2125         'mk': 'mkd',
2126         'ml': 'mal',
2127         'mn': 'mon',
2128         'mr': 'mar',
2129         'ms': 'msa',
2130         'mt': 'mlt',
2131         'my': 'mya',
2132         'na': 'nau',
2133         'nb': 'nob',
2134         'nd': 'nde',
2135         'ne': 'nep',
2136         'ng': 'ndo',
2137         'nl': 'nld',
2138         'nn': 'nno',
2139         'no': 'nor',
2140         'nr': 'nbl',
2141         'nv': 'nav',
2142         'ny': 'nya',
2143         'oc': 'oci',
2144         'oj': 'oji',
2145         'om': 'orm',
2146         'or': 'ori',
2147         'os': 'oss',
2148         'pa': 'pan',
2149         'pi': 'pli',
2150         'pl': 'pol',
2151         'ps': 'pus',
2152         'pt': 'por',
2153         'qu': 'que',
2154         'rm': 'roh',
2155         'rn': 'run',
2156         'ro': 'ron',
2157         'ru': 'rus',
2158         'rw': 'kin',
2159         'sa': 'san',
2160         'sc': 'srd',
2161         'sd': 'snd',
2162         'se': 'sme',
2163         'sg': 'sag',
2164         'si': 'sin',
2165         'sk': 'slk',
2166         'sl': 'slv',
2167         'sm': 'smo',
2168         'sn': 'sna',
2169         'so': 'som',
2170         'sq': 'sqi',
2171         'sr': 'srp',
2172         'ss': 'ssw',
2173         'st': 'sot',
2174         'su': 'sun',
2175         'sv': 'swe',
2176         'sw': 'swa',
2177         'ta': 'tam',
2178         'te': 'tel',
2179         'tg': 'tgk',
2180         'th': 'tha',
2181         'ti': 'tir',
2182         'tk': 'tuk',
2183         'tl': 'tgl',
2184         'tn': 'tsn',
2185         'to': 'ton',
2186         'tr': 'tur',
2187         'ts': 'tso',
2188         'tt': 'tat',
2189         'tw': 'twi',
2190         'ty': 'tah',
2191         'ug': 'uig',
2192         'uk': 'ukr',
2193         'ur': 'urd',
2194         'uz': 'uzb',
2195         've': 'ven',
2196         'vi': 'vie',
2197         'vo': 'vol',
2198         'wa': 'wln',
2199         'wo': 'wol',
2200         'xh': 'xho',
2201         'yi': 'yid',
2202         'yo': 'yor',
2203         'za': 'zha',
2204         'zh': 'zho',
2205         'zu': 'zul',
2206     }
2207
2208     @classmethod
2209     def short2long(cls, code):
2210         """Convert language code from ISO 639-1 to ISO 639-2/T"""
2211         return cls._lang_map.get(code[:2])
2212
2213     @classmethod
2214     def long2short(cls, code):
2215         """Convert language code from ISO 639-2/T to ISO 639-1"""
2216         for short_name, long_name in cls._lang_map.items():
2217             if long_name == code:
2218                 return short_name
2219
2220
2221 class ISO3166Utils(object):
2222     # From http://data.okfn.org/data/core/country-list
2223     _country_map = {
2224         'AF': 'Afghanistan',
2225         'AX': 'Ã…land Islands',
2226         'AL': 'Albania',
2227         'DZ': 'Algeria',
2228         'AS': 'American Samoa',
2229         'AD': 'Andorra',
2230         'AO': 'Angola',
2231         'AI': 'Anguilla',
2232         'AQ': 'Antarctica',
2233         'AG': 'Antigua and Barbuda',
2234         'AR': 'Argentina',
2235         'AM': 'Armenia',
2236         'AW': 'Aruba',
2237         'AU': 'Australia',
2238         'AT': 'Austria',
2239         'AZ': 'Azerbaijan',
2240         'BS': 'Bahamas',
2241         'BH': 'Bahrain',
2242         'BD': 'Bangladesh',
2243         'BB': 'Barbados',
2244         'BY': 'Belarus',
2245         'BE': 'Belgium',
2246         'BZ': 'Belize',
2247         'BJ': 'Benin',
2248         'BM': 'Bermuda',
2249         'BT': 'Bhutan',
2250         'BO': 'Bolivia, Plurinational State of',
2251         'BQ': 'Bonaire, Sint Eustatius and Saba',
2252         'BA': 'Bosnia and Herzegovina',
2253         'BW': 'Botswana',
2254         'BV': 'Bouvet Island',
2255         'BR': 'Brazil',
2256         'IO': 'British Indian Ocean Territory',
2257         'BN': 'Brunei Darussalam',
2258         'BG': 'Bulgaria',
2259         'BF': 'Burkina Faso',
2260         'BI': 'Burundi',
2261         'KH': 'Cambodia',
2262         'CM': 'Cameroon',
2263         'CA': 'Canada',
2264         'CV': 'Cape Verde',
2265         'KY': 'Cayman Islands',
2266         'CF': 'Central African Republic',
2267         'TD': 'Chad',
2268         'CL': 'Chile',
2269         'CN': 'China',
2270         'CX': 'Christmas Island',
2271         'CC': 'Cocos (Keeling) Islands',
2272         'CO': 'Colombia',
2273         'KM': 'Comoros',
2274         'CG': 'Congo',
2275         'CD': 'Congo, the Democratic Republic of the',
2276         'CK': 'Cook Islands',
2277         'CR': 'Costa Rica',
2278         'CI': 'Côte d\'Ivoire',
2279         'HR': 'Croatia',
2280         'CU': 'Cuba',
2281         'CW': 'Curaçao',
2282         'CY': 'Cyprus',
2283         'CZ': 'Czech Republic',
2284         'DK': 'Denmark',
2285         'DJ': 'Djibouti',
2286         'DM': 'Dominica',
2287         'DO': 'Dominican Republic',
2288         'EC': 'Ecuador',
2289         'EG': 'Egypt',
2290         'SV': 'El Salvador',
2291         'GQ': 'Equatorial Guinea',
2292         'ER': 'Eritrea',
2293         'EE': 'Estonia',
2294         'ET': 'Ethiopia',
2295         'FK': 'Falkland Islands (Malvinas)',
2296         'FO': 'Faroe Islands',
2297         'FJ': 'Fiji',
2298         'FI': 'Finland',
2299         'FR': 'France',
2300         'GF': 'French Guiana',
2301         'PF': 'French Polynesia',
2302         'TF': 'French Southern Territories',
2303         'GA': 'Gabon',
2304         'GM': 'Gambia',
2305         'GE': 'Georgia',
2306         'DE': 'Germany',
2307         'GH': 'Ghana',
2308         'GI': 'Gibraltar',
2309         'GR': 'Greece',
2310         'GL': 'Greenland',
2311         'GD': 'Grenada',
2312         'GP': 'Guadeloupe',
2313         'GU': 'Guam',
2314         'GT': 'Guatemala',
2315         'GG': 'Guernsey',
2316         'GN': 'Guinea',
2317         'GW': 'Guinea-Bissau',
2318         'GY': 'Guyana',
2319         'HT': 'Haiti',
2320         'HM': 'Heard Island and McDonald Islands',
2321         'VA': 'Holy See (Vatican City State)',
2322         'HN': 'Honduras',
2323         'HK': 'Hong Kong',
2324         'HU': 'Hungary',
2325         'IS': 'Iceland',
2326         'IN': 'India',
2327         'ID': 'Indonesia',
2328         'IR': 'Iran, Islamic Republic of',
2329         'IQ': 'Iraq',
2330         'IE': 'Ireland',
2331         'IM': 'Isle of Man',
2332         'IL': 'Israel',
2333         'IT': 'Italy',
2334         'JM': 'Jamaica',
2335         'JP': 'Japan',
2336         'JE': 'Jersey',
2337         'JO': 'Jordan',
2338         'KZ': 'Kazakhstan',
2339         'KE': 'Kenya',
2340         'KI': 'Kiribati',
2341         'KP': 'Korea, Democratic People\'s Republic of',
2342         'KR': 'Korea, Republic of',
2343         'KW': 'Kuwait',
2344         'KG': 'Kyrgyzstan',
2345         'LA': 'Lao People\'s Democratic Republic',
2346         'LV': 'Latvia',
2347         'LB': 'Lebanon',
2348         'LS': 'Lesotho',
2349         'LR': 'Liberia',
2350         'LY': 'Libya',
2351         'LI': 'Liechtenstein',
2352         'LT': 'Lithuania',
2353         'LU': 'Luxembourg',
2354         'MO': 'Macao',
2355         'MK': 'Macedonia, the Former Yugoslav Republic of',
2356         'MG': 'Madagascar',
2357         'MW': 'Malawi',
2358         'MY': 'Malaysia',
2359         'MV': 'Maldives',
2360         'ML': 'Mali',
2361         'MT': 'Malta',
2362         'MH': 'Marshall Islands',
2363         'MQ': 'Martinique',
2364         'MR': 'Mauritania',
2365         'MU': 'Mauritius',
2366         'YT': 'Mayotte',
2367         'MX': 'Mexico',
2368         'FM': 'Micronesia, Federated States of',
2369         'MD': 'Moldova, Republic of',
2370         'MC': 'Monaco',
2371         'MN': 'Mongolia',
2372         'ME': 'Montenegro',
2373         'MS': 'Montserrat',
2374         'MA': 'Morocco',
2375         'MZ': 'Mozambique',
2376         'MM': 'Myanmar',
2377         'NA': 'Namibia',
2378         'NR': 'Nauru',
2379         'NP': 'Nepal',
2380         'NL': 'Netherlands',
2381         'NC': 'New Caledonia',
2382         'NZ': 'New Zealand',
2383         'NI': 'Nicaragua',
2384         'NE': 'Niger',
2385         'NG': 'Nigeria',
2386         'NU': 'Niue',
2387         'NF': 'Norfolk Island',
2388         'MP': 'Northern Mariana Islands',
2389         'NO': 'Norway',
2390         'OM': 'Oman',
2391         'PK': 'Pakistan',
2392         'PW': 'Palau',
2393         'PS': 'Palestine, State of',
2394         'PA': 'Panama',
2395         'PG': 'Papua New Guinea',
2396         'PY': 'Paraguay',
2397         'PE': 'Peru',
2398         'PH': 'Philippines',
2399         'PN': 'Pitcairn',
2400         'PL': 'Poland',
2401         'PT': 'Portugal',
2402         'PR': 'Puerto Rico',
2403         'QA': 'Qatar',
2404         'RE': 'Réunion',
2405         'RO': 'Romania',
2406         'RU': 'Russian Federation',
2407         'RW': 'Rwanda',
2408         'BL': 'Saint Barthélemy',
2409         'SH': 'Saint Helena, Ascension and Tristan da Cunha',
2410         'KN': 'Saint Kitts and Nevis',
2411         'LC': 'Saint Lucia',
2412         'MF': 'Saint Martin (French part)',
2413         'PM': 'Saint Pierre and Miquelon',
2414         'VC': 'Saint Vincent and the Grenadines',
2415         'WS': 'Samoa',
2416         'SM': 'San Marino',
2417         'ST': 'Sao Tome and Principe',
2418         'SA': 'Saudi Arabia',
2419         'SN': 'Senegal',
2420         'RS': 'Serbia',
2421         'SC': 'Seychelles',
2422         'SL': 'Sierra Leone',
2423         'SG': 'Singapore',
2424         'SX': 'Sint Maarten (Dutch part)',
2425         'SK': 'Slovakia',
2426         'SI': 'Slovenia',
2427         'SB': 'Solomon Islands',
2428         'SO': 'Somalia',
2429         'ZA': 'South Africa',
2430         'GS': 'South Georgia and the South Sandwich Islands',
2431         'SS': 'South Sudan',
2432         'ES': 'Spain',
2433         'LK': 'Sri Lanka',
2434         'SD': 'Sudan',
2435         'SR': 'Suriname',
2436         'SJ': 'Svalbard and Jan Mayen',
2437         'SZ': 'Swaziland',
2438         'SE': 'Sweden',
2439         'CH': 'Switzerland',
2440         'SY': 'Syrian Arab Republic',
2441         'TW': 'Taiwan, Province of China',
2442         'TJ': 'Tajikistan',
2443         'TZ': 'Tanzania, United Republic of',
2444         'TH': 'Thailand',
2445         'TL': 'Timor-Leste',
2446         'TG': 'Togo',
2447         'TK': 'Tokelau',
2448         'TO': 'Tonga',
2449         'TT': 'Trinidad and Tobago',
2450         'TN': 'Tunisia',
2451         'TR': 'Turkey',
2452         'TM': 'Turkmenistan',
2453         'TC': 'Turks and Caicos Islands',
2454         'TV': 'Tuvalu',
2455         'UG': 'Uganda',
2456         'UA': 'Ukraine',
2457         'AE': 'United Arab Emirates',
2458         'GB': 'United Kingdom',
2459         'US': 'United States',
2460         'UM': 'United States Minor Outlying Islands',
2461         'UY': 'Uruguay',
2462         'UZ': 'Uzbekistan',
2463         'VU': 'Vanuatu',
2464         'VE': 'Venezuela, Bolivarian Republic of',
2465         'VN': 'Viet Nam',
2466         'VG': 'Virgin Islands, British',
2467         'VI': 'Virgin Islands, U.S.',
2468         'WF': 'Wallis and Futuna',
2469         'EH': 'Western Sahara',
2470         'YE': 'Yemen',
2471         'ZM': 'Zambia',
2472         'ZW': 'Zimbabwe',
2473     }
2474
2475     @classmethod
2476     def short2full(cls, code):
2477         """Convert an ISO 3166-2 country code to the corresponding full name"""
2478         return cls._country_map.get(code.upper())
2479
2480
2481 class PerRequestProxyHandler(compat_urllib_request.ProxyHandler):
2482     def __init__(self, proxies=None):
2483         # Set default handlers
2484         for type in ('http', 'https'):
2485             setattr(self, '%s_open' % type,
2486                     lambda r, proxy='__noproxy__', type=type, meth=self.proxy_open:
2487                         meth(r, proxy, type))
2488         return compat_urllib_request.ProxyHandler.__init__(self, proxies)
2489
2490     def proxy_open(self, req, proxy, type):
2491         req_proxy = req.headers.get('Ytdl-request-proxy')
2492         if req_proxy is not None:
2493             proxy = req_proxy
2494             del req.headers['Ytdl-request-proxy']
2495
2496         if proxy == '__noproxy__':
2497             return None  # No Proxy
2498         return compat_urllib_request.ProxyHandler.proxy_open(
2499             self, req, proxy, type)