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