[ntvde] Add new extractor (Fixes #4850)
[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 os
21 import pipes
22 import platform
23 import re
24 import ssl
25 import socket
26 import struct
27 import subprocess
28 import sys
29 import tempfile
30 import traceback
31 import xml.etree.ElementTree
32 import zlib
33
34 from .compat import (
35     compat_basestring,
36     compat_chr,
37     compat_getenv,
38     compat_html_entities,
39     compat_http_client,
40     compat_parse_qs,
41     compat_socket_create_connection,
42     compat_str,
43     compat_urllib_error,
44     compat_urllib_parse,
45     compat_urllib_parse_urlparse,
46     compat_urllib_request,
47     compat_urlparse,
48     shlex_quote,
49 )
50
51
52 # This is not clearly defined otherwise
53 compiled_regex_type = type(re.compile(''))
54
55 std_headers = {
56     'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 (Chrome)',
57     'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
58     'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
59     'Accept-Encoding': 'gzip, deflate',
60     'Accept-Language': 'en-us,en;q=0.5',
61 }
62
63
64 def preferredencoding():
65     """Get preferred encoding.
66
67     Returns the best encoding scheme for the system, based on
68     locale.getpreferredencoding() and some further tweaks.
69     """
70     try:
71         pref = locale.getpreferredencoding()
72         'TEST'.encode(pref)
73     except:
74         pref = 'UTF-8'
75
76     return pref
77
78
79 def write_json_file(obj, fn):
80     """ Encode obj as JSON and write it to fn, atomically if possible """
81
82     fn = encodeFilename(fn)
83     if sys.version_info < (3, 0) and sys.platform != 'win32':
84         encoding = get_filesystem_encoding()
85         # os.path.basename returns a bytes object, but NamedTemporaryFile
86         # will fail if the filename contains non ascii characters unless we
87         # use a unicode object
88         path_basename = lambda f: os.path.basename(fn).decode(encoding)
89         # the same for os.path.dirname
90         path_dirname = lambda f: os.path.dirname(fn).decode(encoding)
91     else:
92         path_basename = os.path.basename
93         path_dirname = os.path.dirname
94
95     args = {
96         'suffix': '.tmp',
97         'prefix': path_basename(fn) + '.',
98         'dir': path_dirname(fn),
99         'delete': False,
100     }
101
102     # In Python 2.x, json.dump expects a bytestream.
103     # In Python 3.x, it writes to a character stream
104     if sys.version_info < (3, 0):
105         args['mode'] = 'wb'
106     else:
107         args.update({
108             'mode': 'w',
109             'encoding': 'utf-8',
110         })
111
112     tf = tempfile.NamedTemporaryFile(**args)
113
114     try:
115         with tf:
116             json.dump(obj, tf)
117         if sys.platform == 'win32':
118             # Need to remove existing file on Windows, else os.rename raises
119             # WindowsError or FileExistsError.
120             try:
121                 os.unlink(fn)
122             except OSError:
123                 pass
124         os.rename(tf.name, fn)
125     except:
126         try:
127             os.remove(tf.name)
128         except OSError:
129             pass
130         raise
131
132
133 if sys.version_info >= (2, 7):
134     def find_xpath_attr(node, xpath, key, val):
135         """ Find the xpath xpath[@key=val] """
136         assert re.match(r'^[a-zA-Z-]+$', key)
137         assert re.match(r'^[a-zA-Z0-9@\s:._-]*$', val)
138         expr = xpath + "[@%s='%s']" % (key, val)
139         return node.find(expr)
140 else:
141     def find_xpath_attr(node, xpath, key, val):
142         # Here comes the crazy part: In 2.6, if the xpath is a unicode,
143         # .//node does not match if a node is a direct child of . !
144         if isinstance(xpath, compat_str):
145             xpath = xpath.encode('ascii')
146
147         for f in node.findall(xpath):
148             if f.attrib.get(key) == val:
149                 return f
150         return None
151
152 # On python2.6 the xml.etree.ElementTree.Element methods don't support
153 # the namespace parameter
154
155
156 def xpath_with_ns(path, ns_map):
157     components = [c.split(':') for c in path.split('/')]
158     replaced = []
159     for c in components:
160         if len(c) == 1:
161             replaced.append(c[0])
162         else:
163             ns, tag = c
164             replaced.append('{%s}%s' % (ns_map[ns], tag))
165     return '/'.join(replaced)
166
167
168 def xpath_text(node, xpath, name=None, fatal=False):
169     if sys.version_info < (2, 7):  # Crazy 2.6
170         xpath = xpath.encode('ascii')
171
172     n = node.find(xpath)
173     if n is None or n.text is None:
174         if fatal:
175             name = xpath if name is None else name
176             raise ExtractorError('Could not find XML element %s' % name)
177         else:
178             return None
179     return n.text
180
181
182 def get_element_by_id(id, html):
183     """Return the content of the tag with the specified ID in the passed HTML document"""
184     return get_element_by_attribute("id", id, html)
185
186
187 def get_element_by_attribute(attribute, value, html):
188     """Return the content of the tag with the specified attribute in the passed HTML document"""
189
190     m = re.search(r'''(?xs)
191         <([a-zA-Z0-9:._-]+)
192          (?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]+|="[^"]+"|='[^']+'))*?
193          \s+%s=['"]?%s['"]?
194          (?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]+|="[^"]+"|='[^']+'))*?
195         \s*>
196         (?P<content>.*?)
197         </\1>
198     ''' % (re.escape(attribute), re.escape(value)), html)
199
200     if not m:
201         return None
202     res = m.group('content')
203
204     if res.startswith('"') or res.startswith("'"):
205         res = res[1:-1]
206
207     return unescapeHTML(res)
208
209
210 def clean_html(html):
211     """Clean an HTML snippet into a readable string"""
212
213     if html is None:  # Convenience for sanitizing descriptions etc.
214         return html
215
216     # Newline vs <br />
217     html = html.replace('\n', ' ')
218     html = re.sub(r'\s*<\s*br\s*/?\s*>\s*', '\n', html)
219     html = re.sub(r'<\s*/\s*p\s*>\s*<\s*p[^>]*>', '\n', html)
220     # Strip html tags
221     html = re.sub('<.*?>', '', html)
222     # Replace html entities
223     html = unescapeHTML(html)
224     return html.strip()
225
226
227 def sanitize_open(filename, open_mode):
228     """Try to open the given filename, and slightly tweak it if this fails.
229
230     Attempts to open the given filename. If this fails, it tries to change
231     the filename slightly, step by step, until it's either able to open it
232     or it fails and raises a final exception, like the standard open()
233     function.
234
235     It returns the tuple (stream, definitive_file_name).
236     """
237     try:
238         if filename == '-':
239             if sys.platform == 'win32':
240                 import msvcrt
241                 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
242             return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename)
243         stream = open(encodeFilename(filename), open_mode)
244         return (stream, filename)
245     except (IOError, OSError) as err:
246         if err.errno in (errno.EACCES,):
247             raise
248
249         # In case of error, try to remove win32 forbidden chars
250         alt_filename = os.path.join(
251             re.sub('[/<>:"\\|\\\\?\\*]', '#', path_part)
252             for path_part in os.path.split(filename)
253         )
254         if alt_filename == filename:
255             raise
256         else:
257             # An exception here should be caught in the caller
258             stream = open(encodeFilename(filename), open_mode)
259             return (stream, alt_filename)
260
261
262 def timeconvert(timestr):
263     """Convert RFC 2822 defined time string into system timestamp"""
264     timestamp = None
265     timetuple = email.utils.parsedate_tz(timestr)
266     if timetuple is not None:
267         timestamp = email.utils.mktime_tz(timetuple)
268     return timestamp
269
270
271 def sanitize_filename(s, restricted=False, is_id=False):
272     """Sanitizes a string so it could be used as part of a filename.
273     If restricted is set, use a stricter subset of allowed characters.
274     Set is_id if this is not an arbitrary string, but an ID that should be kept if possible
275     """
276     def replace_insane(char):
277         if char == '?' or ord(char) < 32 or ord(char) == 127:
278             return ''
279         elif char == '"':
280             return '' if restricted else '\''
281         elif char == ':':
282             return '_-' if restricted else ' -'
283         elif char in '\\/|*<>':
284             return '_'
285         if restricted and (char in '!&\'()[]{}$;`^,#' or char.isspace()):
286             return '_'
287         if restricted and ord(char) > 127:
288             return '_'
289         return char
290
291     # Handle timestamps
292     s = re.sub(r'[0-9]+(?::[0-9]+)+', lambda m: m.group(0).replace(':', '_'), s)
293     result = ''.join(map(replace_insane, s))
294     if not is_id:
295         while '__' in result:
296             result = result.replace('__', '_')
297         result = result.strip('_')
298         # Common case of "Foreign band name - English song title"
299         if restricted and result.startswith('-_'):
300             result = result[2:]
301         if not result:
302             result = '_'
303     return result
304
305
306 def orderedSet(iterable):
307     """ Remove all duplicates from the input iterable """
308     res = []
309     for el in iterable:
310         if el not in res:
311             res.append(el)
312     return res
313
314
315 def _htmlentity_transform(entity):
316     """Transforms an HTML entity to a character."""
317     # Known non-numeric HTML entity
318     if entity in compat_html_entities.name2codepoint:
319         return compat_chr(compat_html_entities.name2codepoint[entity])
320
321     mobj = re.match(r'#(x?[0-9]+)', entity)
322     if mobj is not None:
323         numstr = mobj.group(1)
324         if numstr.startswith('x'):
325             base = 16
326             numstr = '0%s' % numstr
327         else:
328             base = 10
329         return compat_chr(int(numstr, base))
330
331     # Unknown entity in name, return its literal representation
332     return ('&%s;' % entity)
333
334
335 def unescapeHTML(s):
336     if s is None:
337         return None
338     assert type(s) == compat_str
339
340     return re.sub(
341         r'&([^;]+);', lambda m: _htmlentity_transform(m.group(1)), s)
342
343
344 def encodeFilename(s, for_subprocess=False):
345     """
346     @param s The name of the file
347     """
348
349     assert type(s) == compat_str
350
351     # Python 3 has a Unicode API
352     if sys.version_info >= (3, 0):
353         return s
354
355     if sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
356         # Pass '' directly to use Unicode APIs on Windows 2000 and up
357         # (Detecting Windows NT 4 is tricky because 'major >= 4' would
358         # match Windows 9x series as well. Besides, NT 4 is obsolete.)
359         if not for_subprocess:
360             return s
361         else:
362             # For subprocess calls, encode with locale encoding
363             # Refer to http://stackoverflow.com/a/9951851/35070
364             encoding = preferredencoding()
365     else:
366         encoding = sys.getfilesystemencoding()
367     if encoding is None:
368         encoding = 'utf-8'
369     return s.encode(encoding, 'ignore')
370
371
372 def encodeArgument(s):
373     if not isinstance(s, compat_str):
374         # Legacy code that uses byte strings
375         # Uncomment the following line after fixing all post processors
376         # assert False, 'Internal error: %r should be of type %r, is %r' % (s, compat_str, type(s))
377         s = s.decode('ascii')
378     return encodeFilename(s, True)
379
380
381 def decodeOption(optval):
382     if optval is None:
383         return optval
384     if isinstance(optval, bytes):
385         optval = optval.decode(preferredencoding())
386
387     assert isinstance(optval, compat_str)
388     return optval
389
390
391 def formatSeconds(secs):
392     if secs > 3600:
393         return '%d:%02d:%02d' % (secs // 3600, (secs % 3600) // 60, secs % 60)
394     elif secs > 60:
395         return '%d:%02d' % (secs // 60, secs % 60)
396     else:
397         return '%d' % secs
398
399
400 def make_HTTPS_handler(params, **kwargs):
401     opts_no_check_certificate = params.get('nocheckcertificate', False)
402     if hasattr(ssl, 'create_default_context'):  # Python >= 3.4 or 2.7.9
403         context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
404         if opts_no_check_certificate:
405             context.check_hostname = False
406             context.verify_mode = ssl.CERT_NONE
407         try:
408             return YoutubeDLHTTPSHandler(params, context=context, **kwargs)
409         except TypeError:
410             # Python 2.7.8
411             # (create_default_context present but HTTPSHandler has no context=)
412             pass
413
414     if sys.version_info < (3, 2):
415         return YoutubeDLHTTPSHandler(params, **kwargs)
416     else:  # Python < 3.4
417         context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
418         context.verify_mode = (ssl.CERT_NONE
419                                if opts_no_check_certificate
420                                else ssl.CERT_REQUIRED)
421         context.set_default_verify_paths()
422         return YoutubeDLHTTPSHandler(params, context=context, **kwargs)
423
424
425 class ExtractorError(Exception):
426     """Error during info extraction."""
427
428     def __init__(self, msg, tb=None, expected=False, cause=None, video_id=None):
429         """ tb, if given, is the original traceback (so that it can be printed out).
430         If expected is set, this is a normal error message and most likely not a bug in youtube-dl.
431         """
432
433         if sys.exc_info()[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError):
434             expected = True
435         if video_id is not None:
436             msg = video_id + ': ' + msg
437         if cause:
438             msg += ' (caused by %r)' % cause
439         if not expected:
440             if ytdl_is_updateable():
441                 update_cmd = 'type  youtube-dl -U  to update'
442             else:
443                 update_cmd = 'see  https://yt-dl.org/update  on how to update'
444             msg += '; please report this issue on https://yt-dl.org/bug .'
445             msg += ' Make sure you are using the latest version; %s.' % update_cmd
446             msg += ' Be sure to call youtube-dl with the --verbose flag and include its complete output.'
447         super(ExtractorError, self).__init__(msg)
448
449         self.traceback = tb
450         self.exc_info = sys.exc_info()  # preserve original exception
451         self.cause = cause
452         self.video_id = video_id
453
454     def format_traceback(self):
455         if self.traceback is None:
456             return None
457         return ''.join(traceback.format_tb(self.traceback))
458
459
460 class UnsupportedError(ExtractorError):
461     def __init__(self, url):
462         super(UnsupportedError, self).__init__(
463             'Unsupported URL: %s' % url, expected=True)
464         self.url = url
465
466
467 class RegexNotFoundError(ExtractorError):
468     """Error when a regex didn't match"""
469     pass
470
471
472 class DownloadError(Exception):
473     """Download Error exception.
474
475     This exception may be thrown by FileDownloader objects if they are not
476     configured to continue on errors. They will contain the appropriate
477     error message.
478     """
479
480     def __init__(self, msg, exc_info=None):
481         """ exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """
482         super(DownloadError, self).__init__(msg)
483         self.exc_info = exc_info
484
485
486 class SameFileError(Exception):
487     """Same File exception.
488
489     This exception will be thrown by FileDownloader objects if they detect
490     multiple files would have to be downloaded to the same file on disk.
491     """
492     pass
493
494
495 class PostProcessingError(Exception):
496     """Post Processing exception.
497
498     This exception may be raised by PostProcessor's .run() method to
499     indicate an error in the postprocessing task.
500     """
501
502     def __init__(self, msg):
503         self.msg = msg
504
505
506 class MaxDownloadsReached(Exception):
507     """ --max-downloads limit has been reached. """
508     pass
509
510
511 class UnavailableVideoError(Exception):
512     """Unavailable Format exception.
513
514     This exception will be thrown when a video is requested
515     in a format that is not available for that video.
516     """
517     pass
518
519
520 class ContentTooShortError(Exception):
521     """Content Too Short exception.
522
523     This exception may be raised by FileDownloader objects when a file they
524     download is too small for what the server announced first, indicating
525     the connection was probably interrupted.
526     """
527     # Both in bytes
528     downloaded = None
529     expected = None
530
531     def __init__(self, downloaded, expected):
532         self.downloaded = downloaded
533         self.expected = expected
534
535
536 def _create_http_connection(ydl_handler, http_class, is_https, *args, **kwargs):
537     hc = http_class(*args, **kwargs)
538     source_address = ydl_handler._params.get('source_address')
539     if source_address is not None:
540         sa = (source_address, 0)
541         if hasattr(hc, 'source_address'):  # Python 2.7+
542             hc.source_address = sa
543         else:  # Python 2.6
544             def _hc_connect(self, *args, **kwargs):
545                 sock = compat_socket_create_connection(
546                     (self.host, self.port), self.timeout, sa)
547                 if is_https:
548                     self.sock = ssl.wrap_socket(
549                         sock, self.key_file, self.cert_file,
550                         ssl_version=ssl.PROTOCOL_TLSv1)
551                 else:
552                     self.sock = sock
553             hc.connect = functools.partial(_hc_connect, hc)
554
555     return hc
556
557
558 class YoutubeDLHandler(compat_urllib_request.HTTPHandler):
559     """Handler for HTTP requests and responses.
560
561     This class, when installed with an OpenerDirector, automatically adds
562     the standard headers to every HTTP request and handles gzipped and
563     deflated responses from web servers. If compression is to be avoided in
564     a particular request, the original request in the program code only has
565     to include the HTTP header "Youtubedl-No-Compression", which will be
566     removed before making the real request.
567
568     Part of this code was copied from:
569
570     http://techknack.net/python-urllib2-handlers/
571
572     Andrew Rowls, the author of that code, agreed to release it to the
573     public domain.
574     """
575
576     def __init__(self, params, *args, **kwargs):
577         compat_urllib_request.HTTPHandler.__init__(self, *args, **kwargs)
578         self._params = params
579
580     def http_open(self, req):
581         return self.do_open(functools.partial(
582             _create_http_connection, self, compat_http_client.HTTPConnection, False),
583             req)
584
585     @staticmethod
586     def deflate(data):
587         try:
588             return zlib.decompress(data, -zlib.MAX_WBITS)
589         except zlib.error:
590             return zlib.decompress(data)
591
592     @staticmethod
593     def addinfourl_wrapper(stream, headers, url, code):
594         if hasattr(compat_urllib_request.addinfourl, 'getcode'):
595             return compat_urllib_request.addinfourl(stream, headers, url, code)
596         ret = compat_urllib_request.addinfourl(stream, headers, url)
597         ret.code = code
598         return ret
599
600     def http_request(self, req):
601         for h, v in std_headers.items():
602             # Capitalize is needed because of Python bug 2275: http://bugs.python.org/issue2275
603             # The dict keys are capitalized because of this bug by urllib
604             if h.capitalize() not in req.headers:
605                 req.add_header(h, v)
606         if 'Youtubedl-no-compression' in req.headers:
607             if 'Accept-encoding' in req.headers:
608                 del req.headers['Accept-encoding']
609             del req.headers['Youtubedl-no-compression']
610
611         if sys.version_info < (2, 7) and '#' in req.get_full_url():
612             # Python 2.6 is brain-dead when it comes to fragments
613             req._Request__original = req._Request__original.partition('#')[0]
614             req._Request__r_type = req._Request__r_type.partition('#')[0]
615
616         return req
617
618     def http_response(self, req, resp):
619         old_resp = resp
620         # gzip
621         if resp.headers.get('Content-encoding', '') == 'gzip':
622             content = resp.read()
623             gz = gzip.GzipFile(fileobj=io.BytesIO(content), mode='rb')
624             try:
625                 uncompressed = io.BytesIO(gz.read())
626             except IOError as original_ioerror:
627                 # There may be junk add the end of the file
628                 # See http://stackoverflow.com/q/4928560/35070 for details
629                 for i in range(1, 1024):
630                     try:
631                         gz = gzip.GzipFile(fileobj=io.BytesIO(content[:-i]), mode='rb')
632                         uncompressed = io.BytesIO(gz.read())
633                     except IOError:
634                         continue
635                     break
636                 else:
637                     raise original_ioerror
638             resp = self.addinfourl_wrapper(uncompressed, old_resp.headers, old_resp.url, old_resp.code)
639             resp.msg = old_resp.msg
640         # deflate
641         if resp.headers.get('Content-encoding', '') == 'deflate':
642             gz = io.BytesIO(self.deflate(resp.read()))
643             resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code)
644             resp.msg = old_resp.msg
645         return resp
646
647     https_request = http_request
648     https_response = http_response
649
650
651 class YoutubeDLHTTPSHandler(compat_urllib_request.HTTPSHandler):
652     def __init__(self, params, https_conn_class=None, *args, **kwargs):
653         compat_urllib_request.HTTPSHandler.__init__(self, *args, **kwargs)
654         self._https_conn_class = https_conn_class or compat_http_client.HTTPSConnection
655         self._params = params
656
657     def https_open(self, req):
658         kwargs = {}
659         if hasattr(self, '_context'):  # python > 2.6
660             kwargs['context'] = self._context
661         if hasattr(self, '_check_hostname'):  # python 3.x
662             kwargs['check_hostname'] = self._check_hostname
663         return self.do_open(functools.partial(
664             _create_http_connection, self, self._https_conn_class, True),
665             req, **kwargs)
666
667
668 def parse_iso8601(date_str, delimiter='T'):
669     """ Return a UNIX timestamp from the given date """
670
671     if date_str is None:
672         return None
673
674     m = re.search(
675         r'(\.[0-9]+)?(?:Z$| ?(?P<sign>\+|-)(?P<hours>[0-9]{2}):?(?P<minutes>[0-9]{2})$)',
676         date_str)
677     if not m:
678         timezone = datetime.timedelta()
679     else:
680         date_str = date_str[:-len(m.group(0))]
681         if not m.group('sign'):
682             timezone = datetime.timedelta()
683         else:
684             sign = 1 if m.group('sign') == '+' else -1
685             timezone = datetime.timedelta(
686                 hours=sign * int(m.group('hours')),
687                 minutes=sign * int(m.group('minutes')))
688     date_format = '%Y-%m-%d{0}%H:%M:%S'.format(delimiter)
689     dt = datetime.datetime.strptime(date_str, date_format) - timezone
690     return calendar.timegm(dt.timetuple())
691
692
693 def unified_strdate(date_str, day_first=True):
694     """Return a string with the date in the format YYYYMMDD"""
695
696     if date_str is None:
697         return None
698     upload_date = None
699     # Replace commas
700     date_str = date_str.replace(',', ' ')
701     # %z (UTC offset) is only supported in python>=3.2
702     date_str = re.sub(r' ?(\+|-)[0-9]{2}:?[0-9]{2}$', '', date_str)
703     # Remove AM/PM + timezone
704     date_str = re.sub(r'(?i)\s*(?:AM|PM)\s+[A-Z]+', '', date_str)
705
706     format_expressions = [
707         '%d %B %Y',
708         '%d %b %Y',
709         '%B %d %Y',
710         '%b %d %Y',
711         '%b %dst %Y %I:%M%p',
712         '%b %dnd %Y %I:%M%p',
713         '%b %dth %Y %I:%M%p',
714         '%Y %m %d',
715         '%Y-%m-%d',
716         '%Y/%m/%d',
717         '%Y/%m/%d %H:%M:%S',
718         '%Y-%m-%d %H:%M:%S',
719         '%Y-%m-%d %H:%M:%S.%f',
720         '%d.%m.%Y %H:%M',
721         '%d.%m.%Y %H.%M',
722         '%Y-%m-%dT%H:%M:%SZ',
723         '%Y-%m-%dT%H:%M:%S.%fZ',
724         '%Y-%m-%dT%H:%M:%S.%f0Z',
725         '%Y-%m-%dT%H:%M:%S',
726         '%Y-%m-%dT%H:%M:%S.%f',
727         '%Y-%m-%dT%H:%M',
728     ]
729     if day_first:
730         format_expressions.extend([
731             '%d.%m.%Y',
732             '%d/%m/%Y',
733             '%d/%m/%y',
734             '%d/%m/%Y %H:%M:%S',
735         ])
736     else:
737         format_expressions.extend([
738             '%m.%d.%Y',
739             '%m/%d/%Y',
740             '%m/%d/%y',
741             '%m/%d/%Y %H:%M:%S',
742         ])
743     for expression in format_expressions:
744         try:
745             upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
746         except ValueError:
747             pass
748     if upload_date is None:
749         timetuple = email.utils.parsedate_tz(date_str)
750         if timetuple:
751             upload_date = datetime.datetime(*timetuple[:6]).strftime('%Y%m%d')
752     return upload_date
753
754
755 def determine_ext(url, default_ext='unknown_video'):
756     if url is None:
757         return default_ext
758     guess = url.partition('?')[0].rpartition('.')[2]
759     if re.match(r'^[A-Za-z0-9]+$', guess):
760         return guess
761     else:
762         return default_ext
763
764
765 def subtitles_filename(filename, sub_lang, sub_format):
766     return filename.rsplit('.', 1)[0] + '.' + sub_lang + '.' + sub_format
767
768
769 def date_from_str(date_str):
770     """
771     Return a datetime object from a string in the format YYYYMMDD or
772     (now|today)[+-][0-9](day|week|month|year)(s)?"""
773     today = datetime.date.today()
774     if date_str in ('now', 'today'):
775         return today
776     if date_str == 'yesterday':
777         return today - datetime.timedelta(days=1)
778     match = re.match('(now|today)(?P<sign>[+-])(?P<time>\d+)(?P<unit>day|week|month|year)(s)?', date_str)
779     if match is not None:
780         sign = match.group('sign')
781         time = int(match.group('time'))
782         if sign == '-':
783             time = -time
784         unit = match.group('unit')
785         # A bad aproximation?
786         if unit == 'month':
787             unit = 'day'
788             time *= 30
789         elif unit == 'year':
790             unit = 'day'
791             time *= 365
792         unit += 's'
793         delta = datetime.timedelta(**{unit: time})
794         return today + delta
795     return datetime.datetime.strptime(date_str, "%Y%m%d").date()
796
797
798 def hyphenate_date(date_str):
799     """
800     Convert a date in 'YYYYMMDD' format to 'YYYY-MM-DD' format"""
801     match = re.match(r'^(\d\d\d\d)(\d\d)(\d\d)$', date_str)
802     if match is not None:
803         return '-'.join(match.groups())
804     else:
805         return date_str
806
807
808 class DateRange(object):
809     """Represents a time interval between two dates"""
810
811     def __init__(self, start=None, end=None):
812         """start and end must be strings in the format accepted by date"""
813         if start is not None:
814             self.start = date_from_str(start)
815         else:
816             self.start = datetime.datetime.min.date()
817         if end is not None:
818             self.end = date_from_str(end)
819         else:
820             self.end = datetime.datetime.max.date()
821         if self.start > self.end:
822             raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
823
824     @classmethod
825     def day(cls, day):
826         """Returns a range that only contains the given day"""
827         return cls(day, day)
828
829     def __contains__(self, date):
830         """Check if the date is in the range"""
831         if not isinstance(date, datetime.date):
832             date = date_from_str(date)
833         return self.start <= date <= self.end
834
835     def __str__(self):
836         return '%s - %s' % (self.start.isoformat(), self.end.isoformat())
837
838
839 def platform_name():
840     """ Returns the platform name as a compat_str """
841     res = platform.platform()
842     if isinstance(res, bytes):
843         res = res.decode(preferredencoding())
844
845     assert isinstance(res, compat_str)
846     return res
847
848
849 def _windows_write_string(s, out):
850     """ Returns True if the string was written using special methods,
851     False if it has yet to be written out."""
852     # Adapted from http://stackoverflow.com/a/3259271/35070
853
854     import ctypes
855     import ctypes.wintypes
856
857     WIN_OUTPUT_IDS = {
858         1: -11,
859         2: -12,
860     }
861
862     try:
863         fileno = out.fileno()
864     except AttributeError:
865         # If the output stream doesn't have a fileno, it's virtual
866         return False
867     except io.UnsupportedOperation:
868         # Some strange Windows pseudo files?
869         return False
870     if fileno not in WIN_OUTPUT_IDS:
871         return False
872
873     GetStdHandle = ctypes.WINFUNCTYPE(
874         ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD)(
875         (b"GetStdHandle", ctypes.windll.kernel32))
876     h = GetStdHandle(WIN_OUTPUT_IDS[fileno])
877
878     WriteConsoleW = ctypes.WINFUNCTYPE(
879         ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE, ctypes.wintypes.LPWSTR,
880         ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD),
881         ctypes.wintypes.LPVOID)((b"WriteConsoleW", ctypes.windll.kernel32))
882     written = ctypes.wintypes.DWORD(0)
883
884     GetFileType = ctypes.WINFUNCTYPE(ctypes.wintypes.DWORD, ctypes.wintypes.DWORD)((b"GetFileType", ctypes.windll.kernel32))
885     FILE_TYPE_CHAR = 0x0002
886     FILE_TYPE_REMOTE = 0x8000
887     GetConsoleMode = ctypes.WINFUNCTYPE(
888         ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE,
889         ctypes.POINTER(ctypes.wintypes.DWORD))(
890         (b"GetConsoleMode", ctypes.windll.kernel32))
891     INVALID_HANDLE_VALUE = ctypes.wintypes.DWORD(-1).value
892
893     def not_a_console(handle):
894         if handle == INVALID_HANDLE_VALUE or handle is None:
895             return True
896         return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR
897                 or GetConsoleMode(handle, ctypes.byref(ctypes.wintypes.DWORD())) == 0)
898
899     if not_a_console(h):
900         return False
901
902     def next_nonbmp_pos(s):
903         try:
904             return next(i for i, c in enumerate(s) if ord(c) > 0xffff)
905         except StopIteration:
906             return len(s)
907
908     while s:
909         count = min(next_nonbmp_pos(s), 1024)
910
911         ret = WriteConsoleW(
912             h, s, count if count else 2, ctypes.byref(written), None)
913         if ret == 0:
914             raise OSError('Failed to write string')
915         if not count:  # We just wrote a non-BMP character
916             assert written.value == 2
917             s = s[1:]
918         else:
919             assert written.value > 0
920             s = s[written.value:]
921     return True
922
923
924 def write_string(s, out=None, encoding=None):
925     if out is None:
926         out = sys.stderr
927     assert type(s) == compat_str
928
929     if sys.platform == 'win32' and encoding is None and hasattr(out, 'fileno'):
930         if _windows_write_string(s, out):
931             return
932
933     if ('b' in getattr(out, 'mode', '') or
934             sys.version_info[0] < 3):  # Python 2 lies about mode of sys.stderr
935         byt = s.encode(encoding or preferredencoding(), 'ignore')
936         out.write(byt)
937     elif hasattr(out, 'buffer'):
938         enc = encoding or getattr(out, 'encoding', None) or preferredencoding()
939         byt = s.encode(enc, 'ignore')
940         out.buffer.write(byt)
941     else:
942         out.write(s)
943     out.flush()
944
945
946 def bytes_to_intlist(bs):
947     if not bs:
948         return []
949     if isinstance(bs[0], int):  # Python 3
950         return list(bs)
951     else:
952         return [ord(c) for c in bs]
953
954
955 def intlist_to_bytes(xs):
956     if not xs:
957         return b''
958     return struct_pack('%dB' % len(xs), *xs)
959
960
961 # Cross-platform file locking
962 if sys.platform == 'win32':
963     import ctypes.wintypes
964     import msvcrt
965
966     class OVERLAPPED(ctypes.Structure):
967         _fields_ = [
968             ('Internal', ctypes.wintypes.LPVOID),
969             ('InternalHigh', ctypes.wintypes.LPVOID),
970             ('Offset', ctypes.wintypes.DWORD),
971             ('OffsetHigh', ctypes.wintypes.DWORD),
972             ('hEvent', ctypes.wintypes.HANDLE),
973         ]
974
975     kernel32 = ctypes.windll.kernel32
976     LockFileEx = kernel32.LockFileEx
977     LockFileEx.argtypes = [
978         ctypes.wintypes.HANDLE,     # hFile
979         ctypes.wintypes.DWORD,      # dwFlags
980         ctypes.wintypes.DWORD,      # dwReserved
981         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
982         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
983         ctypes.POINTER(OVERLAPPED)  # Overlapped
984     ]
985     LockFileEx.restype = ctypes.wintypes.BOOL
986     UnlockFileEx = kernel32.UnlockFileEx
987     UnlockFileEx.argtypes = [
988         ctypes.wintypes.HANDLE,     # hFile
989         ctypes.wintypes.DWORD,      # dwReserved
990         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
991         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
992         ctypes.POINTER(OVERLAPPED)  # Overlapped
993     ]
994     UnlockFileEx.restype = ctypes.wintypes.BOOL
995     whole_low = 0xffffffff
996     whole_high = 0x7fffffff
997
998     def _lock_file(f, exclusive):
999         overlapped = OVERLAPPED()
1000         overlapped.Offset = 0
1001         overlapped.OffsetHigh = 0
1002         overlapped.hEvent = 0
1003         f._lock_file_overlapped_p = ctypes.pointer(overlapped)
1004         handle = msvcrt.get_osfhandle(f.fileno())
1005         if not LockFileEx(handle, 0x2 if exclusive else 0x0, 0,
1006                           whole_low, whole_high, f._lock_file_overlapped_p):
1007             raise OSError('Locking file failed: %r' % ctypes.FormatError())
1008
1009     def _unlock_file(f):
1010         assert f._lock_file_overlapped_p
1011         handle = msvcrt.get_osfhandle(f.fileno())
1012         if not UnlockFileEx(handle, 0,
1013                             whole_low, whole_high, f._lock_file_overlapped_p):
1014             raise OSError('Unlocking file failed: %r' % ctypes.FormatError())
1015
1016 else:
1017     import fcntl
1018
1019     def _lock_file(f, exclusive):
1020         fcntl.flock(f, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
1021
1022     def _unlock_file(f):
1023         fcntl.flock(f, fcntl.LOCK_UN)
1024
1025
1026 class locked_file(object):
1027     def __init__(self, filename, mode, encoding=None):
1028         assert mode in ['r', 'a', 'w']
1029         self.f = io.open(filename, mode, encoding=encoding)
1030         self.mode = mode
1031
1032     def __enter__(self):
1033         exclusive = self.mode != 'r'
1034         try:
1035             _lock_file(self.f, exclusive)
1036         except IOError:
1037             self.f.close()
1038             raise
1039         return self
1040
1041     def __exit__(self, etype, value, traceback):
1042         try:
1043             _unlock_file(self.f)
1044         finally:
1045             self.f.close()
1046
1047     def __iter__(self):
1048         return iter(self.f)
1049
1050     def write(self, *args):
1051         return self.f.write(*args)
1052
1053     def read(self, *args):
1054         return self.f.read(*args)
1055
1056
1057 def get_filesystem_encoding():
1058     encoding = sys.getfilesystemencoding()
1059     return encoding if encoding is not None else 'utf-8'
1060
1061
1062 def shell_quote(args):
1063     quoted_args = []
1064     encoding = get_filesystem_encoding()
1065     for a in args:
1066         if isinstance(a, bytes):
1067             # We may get a filename encoded with 'encodeFilename'
1068             a = a.decode(encoding)
1069         quoted_args.append(pipes.quote(a))
1070     return ' '.join(quoted_args)
1071
1072
1073 def takewhile_inclusive(pred, seq):
1074     """ Like itertools.takewhile, but include the latest evaluated element
1075         (the first element so that Not pred(e)) """
1076     for e in seq:
1077         yield e
1078         if not pred(e):
1079             return
1080
1081
1082 def smuggle_url(url, data):
1083     """ Pass additional data in a URL for internal use. """
1084
1085     sdata = compat_urllib_parse.urlencode(
1086         {'__youtubedl_smuggle': json.dumps(data)})
1087     return url + '#' + sdata
1088
1089
1090 def unsmuggle_url(smug_url, default=None):
1091     if '#__youtubedl_smuggle' not in smug_url:
1092         return smug_url, default
1093     url, _, sdata = smug_url.rpartition('#')
1094     jsond = compat_parse_qs(sdata)['__youtubedl_smuggle'][0]
1095     data = json.loads(jsond)
1096     return url, data
1097
1098
1099 def format_bytes(bytes):
1100     if bytes is None:
1101         return 'N/A'
1102     if type(bytes) is str:
1103         bytes = float(bytes)
1104     if bytes == 0.0:
1105         exponent = 0
1106     else:
1107         exponent = int(math.log(bytes, 1024.0))
1108     suffix = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'][exponent]
1109     converted = float(bytes) / float(1024 ** exponent)
1110     return '%.2f%s' % (converted, suffix)
1111
1112
1113 def parse_filesize(s):
1114     if s is None:
1115         return None
1116
1117     # The lower-case forms are of course incorrect and inofficial,
1118     # but we support those too
1119     _UNIT_TABLE = {
1120         'B': 1,
1121         'b': 1,
1122         'KiB': 1024,
1123         'KB': 1000,
1124         'kB': 1024,
1125         'Kb': 1000,
1126         'MiB': 1024 ** 2,
1127         'MB': 1000 ** 2,
1128         'mB': 1024 ** 2,
1129         'Mb': 1000 ** 2,
1130         'GiB': 1024 ** 3,
1131         'GB': 1000 ** 3,
1132         'gB': 1024 ** 3,
1133         'Gb': 1000 ** 3,
1134         'TiB': 1024 ** 4,
1135         'TB': 1000 ** 4,
1136         'tB': 1024 ** 4,
1137         'Tb': 1000 ** 4,
1138         'PiB': 1024 ** 5,
1139         'PB': 1000 ** 5,
1140         'pB': 1024 ** 5,
1141         'Pb': 1000 ** 5,
1142         'EiB': 1024 ** 6,
1143         'EB': 1000 ** 6,
1144         'eB': 1024 ** 6,
1145         'Eb': 1000 ** 6,
1146         'ZiB': 1024 ** 7,
1147         'ZB': 1000 ** 7,
1148         'zB': 1024 ** 7,
1149         'Zb': 1000 ** 7,
1150         'YiB': 1024 ** 8,
1151         'YB': 1000 ** 8,
1152         'yB': 1024 ** 8,
1153         'Yb': 1000 ** 8,
1154     }
1155
1156     units_re = '|'.join(re.escape(u) for u in _UNIT_TABLE)
1157     m = re.match(
1158         r'(?P<num>[0-9]+(?:[,.][0-9]*)?)\s*(?P<unit>%s)' % units_re, s)
1159     if not m:
1160         return None
1161
1162     num_str = m.group('num').replace(',', '.')
1163     mult = _UNIT_TABLE[m.group('unit')]
1164     return int(float(num_str) * mult)
1165
1166
1167 def get_term_width():
1168     columns = compat_getenv('COLUMNS', None)
1169     if columns:
1170         return int(columns)
1171
1172     try:
1173         sp = subprocess.Popen(
1174             ['stty', 'size'],
1175             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1176         out, err = sp.communicate()
1177         return int(out.split()[1])
1178     except:
1179         pass
1180     return None
1181
1182
1183 def month_by_name(name):
1184     """ Return the number of a month by (locale-independently) English name """
1185
1186     ENGLISH_NAMES = [
1187         'January', 'February', 'March', 'April', 'May', 'June',
1188         'July', 'August', 'September', 'October', 'November', 'December']
1189     try:
1190         return ENGLISH_NAMES.index(name) + 1
1191     except ValueError:
1192         return None
1193
1194
1195 def fix_xml_ampersands(xml_str):
1196     """Replace all the '&' by '&amp;' in XML"""
1197     return re.sub(
1198         r'&(?!amp;|lt;|gt;|apos;|quot;|#x[0-9a-fA-F]{,4};|#[0-9]{,4};)',
1199         '&amp;',
1200         xml_str)
1201
1202
1203 def setproctitle(title):
1204     assert isinstance(title, compat_str)
1205     try:
1206         libc = ctypes.cdll.LoadLibrary("libc.so.6")
1207     except OSError:
1208         return
1209     title_bytes = title.encode('utf-8')
1210     buf = ctypes.create_string_buffer(len(title_bytes))
1211     buf.value = title_bytes
1212     try:
1213         libc.prctl(15, buf, 0, 0, 0)
1214     except AttributeError:
1215         return  # Strange libc, just skip this
1216
1217
1218 def remove_start(s, start):
1219     if s.startswith(start):
1220         return s[len(start):]
1221     return s
1222
1223
1224 def remove_end(s, end):
1225     if s.endswith(end):
1226         return s[:-len(end)]
1227     return s
1228
1229
1230 def url_basename(url):
1231     path = compat_urlparse.urlparse(url).path
1232     return path.strip('/').split('/')[-1]
1233
1234
1235 class HEADRequest(compat_urllib_request.Request):
1236     def get_method(self):
1237         return "HEAD"
1238
1239
1240 def int_or_none(v, scale=1, default=None, get_attr=None, invscale=1):
1241     if get_attr:
1242         if v is not None:
1243             v = getattr(v, get_attr, None)
1244     if v == '':
1245         v = None
1246     return default if v is None else (int(v) * invscale // scale)
1247
1248
1249 def str_or_none(v, default=None):
1250     return default if v is None else compat_str(v)
1251
1252
1253 def str_to_int(int_str):
1254     """ A more relaxed version of int_or_none """
1255     if int_str is None:
1256         return None
1257     int_str = re.sub(r'[,\.\+]', '', int_str)
1258     return int(int_str)
1259
1260
1261 def float_or_none(v, scale=1, invscale=1, default=None):
1262     return default if v is None else (float(v) * invscale / scale)
1263
1264
1265 def parse_duration(s):
1266     if not isinstance(s, compat_basestring):
1267         return None
1268
1269     s = s.strip()
1270
1271     m = re.match(
1272         r'''(?ix)(?:P?T)?
1273         (?:
1274             (?P<only_mins>[0-9.]+)\s*(?:mins?|minutes?)\s*|
1275             (?P<only_hours>[0-9.]+)\s*(?:hours?)|
1276
1277             (?:
1278                 (?:
1279                     (?:(?P<days>[0-9]+)\s*(?:[:d]|days?)\s*)?
1280                     (?P<hours>[0-9]+)\s*(?:[:h]|hours?)\s*
1281                 )?
1282                 (?P<mins>[0-9]+)\s*(?:[:m]|mins?|minutes?)\s*
1283             )?
1284             (?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?\s*(?:s|secs?|seconds?)?
1285         )$''', s)
1286     if not m:
1287         return None
1288     res = 0
1289     if m.group('only_mins'):
1290         return float_or_none(m.group('only_mins'), invscale=60)
1291     if m.group('only_hours'):
1292         return float_or_none(m.group('only_hours'), invscale=60 * 60)
1293     if m.group('secs'):
1294         res += int(m.group('secs'))
1295     if m.group('mins'):
1296         res += int(m.group('mins')) * 60
1297     if m.group('hours'):
1298         res += int(m.group('hours')) * 60 * 60
1299     if m.group('days'):
1300         res += int(m.group('days')) * 24 * 60 * 60
1301     if m.group('ms'):
1302         res += float(m.group('ms'))
1303     return res
1304
1305
1306 def prepend_extension(filename, ext):
1307     name, real_ext = os.path.splitext(filename)
1308     return '{0}.{1}{2}'.format(name, ext, real_ext)
1309
1310
1311 def check_executable(exe, args=[]):
1312     """ Checks if the given binary is installed somewhere in PATH, and returns its name.
1313     args can be a list of arguments for a short output (like -version) """
1314     try:
1315         subprocess.Popen([exe] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
1316     except OSError:
1317         return False
1318     return exe
1319
1320
1321 def get_exe_version(exe, args=['--version'],
1322                     version_re=None, unrecognized='present'):
1323     """ Returns the version of the specified executable,
1324     or False if the executable is not present """
1325     try:
1326         out, _ = subprocess.Popen(
1327             [exe] + args,
1328             stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()
1329     except OSError:
1330         return False
1331     if isinstance(out, bytes):  # Python 2.x
1332         out = out.decode('ascii', 'ignore')
1333     return detect_exe_version(out, version_re, unrecognized)
1334
1335
1336 def detect_exe_version(output, version_re=None, unrecognized='present'):
1337     assert isinstance(output, compat_str)
1338     if version_re is None:
1339         version_re = r'version\s+([-0-9._a-zA-Z]+)'
1340     m = re.search(version_re, output)
1341     if m:
1342         return m.group(1)
1343     else:
1344         return unrecognized
1345
1346
1347 class PagedList(object):
1348     def __len__(self):
1349         # This is only useful for tests
1350         return len(self.getslice())
1351
1352
1353 class OnDemandPagedList(PagedList):
1354     def __init__(self, pagefunc, pagesize):
1355         self._pagefunc = pagefunc
1356         self._pagesize = pagesize
1357
1358     def getslice(self, start=0, end=None):
1359         res = []
1360         for pagenum in itertools.count(start // self._pagesize):
1361             firstid = pagenum * self._pagesize
1362             nextfirstid = pagenum * self._pagesize + self._pagesize
1363             if start >= nextfirstid:
1364                 continue
1365
1366             page_results = list(self._pagefunc(pagenum))
1367
1368             startv = (
1369                 start % self._pagesize
1370                 if firstid <= start < nextfirstid
1371                 else 0)
1372
1373             endv = (
1374                 ((end - 1) % self._pagesize) + 1
1375                 if (end is not None and firstid <= end <= nextfirstid)
1376                 else None)
1377
1378             if startv != 0 or endv is not None:
1379                 page_results = page_results[startv:endv]
1380             res.extend(page_results)
1381
1382             # A little optimization - if current page is not "full", ie. does
1383             # not contain page_size videos then we can assume that this page
1384             # is the last one - there are no more ids on further pages -
1385             # i.e. no need to query again.
1386             if len(page_results) + startv < self._pagesize:
1387                 break
1388
1389             # If we got the whole page, but the next page is not interesting,
1390             # break out early as well
1391             if end == nextfirstid:
1392                 break
1393         return res
1394
1395
1396 class InAdvancePagedList(PagedList):
1397     def __init__(self, pagefunc, pagecount, pagesize):
1398         self._pagefunc = pagefunc
1399         self._pagecount = pagecount
1400         self._pagesize = pagesize
1401
1402     def getslice(self, start=0, end=None):
1403         res = []
1404         start_page = start // self._pagesize
1405         end_page = (
1406             self._pagecount if end is None else (end // self._pagesize + 1))
1407         skip_elems = start - start_page * self._pagesize
1408         only_more = None if end is None else end - start
1409         for pagenum in range(start_page, end_page):
1410             page = list(self._pagefunc(pagenum))
1411             if skip_elems:
1412                 page = page[skip_elems:]
1413                 skip_elems = None
1414             if only_more is not None:
1415                 if len(page) < only_more:
1416                     only_more -= len(page)
1417                 else:
1418                     page = page[:only_more]
1419                     res.extend(page)
1420                     break
1421             res.extend(page)
1422         return res
1423
1424
1425 def uppercase_escape(s):
1426     unicode_escape = codecs.getdecoder('unicode_escape')
1427     return re.sub(
1428         r'\\U[0-9a-fA-F]{8}',
1429         lambda m: unicode_escape(m.group(0))[0],
1430         s)
1431
1432
1433 def escape_rfc3986(s):
1434     """Escape non-ASCII characters as suggested by RFC 3986"""
1435     if sys.version_info < (3, 0) and isinstance(s, compat_str):
1436         s = s.encode('utf-8')
1437     return compat_urllib_parse.quote(s, b"%/;:@&=+$,!~*'()?#[]")
1438
1439
1440 def escape_url(url):
1441     """Escape URL as suggested by RFC 3986"""
1442     url_parsed = compat_urllib_parse_urlparse(url)
1443     return url_parsed._replace(
1444         path=escape_rfc3986(url_parsed.path),
1445         params=escape_rfc3986(url_parsed.params),
1446         query=escape_rfc3986(url_parsed.query),
1447         fragment=escape_rfc3986(url_parsed.fragment)
1448     ).geturl()
1449
1450 try:
1451     struct.pack('!I', 0)
1452 except TypeError:
1453     # In Python 2.6 (and some 2.7 versions), struct requires a bytes argument
1454     def struct_pack(spec, *args):
1455         if isinstance(spec, compat_str):
1456             spec = spec.encode('ascii')
1457         return struct.pack(spec, *args)
1458
1459     def struct_unpack(spec, *args):
1460         if isinstance(spec, compat_str):
1461             spec = spec.encode('ascii')
1462         return struct.unpack(spec, *args)
1463 else:
1464     struct_pack = struct.pack
1465     struct_unpack = struct.unpack
1466
1467
1468 def read_batch_urls(batch_fd):
1469     def fixup(url):
1470         if not isinstance(url, compat_str):
1471             url = url.decode('utf-8', 'replace')
1472         BOM_UTF8 = '\xef\xbb\xbf'
1473         if url.startswith(BOM_UTF8):
1474             url = url[len(BOM_UTF8):]
1475         url = url.strip()
1476         if url.startswith(('#', ';', ']')):
1477             return False
1478         return url
1479
1480     with contextlib.closing(batch_fd) as fd:
1481         return [url for url in map(fixup, fd) if url]
1482
1483
1484 def urlencode_postdata(*args, **kargs):
1485     return compat_urllib_parse.urlencode(*args, **kargs).encode('ascii')
1486
1487
1488 try:
1489     etree_iter = xml.etree.ElementTree.Element.iter
1490 except AttributeError:  # Python <=2.6
1491     etree_iter = lambda n: n.findall('.//*')
1492
1493
1494 def parse_xml(s):
1495     class TreeBuilder(xml.etree.ElementTree.TreeBuilder):
1496         def doctype(self, name, pubid, system):
1497             pass  # Ignore doctypes
1498
1499     parser = xml.etree.ElementTree.XMLParser(target=TreeBuilder())
1500     kwargs = {'parser': parser} if sys.version_info >= (2, 7) else {}
1501     tree = xml.etree.ElementTree.XML(s.encode('utf-8'), **kwargs)
1502     # Fix up XML parser in Python 2.x
1503     if sys.version_info < (3, 0):
1504         for n in etree_iter(tree):
1505             if n.text is not None:
1506                 if not isinstance(n.text, compat_str):
1507                     n.text = n.text.decode('utf-8')
1508     return tree
1509
1510
1511 US_RATINGS = {
1512     'G': 0,
1513     'PG': 10,
1514     'PG-13': 13,
1515     'R': 16,
1516     'NC': 18,
1517 }
1518
1519
1520 def parse_age_limit(s):
1521     if s is None:
1522         return None
1523     m = re.match(r'^(?P<age>\d{1,2})\+?$', s)
1524     return int(m.group('age')) if m else US_RATINGS.get(s, None)
1525
1526
1527 def strip_jsonp(code):
1528     return re.sub(
1529         r'(?s)^[a-zA-Z0-9_]+\s*\(\s*(.*)\);?\s*?(?://[^\n]*)*$', r'\1', code)
1530
1531
1532 def js_to_json(code):
1533     def fix_kv(m):
1534         v = m.group(0)
1535         if v in ('true', 'false', 'null'):
1536             return v
1537         if v.startswith('"'):
1538             return v
1539         if v.startswith("'"):
1540             v = v[1:-1]
1541             v = re.sub(r"\\\\|\\'|\"", lambda m: {
1542                 '\\\\': '\\\\',
1543                 "\\'": "'",
1544                 '"': '\\"',
1545             }[m.group(0)], v)
1546         return '"%s"' % v
1547
1548     res = re.sub(r'''(?x)
1549         "(?:[^"\\]*(?:\\\\|\\")?)*"|
1550         '(?:[^'\\]*(?:\\\\|\\')?)*'|
1551         [a-zA-Z_][.a-zA-Z_0-9]*
1552         ''', fix_kv, code)
1553     res = re.sub(r',(\s*\])', lambda m: m.group(1), res)
1554     return res
1555
1556
1557 def qualities(quality_ids):
1558     """ Get a numeric quality value out of a list of possible values """
1559     def q(qid):
1560         try:
1561             return quality_ids.index(qid)
1562         except ValueError:
1563             return -1
1564     return q
1565
1566
1567 DEFAULT_OUTTMPL = '%(title)s-%(id)s.%(ext)s'
1568
1569
1570 def limit_length(s, length):
1571     """ Add ellipses to overly long strings """
1572     if s is None:
1573         return None
1574     ELLIPSES = '...'
1575     if len(s) > length:
1576         return s[:length - len(ELLIPSES)] + ELLIPSES
1577     return s
1578
1579
1580 def version_tuple(v):
1581     return tuple(int(e) for e in re.split(r'[-.]', v))
1582
1583
1584 def is_outdated_version(version, limit, assume_new=True):
1585     if not version:
1586         return not assume_new
1587     try:
1588         return version_tuple(version) < version_tuple(limit)
1589     except ValueError:
1590         return not assume_new
1591
1592
1593 def ytdl_is_updateable():
1594     """ Returns if youtube-dl can be updated with -U """
1595     from zipimport import zipimporter
1596
1597     return isinstance(globals().get('__loader__'), zipimporter) or hasattr(sys, 'frozen')
1598
1599
1600 def args_to_str(args):
1601     # Get a short string representation for a subprocess command
1602     return ' '.join(shlex_quote(a) for a in args)
1603
1604
1605 def urlhandle_detect_ext(url_handle):
1606     try:
1607         url_handle.headers
1608         getheader = lambda h: url_handle.headers[h]
1609     except AttributeError:  # Python < 3
1610         getheader = url_handle.info().getheader
1611
1612     cd = getheader('Content-Disposition')
1613     if cd:
1614         m = re.match(r'attachment;\s*filename="(?P<filename>[^"]+)"', cd)
1615         if m:
1616             e = determine_ext(m.group('filename'), default_ext=None)
1617             if e:
1618                 return e
1619
1620     return getheader('Content-Type').split("/")[1]
1621
1622
1623 def age_restricted(content_limit, age_limit):
1624     """ Returns True iff the content should be blocked """
1625
1626     if age_limit is None:  # No limit set
1627         return False
1628     if content_limit is None:
1629         return False  # Content available for everyone
1630     return age_limit < content_limit
1631
1632
1633 def is_html(first_bytes):
1634     """ Detect whether a file contains HTML by examining its first bytes. """
1635
1636     BOMS = [
1637         (b'\xef\xbb\xbf', 'utf-8'),
1638         (b'\x00\x00\xfe\xff', 'utf-32-be'),
1639         (b'\xff\xfe\x00\x00', 'utf-32-le'),
1640         (b'\xff\xfe', 'utf-16-le'),
1641         (b'\xfe\xff', 'utf-16-be'),
1642     ]
1643     for bom, enc in BOMS:
1644         if first_bytes.startswith(bom):
1645             s = first_bytes[len(bom):].decode(enc, 'replace')
1646             break
1647     else:
1648         s = first_bytes.decode('utf-8', 'replace')
1649
1650     return re.match(r'^\s*<', s)
1651
1652
1653 def determine_protocol(info_dict):
1654     protocol = info_dict.get('protocol')
1655     if protocol is not None:
1656         return protocol
1657
1658     url = info_dict['url']
1659     if url.startswith('rtmp'):
1660         return 'rtmp'
1661     elif url.startswith('mms'):
1662         return 'mms'
1663     elif url.startswith('rtsp'):
1664         return 'rtsp'
1665
1666     ext = determine_ext(url)
1667     if ext == 'm3u8':
1668         return 'm3u8'
1669     elif ext == 'f4m':
1670         return 'f4m'
1671
1672     return compat_urllib_parse_urlparse(url).scheme
1673
1674
1675 def render_table(header_row, data):
1676     """ Render a list of rows, each as a list of values """
1677     table = [header_row] + data
1678     max_lens = [max(len(compat_str(v)) for v in col) for col in zip(*table)]
1679     format_str = ' '.join('%-' + compat_str(ml + 1) + 's' for ml in max_lens[:-1]) + '%s'
1680     return '\n'.join(format_str % tuple(row) for row in table)