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