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