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