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