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