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