[utils] Add `get_subprocess_encoding` and filename/argument decode counterparts
[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 takewhile_inclusive(pred, seq):
1132     """ Like itertools.takewhile, but include the latest evaluated element
1133         (the first element so that Not pred(e)) """
1134     for e in seq:
1135         yield e
1136         if not pred(e):
1137             return
1138
1139
1140 def smuggle_url(url, data):
1141     """ Pass additional data in a URL for internal use. """
1142
1143     sdata = compat_urllib_parse.urlencode(
1144         {'__youtubedl_smuggle': json.dumps(data)})
1145     return url + '#' + sdata
1146
1147
1148 def unsmuggle_url(smug_url, default=None):
1149     if '#__youtubedl_smuggle' not in smug_url:
1150         return smug_url, default
1151     url, _, sdata = smug_url.rpartition('#')
1152     jsond = compat_parse_qs(sdata)['__youtubedl_smuggle'][0]
1153     data = json.loads(jsond)
1154     return url, data
1155
1156
1157 def format_bytes(bytes):
1158     if bytes is None:
1159         return 'N/A'
1160     if type(bytes) is str:
1161         bytes = float(bytes)
1162     if bytes == 0.0:
1163         exponent = 0
1164     else:
1165         exponent = int(math.log(bytes, 1024.0))
1166     suffix = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'][exponent]
1167     converted = float(bytes) / float(1024 ** exponent)
1168     return '%.2f%s' % (converted, suffix)
1169
1170
1171 def parse_filesize(s):
1172     if s is None:
1173         return None
1174
1175     # The lower-case forms are of course incorrect and inofficial,
1176     # but we support those too
1177     _UNIT_TABLE = {
1178         'B': 1,
1179         'b': 1,
1180         'KiB': 1024,
1181         'KB': 1000,
1182         'kB': 1024,
1183         'Kb': 1000,
1184         'MiB': 1024 ** 2,
1185         'MB': 1000 ** 2,
1186         'mB': 1024 ** 2,
1187         'Mb': 1000 ** 2,
1188         'GiB': 1024 ** 3,
1189         'GB': 1000 ** 3,
1190         'gB': 1024 ** 3,
1191         'Gb': 1000 ** 3,
1192         'TiB': 1024 ** 4,
1193         'TB': 1000 ** 4,
1194         'tB': 1024 ** 4,
1195         'Tb': 1000 ** 4,
1196         'PiB': 1024 ** 5,
1197         'PB': 1000 ** 5,
1198         'pB': 1024 ** 5,
1199         'Pb': 1000 ** 5,
1200         'EiB': 1024 ** 6,
1201         'EB': 1000 ** 6,
1202         'eB': 1024 ** 6,
1203         'Eb': 1000 ** 6,
1204         'ZiB': 1024 ** 7,
1205         'ZB': 1000 ** 7,
1206         'zB': 1024 ** 7,
1207         'Zb': 1000 ** 7,
1208         'YiB': 1024 ** 8,
1209         'YB': 1000 ** 8,
1210         'yB': 1024 ** 8,
1211         'Yb': 1000 ** 8,
1212     }
1213
1214     units_re = '|'.join(re.escape(u) for u in _UNIT_TABLE)
1215     m = re.match(
1216         r'(?P<num>[0-9]+(?:[,.][0-9]*)?)\s*(?P<unit>%s)' % units_re, s)
1217     if not m:
1218         return None
1219
1220     num_str = m.group('num').replace(',', '.')
1221     mult = _UNIT_TABLE[m.group('unit')]
1222     return int(float(num_str) * mult)
1223
1224
1225 def month_by_name(name):
1226     """ Return the number of a month by (locale-independently) English name """
1227
1228     try:
1229         return ENGLISH_MONTH_NAMES.index(name) + 1
1230     except ValueError:
1231         return None
1232
1233
1234 def month_by_abbreviation(abbrev):
1235     """ Return the number of a month by (locale-independently) English
1236         abbreviations """
1237
1238     try:
1239         return [s[:3] for s in ENGLISH_MONTH_NAMES].index(abbrev) + 1
1240     except ValueError:
1241         return None
1242
1243
1244 def fix_xml_ampersands(xml_str):
1245     """Replace all the '&' by '&amp;' in XML"""
1246     return re.sub(
1247         r'&(?!amp;|lt;|gt;|apos;|quot;|#x[0-9a-fA-F]{,4};|#[0-9]{,4};)',
1248         '&amp;',
1249         xml_str)
1250
1251
1252 def setproctitle(title):
1253     assert isinstance(title, compat_str)
1254     try:
1255         libc = ctypes.cdll.LoadLibrary("libc.so.6")
1256     except OSError:
1257         return
1258     title_bytes = title.encode('utf-8')
1259     buf = ctypes.create_string_buffer(len(title_bytes))
1260     buf.value = title_bytes
1261     try:
1262         libc.prctl(15, buf, 0, 0, 0)
1263     except AttributeError:
1264         return  # Strange libc, just skip this
1265
1266
1267 def remove_start(s, start):
1268     if s.startswith(start):
1269         return s[len(start):]
1270     return s
1271
1272
1273 def remove_end(s, end):
1274     if s.endswith(end):
1275         return s[:-len(end)]
1276     return s
1277
1278
1279 def url_basename(url):
1280     path = compat_urlparse.urlparse(url).path
1281     return path.strip('/').split('/')[-1]
1282
1283
1284 class HEADRequest(compat_urllib_request.Request):
1285     def get_method(self):
1286         return "HEAD"
1287
1288
1289 def int_or_none(v, scale=1, default=None, get_attr=None, invscale=1):
1290     if get_attr:
1291         if v is not None:
1292             v = getattr(v, get_attr, None)
1293     if v == '':
1294         v = None
1295     return default if v is None else (int(v) * invscale // scale)
1296
1297
1298 def str_or_none(v, default=None):
1299     return default if v is None else compat_str(v)
1300
1301
1302 def str_to_int(int_str):
1303     """ A more relaxed version of int_or_none """
1304     if int_str is None:
1305         return None
1306     int_str = re.sub(r'[,\.\+]', '', int_str)
1307     return int(int_str)
1308
1309
1310 def float_or_none(v, scale=1, invscale=1, default=None):
1311     return default if v is None else (float(v) * invscale / scale)
1312
1313
1314 def parse_duration(s):
1315     if not isinstance(s, compat_basestring):
1316         return None
1317
1318     s = s.strip()
1319
1320     m = re.match(
1321         r'''(?ix)(?:P?T)?
1322         (?:
1323             (?P<only_mins>[0-9.]+)\s*(?:mins?|minutes?)\s*|
1324             (?P<only_hours>[0-9.]+)\s*(?:hours?)|
1325
1326             \s*(?P<hours_reversed>[0-9]+)\s*(?:[:h]|hours?)\s*(?P<mins_reversed>[0-9]+)\s*(?:[:m]|mins?|minutes?)\s*|
1327             (?:
1328                 (?:
1329                     (?:(?P<days>[0-9]+)\s*(?:[:d]|days?)\s*)?
1330                     (?P<hours>[0-9]+)\s*(?:[:h]|hours?)\s*
1331                 )?
1332                 (?P<mins>[0-9]+)\s*(?:[:m]|mins?|minutes?)\s*
1333             )?
1334             (?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?\s*(?:s|secs?|seconds?)?
1335         )$''', s)
1336     if not m:
1337         return None
1338     res = 0
1339     if m.group('only_mins'):
1340         return float_or_none(m.group('only_mins'), invscale=60)
1341     if m.group('only_hours'):
1342         return float_or_none(m.group('only_hours'), invscale=60 * 60)
1343     if m.group('secs'):
1344         res += int(m.group('secs'))
1345     if m.group('mins_reversed'):
1346         res += int(m.group('mins_reversed')) * 60
1347     if m.group('mins'):
1348         res += int(m.group('mins')) * 60
1349     if m.group('hours'):
1350         res += int(m.group('hours')) * 60 * 60
1351     if m.group('hours_reversed'):
1352         res += int(m.group('hours_reversed')) * 60 * 60
1353     if m.group('days'):
1354         res += int(m.group('days')) * 24 * 60 * 60
1355     if m.group('ms'):
1356         res += float(m.group('ms'))
1357     return res
1358
1359
1360 def prepend_extension(filename, ext):
1361     name, real_ext = os.path.splitext(filename)
1362     return '{0}.{1}{2}'.format(name, ext, real_ext)
1363
1364
1365 def check_executable(exe, args=[]):
1366     """ Checks if the given binary is installed somewhere in PATH, and returns its name.
1367     args can be a list of arguments for a short output (like -version) """
1368     try:
1369         subprocess.Popen([exe] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
1370     except OSError:
1371         return False
1372     return exe
1373
1374
1375 def get_exe_version(exe, args=['--version'],
1376                     version_re=None, unrecognized='present'):
1377     """ Returns the version of the specified executable,
1378     or False if the executable is not present """
1379     try:
1380         out, _ = subprocess.Popen(
1381             [exe] + args,
1382             stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()
1383     except OSError:
1384         return False
1385     if isinstance(out, bytes):  # Python 2.x
1386         out = out.decode('ascii', 'ignore')
1387     return detect_exe_version(out, version_re, unrecognized)
1388
1389
1390 def detect_exe_version(output, version_re=None, unrecognized='present'):
1391     assert isinstance(output, compat_str)
1392     if version_re is None:
1393         version_re = r'version\s+([-0-9._a-zA-Z]+)'
1394     m = re.search(version_re, output)
1395     if m:
1396         return m.group(1)
1397     else:
1398         return unrecognized
1399
1400
1401 class PagedList(object):
1402     def __len__(self):
1403         # This is only useful for tests
1404         return len(self.getslice())
1405
1406
1407 class OnDemandPagedList(PagedList):
1408     def __init__(self, pagefunc, pagesize):
1409         self._pagefunc = pagefunc
1410         self._pagesize = pagesize
1411
1412     def getslice(self, start=0, end=None):
1413         res = []
1414         for pagenum in itertools.count(start // self._pagesize):
1415             firstid = pagenum * self._pagesize
1416             nextfirstid = pagenum * self._pagesize + self._pagesize
1417             if start >= nextfirstid:
1418                 continue
1419
1420             page_results = list(self._pagefunc(pagenum))
1421
1422             startv = (
1423                 start % self._pagesize
1424                 if firstid <= start < nextfirstid
1425                 else 0)
1426
1427             endv = (
1428                 ((end - 1) % self._pagesize) + 1
1429                 if (end is not None and firstid <= end <= nextfirstid)
1430                 else None)
1431
1432             if startv != 0 or endv is not None:
1433                 page_results = page_results[startv:endv]
1434             res.extend(page_results)
1435
1436             # A little optimization - if current page is not "full", ie. does
1437             # not contain page_size videos then we can assume that this page
1438             # is the last one - there are no more ids on further pages -
1439             # i.e. no need to query again.
1440             if len(page_results) + startv < self._pagesize:
1441                 break
1442
1443             # If we got the whole page, but the next page is not interesting,
1444             # break out early as well
1445             if end == nextfirstid:
1446                 break
1447         return res
1448
1449
1450 class InAdvancePagedList(PagedList):
1451     def __init__(self, pagefunc, pagecount, pagesize):
1452         self._pagefunc = pagefunc
1453         self._pagecount = pagecount
1454         self._pagesize = pagesize
1455
1456     def getslice(self, start=0, end=None):
1457         res = []
1458         start_page = start // self._pagesize
1459         end_page = (
1460             self._pagecount if end is None else (end // self._pagesize + 1))
1461         skip_elems = start - start_page * self._pagesize
1462         only_more = None if end is None else end - start
1463         for pagenum in range(start_page, end_page):
1464             page = list(self._pagefunc(pagenum))
1465             if skip_elems:
1466                 page = page[skip_elems:]
1467                 skip_elems = None
1468             if only_more is not None:
1469                 if len(page) < only_more:
1470                     only_more -= len(page)
1471                 else:
1472                     page = page[:only_more]
1473                     res.extend(page)
1474                     break
1475             res.extend(page)
1476         return res
1477
1478
1479 def uppercase_escape(s):
1480     unicode_escape = codecs.getdecoder('unicode_escape')
1481     return re.sub(
1482         r'\\U[0-9a-fA-F]{8}',
1483         lambda m: unicode_escape(m.group(0))[0],
1484         s)
1485
1486
1487 def escape_rfc3986(s):
1488     """Escape non-ASCII characters as suggested by RFC 3986"""
1489     if sys.version_info < (3, 0) and isinstance(s, compat_str):
1490         s = s.encode('utf-8')
1491     return compat_urllib_parse.quote(s, b"%/;:@&=+$,!~*'()?#[]")
1492
1493
1494 def escape_url(url):
1495     """Escape URL as suggested by RFC 3986"""
1496     url_parsed = compat_urllib_parse_urlparse(url)
1497     return url_parsed._replace(
1498         path=escape_rfc3986(url_parsed.path),
1499         params=escape_rfc3986(url_parsed.params),
1500         query=escape_rfc3986(url_parsed.query),
1501         fragment=escape_rfc3986(url_parsed.fragment)
1502     ).geturl()
1503
1504 try:
1505     struct.pack('!I', 0)
1506 except TypeError:
1507     # In Python 2.6 (and some 2.7 versions), struct requires a bytes argument
1508     def struct_pack(spec, *args):
1509         if isinstance(spec, compat_str):
1510             spec = spec.encode('ascii')
1511         return struct.pack(spec, *args)
1512
1513     def struct_unpack(spec, *args):
1514         if isinstance(spec, compat_str):
1515             spec = spec.encode('ascii')
1516         return struct.unpack(spec, *args)
1517 else:
1518     struct_pack = struct.pack
1519     struct_unpack = struct.unpack
1520
1521
1522 def read_batch_urls(batch_fd):
1523     def fixup(url):
1524         if not isinstance(url, compat_str):
1525             url = url.decode('utf-8', 'replace')
1526         BOM_UTF8 = '\xef\xbb\xbf'
1527         if url.startswith(BOM_UTF8):
1528             url = url[len(BOM_UTF8):]
1529         url = url.strip()
1530         if url.startswith(('#', ';', ']')):
1531             return False
1532         return url
1533
1534     with contextlib.closing(batch_fd) as fd:
1535         return [url for url in map(fixup, fd) if url]
1536
1537
1538 def urlencode_postdata(*args, **kargs):
1539     return compat_urllib_parse.urlencode(*args, **kargs).encode('ascii')
1540
1541
1542 try:
1543     etree_iter = xml.etree.ElementTree.Element.iter
1544 except AttributeError:  # Python <=2.6
1545     etree_iter = lambda n: n.findall('.//*')
1546
1547
1548 def parse_xml(s):
1549     class TreeBuilder(xml.etree.ElementTree.TreeBuilder):
1550         def doctype(self, name, pubid, system):
1551             pass  # Ignore doctypes
1552
1553     parser = xml.etree.ElementTree.XMLParser(target=TreeBuilder())
1554     kwargs = {'parser': parser} if sys.version_info >= (2, 7) else {}
1555     tree = xml.etree.ElementTree.XML(s.encode('utf-8'), **kwargs)
1556     # Fix up XML parser in Python 2.x
1557     if sys.version_info < (3, 0):
1558         for n in etree_iter(tree):
1559             if n.text is not None:
1560                 if not isinstance(n.text, compat_str):
1561                     n.text = n.text.decode('utf-8')
1562     return tree
1563
1564
1565 US_RATINGS = {
1566     'G': 0,
1567     'PG': 10,
1568     'PG-13': 13,
1569     'R': 16,
1570     'NC': 18,
1571 }
1572
1573
1574 def parse_age_limit(s):
1575     if s is None:
1576         return None
1577     m = re.match(r'^(?P<age>\d{1,2})\+?$', s)
1578     return int(m.group('age')) if m else US_RATINGS.get(s, None)
1579
1580
1581 def strip_jsonp(code):
1582     return re.sub(
1583         r'(?s)^[a-zA-Z0-9_]+\s*\(\s*(.*)\);?\s*?(?://[^\n]*)*$', r'\1', code)
1584
1585
1586 def js_to_json(code):
1587     def fix_kv(m):
1588         v = m.group(0)
1589         if v in ('true', 'false', 'null'):
1590             return v
1591         if v.startswith('"'):
1592             return v
1593         if v.startswith("'"):
1594             v = v[1:-1]
1595             v = re.sub(r"\\\\|\\'|\"", lambda m: {
1596                 '\\\\': '\\\\',
1597                 "\\'": "'",
1598                 '"': '\\"',
1599             }[m.group(0)], v)
1600         return '"%s"' % v
1601
1602     res = re.sub(r'''(?x)
1603         "(?:[^"\\]*(?:\\\\|\\['"nu]))*[^"\\]*"|
1604         '(?:[^'\\]*(?:\\\\|\\['"nu]))*[^'\\]*'|
1605         [a-zA-Z_][.a-zA-Z_0-9]*
1606         ''', fix_kv, code)
1607     res = re.sub(r',(\s*[\]}])', lambda m: m.group(1), res)
1608     return res
1609
1610
1611 def qualities(quality_ids):
1612     """ Get a numeric quality value out of a list of possible values """
1613     def q(qid):
1614         try:
1615             return quality_ids.index(qid)
1616         except ValueError:
1617             return -1
1618     return q
1619
1620
1621 DEFAULT_OUTTMPL = '%(title)s-%(id)s.%(ext)s'
1622
1623
1624 def limit_length(s, length):
1625     """ Add ellipses to overly long strings """
1626     if s is None:
1627         return None
1628     ELLIPSES = '...'
1629     if len(s) > length:
1630         return s[:length - len(ELLIPSES)] + ELLIPSES
1631     return s
1632
1633
1634 def version_tuple(v):
1635     return tuple(int(e) for e in re.split(r'[-.]', v))
1636
1637
1638 def is_outdated_version(version, limit, assume_new=True):
1639     if not version:
1640         return not assume_new
1641     try:
1642         return version_tuple(version) < version_tuple(limit)
1643     except ValueError:
1644         return not assume_new
1645
1646
1647 def ytdl_is_updateable():
1648     """ Returns if youtube-dl can be updated with -U """
1649     from zipimport import zipimporter
1650
1651     return isinstance(globals().get('__loader__'), zipimporter) or hasattr(sys, 'frozen')
1652
1653
1654 def args_to_str(args):
1655     # Get a short string representation for a subprocess command
1656     return ' '.join(shlex_quote(a) for a in args)
1657
1658
1659 def mimetype2ext(mt):
1660     _, _, res = mt.rpartition('/')
1661
1662     return {
1663         'x-ms-wmv': 'wmv',
1664         'x-mp4-fragmented': 'mp4',
1665     }.get(res, res)
1666
1667
1668 def urlhandle_detect_ext(url_handle):
1669     try:
1670         url_handle.headers
1671         getheader = lambda h: url_handle.headers[h]
1672     except AttributeError:  # Python < 3
1673         getheader = url_handle.info().getheader
1674
1675     cd = getheader('Content-Disposition')
1676     if cd:
1677         m = re.match(r'attachment;\s*filename="(?P<filename>[^"]+)"', cd)
1678         if m:
1679             e = determine_ext(m.group('filename'), default_ext=None)
1680             if e:
1681                 return e
1682
1683     return mimetype2ext(getheader('Content-Type'))
1684
1685
1686 def age_restricted(content_limit, age_limit):
1687     """ Returns True iff the content should be blocked """
1688
1689     if age_limit is None:  # No limit set
1690         return False
1691     if content_limit is None:
1692         return False  # Content available for everyone
1693     return age_limit < content_limit
1694
1695
1696 def is_html(first_bytes):
1697     """ Detect whether a file contains HTML by examining its first bytes. """
1698
1699     BOMS = [
1700         (b'\xef\xbb\xbf', 'utf-8'),
1701         (b'\x00\x00\xfe\xff', 'utf-32-be'),
1702         (b'\xff\xfe\x00\x00', 'utf-32-le'),
1703         (b'\xff\xfe', 'utf-16-le'),
1704         (b'\xfe\xff', 'utf-16-be'),
1705     ]
1706     for bom, enc in BOMS:
1707         if first_bytes.startswith(bom):
1708             s = first_bytes[len(bom):].decode(enc, 'replace')
1709             break
1710     else:
1711         s = first_bytes.decode('utf-8', 'replace')
1712
1713     return re.match(r'^\s*<', s)
1714
1715
1716 def determine_protocol(info_dict):
1717     protocol = info_dict.get('protocol')
1718     if protocol is not None:
1719         return protocol
1720
1721     url = info_dict['url']
1722     if url.startswith('rtmp'):
1723         return 'rtmp'
1724     elif url.startswith('mms'):
1725         return 'mms'
1726     elif url.startswith('rtsp'):
1727         return 'rtsp'
1728
1729     ext = determine_ext(url)
1730     if ext == 'm3u8':
1731         return 'm3u8'
1732     elif ext == 'f4m':
1733         return 'f4m'
1734
1735     return compat_urllib_parse_urlparse(url).scheme
1736
1737
1738 def render_table(header_row, data):
1739     """ Render a list of rows, each as a list of values """
1740     table = [header_row] + data
1741     max_lens = [max(len(compat_str(v)) for v in col) for col in zip(*table)]
1742     format_str = ' '.join('%-' + compat_str(ml + 1) + 's' for ml in max_lens[:-1]) + '%s'
1743     return '\n'.join(format_str % tuple(row) for row in table)
1744
1745
1746 def _match_one(filter_part, dct):
1747     COMPARISON_OPERATORS = {
1748         '<': operator.lt,
1749         '<=': operator.le,
1750         '>': operator.gt,
1751         '>=': operator.ge,
1752         '=': operator.eq,
1753         '!=': operator.ne,
1754     }
1755     operator_rex = re.compile(r'''(?x)\s*
1756         (?P<key>[a-z_]+)
1757         \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
1758         (?:
1759             (?P<intval>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)|
1760             (?P<strval>(?![0-9.])[a-z0-9A-Z]*)
1761         )
1762         \s*$
1763         ''' % '|'.join(map(re.escape, COMPARISON_OPERATORS.keys())))
1764     m = operator_rex.search(filter_part)
1765     if m:
1766         op = COMPARISON_OPERATORS[m.group('op')]
1767         if m.group('strval') is not None:
1768             if m.group('op') not in ('=', '!='):
1769                 raise ValueError(
1770                     'Operator %s does not support string values!' % m.group('op'))
1771             comparison_value = m.group('strval')
1772         else:
1773             try:
1774                 comparison_value = int(m.group('intval'))
1775             except ValueError:
1776                 comparison_value = parse_filesize(m.group('intval'))
1777                 if comparison_value is None:
1778                     comparison_value = parse_filesize(m.group('intval') + 'B')
1779                 if comparison_value is None:
1780                     raise ValueError(
1781                         'Invalid integer value %r in filter part %r' % (
1782                             m.group('intval'), filter_part))
1783         actual_value = dct.get(m.group('key'))
1784         if actual_value is None:
1785             return m.group('none_inclusive')
1786         return op(actual_value, comparison_value)
1787
1788     UNARY_OPERATORS = {
1789         '': lambda v: v is not None,
1790         '!': lambda v: v is None,
1791     }
1792     operator_rex = re.compile(r'''(?x)\s*
1793         (?P<op>%s)\s*(?P<key>[a-z_]+)
1794         \s*$
1795         ''' % '|'.join(map(re.escape, UNARY_OPERATORS.keys())))
1796     m = operator_rex.search(filter_part)
1797     if m:
1798         op = UNARY_OPERATORS[m.group('op')]
1799         actual_value = dct.get(m.group('key'))
1800         return op(actual_value)
1801
1802     raise ValueError('Invalid filter part %r' % filter_part)
1803
1804
1805 def match_str(filter_str, dct):
1806     """ Filter a dictionary with a simple string syntax. Returns True (=passes filter) or false """
1807
1808     return all(
1809         _match_one(filter_part, dct) for filter_part in filter_str.split('&'))
1810
1811
1812 def match_filter_func(filter_str):
1813     def _match_func(info_dict):
1814         if match_str(filter_str, info_dict):
1815             return None
1816         else:
1817             video_title = info_dict.get('title', info_dict.get('id', 'video'))
1818             return '%s does not pass filter %s, skipping ..' % (video_title, filter_str)
1819     return _match_func
1820
1821
1822 def parse_dfxp_time_expr(time_expr):
1823     if not time_expr:
1824         return 0.0
1825
1826     mobj = re.match(r'^(?P<time_offset>\d+(?:\.\d+)?)s?$', time_expr)
1827     if mobj:
1828         return float(mobj.group('time_offset'))
1829
1830     mobj = re.match(r'^(\d+):(\d\d):(\d\d(?:\.\d+)?)$', time_expr)
1831     if mobj:
1832         return 3600 * int(mobj.group(1)) + 60 * int(mobj.group(2)) + float(mobj.group(3))
1833
1834
1835 def format_srt_time(seconds):
1836     (mins, secs) = divmod(seconds, 60)
1837     (hours, mins) = divmod(mins, 60)
1838     millisecs = (secs - int(secs)) * 1000
1839     secs = int(secs)
1840     return '%02d:%02d:%02d,%03d' % (hours, mins, secs, millisecs)
1841
1842
1843 def dfxp2srt(dfxp_data):
1844     _x = functools.partial(xpath_with_ns, ns_map={'ttml': 'http://www.w3.org/ns/ttml'})
1845
1846     def parse_node(node):
1847         str_or_empty = functools.partial(str_or_none, default='')
1848
1849         out = str_or_empty(node.text)
1850
1851         for child in node:
1852             if child.tag == _x('ttml:br'):
1853                 out += '\n' + str_or_empty(child.tail)
1854             elif child.tag == _x('ttml:span'):
1855                 out += str_or_empty(parse_node(child))
1856             else:
1857                 out += str_or_empty(xml.etree.ElementTree.tostring(child))
1858
1859         return out
1860
1861     dfxp = xml.etree.ElementTree.fromstring(dfxp_data.encode('utf-8'))
1862     out = []
1863     paras = dfxp.findall(_x('.//ttml:p'))
1864
1865     for para, index in zip(paras, itertools.count(1)):
1866         out.append('%d\n%s --> %s\n%s\n\n' % (
1867             index,
1868             format_srt_time(parse_dfxp_time_expr(para.attrib.get('begin'))),
1869             format_srt_time(parse_dfxp_time_expr(para.attrib.get('end'))),
1870             parse_node(para)))
1871
1872     return ''.join(out)
1873
1874
1875 class PerRequestProxyHandler(compat_urllib_request.ProxyHandler):
1876     def __init__(self, proxies=None):
1877         # Set default handlers
1878         for type in ('http', 'https'):
1879             setattr(self, '%s_open' % type,
1880                     lambda r, proxy='__noproxy__', type=type, meth=self.proxy_open:
1881                         meth(r, proxy, type))
1882         return compat_urllib_request.ProxyHandler.__init__(self, proxies)
1883
1884     def proxy_open(self, req, proxy, type):
1885         req_proxy = req.headers.get('Ytdl-request-proxy')
1886         if req_proxy is not None:
1887             proxy = req_proxy
1888             del req.headers['Ytdl-request-proxy']
1889
1890         if proxy == '__noproxy__':
1891             return None  # No Proxy
1892         return compat_urllib_request.ProxyHandler.proxy_open(
1893             self, req, proxy, type)