Merge pull request #8092 from bpfoley/twitter-thumbnail
[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 base64
7 import binascii
8 import calendar
9 import codecs
10 import contextlib
11 import ctypes
12 import datetime
13 import email.utils
14 import errno
15 import functools
16 import gzip
17 import itertools
18 import io
19 import json
20 import locale
21 import math
22 import operator
23 import os
24 import pipes
25 import platform
26 import re
27 import ssl
28 import socket
29 import struct
30 import subprocess
31 import sys
32 import tempfile
33 import traceback
34 import xml.etree.ElementTree
35 import zlib
36
37 from .compat import (
38     compat_HTMLParser,
39     compat_basestring,
40     compat_chr,
41     compat_etree_fromstring,
42     compat_html_entities,
43     compat_http_client,
44     compat_kwargs,
45     compat_parse_qs,
46     compat_socket_create_connection,
47     compat_str,
48     compat_urllib_error,
49     compat_urllib_parse,
50     compat_urllib_parse_urlparse,
51     compat_urllib_request,
52     compat_urlparse,
53     shlex_quote,
54 )
55
56
57 # This is not clearly defined otherwise
58 compiled_regex_type = type(re.compile(''))
59
60 std_headers = {
61     'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/44.0 (Chrome)',
62     'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
63     'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
64     'Accept-Encoding': 'gzip, deflate',
65     'Accept-Language': 'en-us,en;q=0.5',
66 }
67
68
69 NO_DEFAULT = object()
70
71 ENGLISH_MONTH_NAMES = [
72     'January', 'February', 'March', 'April', 'May', 'June',
73     'July', 'August', 'September', 'October', 'November', 'December']
74
75 KNOWN_EXTENSIONS = (
76     'mp4', 'm4a', 'm4p', 'm4b', 'm4r', 'm4v', 'aac',
77     'flv', 'f4v', 'f4a', 'f4b',
78     'webm', 'ogg', 'ogv', 'oga', 'ogx', 'spx', 'opus',
79     'mkv', 'mka', 'mk3d',
80     'avi', 'divx',
81     'mov',
82     'asf', 'wmv', 'wma',
83     '3gp', '3g2',
84     'mp3',
85     'flac',
86     'ape',
87     'wav',
88     'f4f', 'f4m', 'm3u8', 'smil')
89
90
91 def preferredencoding():
92     """Get preferred encoding.
93
94     Returns the best encoding scheme for the system, based on
95     locale.getpreferredencoding() and some further tweaks.
96     """
97     try:
98         pref = locale.getpreferredencoding()
99         'TEST'.encode(pref)
100     except Exception:
101         pref = 'UTF-8'
102
103     return pref
104
105
106 def write_json_file(obj, fn):
107     """ Encode obj as JSON and write it to fn, atomically if possible """
108
109     fn = encodeFilename(fn)
110     if sys.version_info < (3, 0) and sys.platform != 'win32':
111         encoding = get_filesystem_encoding()
112         # os.path.basename returns a bytes object, but NamedTemporaryFile
113         # will fail if the filename contains non ascii characters unless we
114         # use a unicode object
115         path_basename = lambda f: os.path.basename(fn).decode(encoding)
116         # the same for os.path.dirname
117         path_dirname = lambda f: os.path.dirname(fn).decode(encoding)
118     else:
119         path_basename = os.path.basename
120         path_dirname = os.path.dirname
121
122     args = {
123         'suffix': '.tmp',
124         'prefix': path_basename(fn) + '.',
125         'dir': path_dirname(fn),
126         'delete': False,
127     }
128
129     # In Python 2.x, json.dump expects a bytestream.
130     # In Python 3.x, it writes to a character stream
131     if sys.version_info < (3, 0):
132         args['mode'] = 'wb'
133     else:
134         args.update({
135             'mode': 'w',
136             'encoding': 'utf-8',
137         })
138
139     tf = tempfile.NamedTemporaryFile(**compat_kwargs(args))
140
141     try:
142         with tf:
143             json.dump(obj, tf)
144         if sys.platform == 'win32':
145             # Need to remove existing file on Windows, else os.rename raises
146             # WindowsError or FileExistsError.
147             try:
148                 os.unlink(fn)
149             except OSError:
150                 pass
151         os.rename(tf.name, fn)
152     except Exception:
153         try:
154             os.remove(tf.name)
155         except OSError:
156             pass
157         raise
158
159
160 if sys.version_info >= (2, 7):
161     def find_xpath_attr(node, xpath, key, val=None):
162         """ Find the xpath xpath[@key=val] """
163         assert re.match(r'^[a-zA-Z_-]+$', key)
164         expr = xpath + ('[@%s]' % key if val is None else "[@%s='%s']" % (key, val))
165         return node.find(expr)
166 else:
167     def find_xpath_attr(node, xpath, key, val=None):
168         # Here comes the crazy part: In 2.6, if the xpath is a unicode,
169         # .//node does not match if a node is a direct child of . !
170         if isinstance(xpath, compat_str):
171             xpath = xpath.encode('ascii')
172
173         for f in node.findall(xpath):
174             if key not in f.attrib:
175                 continue
176             if val is None or f.attrib.get(key) == val:
177                 return f
178         return None
179
180 # On python2.6 the xml.etree.ElementTree.Element methods don't support
181 # the namespace parameter
182
183
184 def xpath_with_ns(path, ns_map):
185     components = [c.split(':') for c in path.split('/')]
186     replaced = []
187     for c in components:
188         if len(c) == 1:
189             replaced.append(c[0])
190         else:
191             ns, tag = c
192             replaced.append('{%s}%s' % (ns_map[ns], tag))
193     return '/'.join(replaced)
194
195
196 def xpath_element(node, xpath, name=None, fatal=False, default=NO_DEFAULT):
197     def _find_xpath(xpath):
198         if sys.version_info < (2, 7):  # Crazy 2.6
199             xpath = xpath.encode('ascii')
200         return node.find(xpath)
201
202     if isinstance(xpath, (str, compat_str)):
203         n = _find_xpath(xpath)
204     else:
205         for xp in xpath:
206             n = _find_xpath(xp)
207             if n is not None:
208                 break
209
210     if n is None:
211         if default is not NO_DEFAULT:
212             return default
213         elif fatal:
214             name = xpath if name is None else name
215             raise ExtractorError('Could not find XML element %s' % name)
216         else:
217             return None
218     return n
219
220
221 def xpath_text(node, xpath, name=None, fatal=False, default=NO_DEFAULT):
222     n = xpath_element(node, xpath, name, fatal=fatal, default=default)
223     if n is None or n == default:
224         return n
225     if n.text is None:
226         if default is not NO_DEFAULT:
227             return default
228         elif fatal:
229             name = xpath if name is None else name
230             raise ExtractorError('Could not find XML element\'s text %s' % name)
231         else:
232             return None
233     return n.text
234
235
236 def xpath_attr(node, xpath, key, name=None, fatal=False, default=NO_DEFAULT):
237     n = find_xpath_attr(node, xpath, key)
238     if n is None:
239         if default is not NO_DEFAULT:
240             return default
241         elif fatal:
242             name = '%s[@%s]' % (xpath, key) if name is None else name
243             raise ExtractorError('Could not find XML attribute %s' % name)
244         else:
245             return None
246     return n.attrib[key]
247
248
249 def get_element_by_id(id, html):
250     """Return the content of the tag with the specified ID in the passed HTML document"""
251     return get_element_by_attribute('id', id, html)
252
253
254 def get_element_by_attribute(attribute, value, html):
255     """Return the content of the tag with the specified attribute in the passed HTML document"""
256
257     m = re.search(r'''(?xs)
258         <([a-zA-Z0-9:._-]+)
259          (?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]+|="[^"]+"|='[^']+'))*?
260          \s+%s=['"]?%s['"]?
261          (?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]+|="[^"]+"|='[^']+'))*?
262         \s*>
263         (?P<content>.*?)
264         </\1>
265     ''' % (re.escape(attribute), re.escape(value)), html)
266
267     if not m:
268         return None
269     res = m.group('content')
270
271     if res.startswith('"') or res.startswith("'"):
272         res = res[1:-1]
273
274     return unescapeHTML(res)
275
276 class HTMLAttributeParser(compat_HTMLParser):
277     """Trivial HTML parser to gather the attributes for a single element"""
278     def __init__(self):
279         self.attrs = { }
280         compat_HTMLParser.__init__(self)
281
282     def handle_starttag(self, tag, attrs):
283         self.attrs = dict(attrs)
284
285 def extract_attributes(html_element):
286     """Given a string for an HTML element such as
287     <el
288          a="foo" B="bar" c="&98;az" d=boz
289          empty= noval entity="&amp;"
290          sq='"' dq="'"
291     >
292     Decode and return a dictionary of attributes.
293     {
294         'a': 'foo', 'b': 'bar', c: 'baz', d: 'boz',
295         'empty': '', 'noval': None, 'entity': '&',
296         'sq': '"', 'dq': '\''
297     }.
298     NB HTMLParser is stricter in Python 2.6 & 3.2 than in later versions,
299     but the cases in the unit test will work for all of 2.6, 2.7, 3.2-3.5.
300     """
301     parser = HTMLAttributeParser()
302     parser.feed(html_element)
303     parser.close()
304     return parser.attrs
305
306 def clean_html(html):
307     """Clean an HTML snippet into a readable string"""
308
309     if html is None:  # Convenience for sanitizing descriptions etc.
310         return html
311
312     # Newline vs <br />
313     html = html.replace('\n', ' ')
314     html = re.sub(r'\s*<\s*br\s*/?\s*>\s*', '\n', html)
315     html = re.sub(r'<\s*/\s*p\s*>\s*<\s*p[^>]*>', '\n', html)
316     # Strip html tags
317     html = re.sub('<.*?>', '', html)
318     # Replace html entities
319     html = unescapeHTML(html)
320     return html.strip()
321
322
323 def sanitize_open(filename, open_mode):
324     """Try to open the given filename, and slightly tweak it if this fails.
325
326     Attempts to open the given filename. If this fails, it tries to change
327     the filename slightly, step by step, until it's either able to open it
328     or it fails and raises a final exception, like the standard open()
329     function.
330
331     It returns the tuple (stream, definitive_file_name).
332     """
333     try:
334         if filename == '-':
335             if sys.platform == 'win32':
336                 import msvcrt
337                 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
338             return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename)
339         stream = open(encodeFilename(filename), open_mode)
340         return (stream, filename)
341     except (IOError, OSError) as err:
342         if err.errno in (errno.EACCES,):
343             raise
344
345         # In case of error, try to remove win32 forbidden chars
346         alt_filename = sanitize_path(filename)
347         if alt_filename == filename:
348             raise
349         else:
350             # An exception here should be caught in the caller
351             stream = open(encodeFilename(alt_filename), open_mode)
352             return (stream, alt_filename)
353
354
355 def timeconvert(timestr):
356     """Convert RFC 2822 defined time string into system timestamp"""
357     timestamp = None
358     timetuple = email.utils.parsedate_tz(timestr)
359     if timetuple is not None:
360         timestamp = email.utils.mktime_tz(timetuple)
361     return timestamp
362
363
364 def sanitize_filename(s, restricted=False, is_id=False):
365     """Sanitizes a string so it could be used as part of a filename.
366     If restricted is set, use a stricter subset of allowed characters.
367     Set is_id if this is not an arbitrary string, but an ID that should be kept if possible
368     """
369     def replace_insane(char):
370         if char == '?' or ord(char) < 32 or ord(char) == 127:
371             return ''
372         elif char == '"':
373             return '' if restricted else '\''
374         elif char == ':':
375             return '_-' if restricted else ' -'
376         elif char in '\\/|*<>':
377             return '_'
378         if restricted and (char in '!&\'()[]{}$;`^,#' or char.isspace()):
379             return '_'
380         if restricted and ord(char) > 127:
381             return '_'
382         return char
383
384     # Handle timestamps
385     s = re.sub(r'[0-9]+(?::[0-9]+)+', lambda m: m.group(0).replace(':', '_'), s)
386     result = ''.join(map(replace_insane, s))
387     if not is_id:
388         while '__' in result:
389             result = result.replace('__', '_')
390         result = result.strip('_')
391         # Common case of "Foreign band name - English song title"
392         if restricted and result.startswith('-_'):
393             result = result[2:]
394         if result.startswith('-'):
395             result = '_' + result[len('-'):]
396         result = result.lstrip('.')
397         if not result:
398             result = '_'
399     return result
400
401
402 def sanitize_path(s):
403     """Sanitizes and normalizes path on Windows"""
404     if sys.platform != 'win32':
405         return s
406     drive_or_unc, _ = os.path.splitdrive(s)
407     if sys.version_info < (2, 7) and not drive_or_unc:
408         drive_or_unc, _ = os.path.splitunc(s)
409     norm_path = os.path.normpath(remove_start(s, drive_or_unc)).split(os.path.sep)
410     if drive_or_unc:
411         norm_path.pop(0)
412     sanitized_path = [
413         path_part if path_part in ['.', '..'] else re.sub('(?:[/<>:"\\|\\\\?\\*]|[\s.]$)', '#', path_part)
414         for path_part in norm_path]
415     if drive_or_unc:
416         sanitized_path.insert(0, drive_or_unc + os.path.sep)
417     return os.path.join(*sanitized_path)
418
419
420 # Prepend protocol-less URLs with `http:` scheme in order to mitigate the number of
421 # unwanted failures due to missing protocol
422 def sanitized_Request(url, *args, **kwargs):
423     return compat_urllib_request.Request(
424         'http:%s' % url if url.startswith('//') else url, *args, **kwargs)
425
426
427 def orderedSet(iterable):
428     """ Remove all duplicates from the input iterable """
429     res = []
430     for el in iterable:
431         if el not in res:
432             res.append(el)
433     return res
434
435
436 def _htmlentity_transform(entity):
437     """Transforms an HTML entity to a character."""
438     # Known non-numeric HTML entity
439     if entity in compat_html_entities.name2codepoint:
440         return compat_chr(compat_html_entities.name2codepoint[entity])
441
442     mobj = re.match(r'#(x[0-9a-fA-F]+|[0-9]+)', entity)
443     if mobj is not None:
444         numstr = mobj.group(1)
445         if numstr.startswith('x'):
446             base = 16
447             numstr = '0%s' % numstr
448         else:
449             base = 10
450         # See https://github.com/rg3/youtube-dl/issues/7518
451         try:
452             return compat_chr(int(numstr, base))
453         except ValueError:
454             pass
455
456     # Unknown entity in name, return its literal representation
457     return '&%s;' % entity
458
459
460 def unescapeHTML(s):
461     if s is None:
462         return None
463     assert type(s) == compat_str
464
465     return re.sub(
466         r'&([^;]+);', lambda m: _htmlentity_transform(m.group(1)), s)
467
468
469 def get_subprocess_encoding():
470     if sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
471         # For subprocess calls, encode with locale encoding
472         # Refer to http://stackoverflow.com/a/9951851/35070
473         encoding = preferredencoding()
474     else:
475         encoding = sys.getfilesystemencoding()
476     if encoding is None:
477         encoding = 'utf-8'
478     return encoding
479
480
481 def encodeFilename(s, for_subprocess=False):
482     """
483     @param s The name of the file
484     """
485
486     assert type(s) == compat_str
487
488     # Python 3 has a Unicode API
489     if sys.version_info >= (3, 0):
490         return s
491
492     # Pass '' directly to use Unicode APIs on Windows 2000 and up
493     # (Detecting Windows NT 4 is tricky because 'major >= 4' would
494     # match Windows 9x series as well. Besides, NT 4 is obsolete.)
495     if not for_subprocess and sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
496         return s
497
498     # Jython assumes filenames are Unicode strings though reported as Python 2.x compatible
499     if sys.platform.startswith('java'):
500         return s
501
502     return s.encode(get_subprocess_encoding(), 'ignore')
503
504
505 def decodeFilename(b, for_subprocess=False):
506
507     if sys.version_info >= (3, 0):
508         return b
509
510     if not isinstance(b, bytes):
511         return b
512
513     return b.decode(get_subprocess_encoding(), 'ignore')
514
515
516 def encodeArgument(s):
517     if not isinstance(s, compat_str):
518         # Legacy code that uses byte strings
519         # Uncomment the following line after fixing all post processors
520         # assert False, 'Internal error: %r should be of type %r, is %r' % (s, compat_str, type(s))
521         s = s.decode('ascii')
522     return encodeFilename(s, True)
523
524
525 def decodeArgument(b):
526     return decodeFilename(b, True)
527
528
529 def decodeOption(optval):
530     if optval is None:
531         return optval
532     if isinstance(optval, bytes):
533         optval = optval.decode(preferredencoding())
534
535     assert isinstance(optval, compat_str)
536     return optval
537
538
539 def formatSeconds(secs):
540     if secs > 3600:
541         return '%d:%02d:%02d' % (secs // 3600, (secs % 3600) // 60, secs % 60)
542     elif secs > 60:
543         return '%d:%02d' % (secs // 60, secs % 60)
544     else:
545         return '%d' % secs
546
547
548 def make_HTTPS_handler(params, **kwargs):
549     opts_no_check_certificate = params.get('nocheckcertificate', False)
550     if hasattr(ssl, 'create_default_context'):  # Python >= 3.4 or 2.7.9
551         context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
552         if opts_no_check_certificate:
553             context.check_hostname = False
554             context.verify_mode = ssl.CERT_NONE
555         try:
556             return YoutubeDLHTTPSHandler(params, context=context, **kwargs)
557         except TypeError:
558             # Python 2.7.8
559             # (create_default_context present but HTTPSHandler has no context=)
560             pass
561
562     if sys.version_info < (3, 2):
563         return YoutubeDLHTTPSHandler(params, **kwargs)
564     else:  # Python < 3.4
565         context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
566         context.verify_mode = (ssl.CERT_NONE
567                                if opts_no_check_certificate
568                                else ssl.CERT_REQUIRED)
569         context.set_default_verify_paths()
570         return YoutubeDLHTTPSHandler(params, context=context, **kwargs)
571
572
573 def bug_reports_message():
574     if ytdl_is_updateable():
575         update_cmd = 'type  youtube-dl -U  to update'
576     else:
577         update_cmd = 'see  https://yt-dl.org/update  on how to update'
578     msg = '; please report this issue on https://yt-dl.org/bug .'
579     msg += ' Make sure you are using the latest version; %s.' % update_cmd
580     msg += ' Be sure to call youtube-dl with the --verbose flag and include its complete output.'
581     return msg
582
583
584 class ExtractorError(Exception):
585     """Error during info extraction."""
586
587     def __init__(self, msg, tb=None, expected=False, cause=None, video_id=None):
588         """ tb, if given, is the original traceback (so that it can be printed out).
589         If expected is set, this is a normal error message and most likely not a bug in youtube-dl.
590         """
591
592         if sys.exc_info()[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError):
593             expected = True
594         if video_id is not None:
595             msg = video_id + ': ' + msg
596         if cause:
597             msg += ' (caused by %r)' % cause
598         if not expected:
599             msg += bug_reports_message()
600         super(ExtractorError, self).__init__(msg)
601
602         self.traceback = tb
603         self.exc_info = sys.exc_info()  # preserve original exception
604         self.cause = cause
605         self.video_id = video_id
606
607     def format_traceback(self):
608         if self.traceback is None:
609             return None
610         return ''.join(traceback.format_tb(self.traceback))
611
612
613 class UnsupportedError(ExtractorError):
614     def __init__(self, url):
615         super(UnsupportedError, self).__init__(
616             'Unsupported URL: %s' % url, expected=True)
617         self.url = url
618
619
620 class RegexNotFoundError(ExtractorError):
621     """Error when a regex didn't match"""
622     pass
623
624
625 class DownloadError(Exception):
626     """Download Error exception.
627
628     This exception may be thrown by FileDownloader objects if they are not
629     configured to continue on errors. They will contain the appropriate
630     error message.
631     """
632
633     def __init__(self, msg, exc_info=None):
634         """ exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """
635         super(DownloadError, self).__init__(msg)
636         self.exc_info = exc_info
637
638
639 class SameFileError(Exception):
640     """Same File exception.
641
642     This exception will be thrown by FileDownloader objects if they detect
643     multiple files would have to be downloaded to the same file on disk.
644     """
645     pass
646
647
648 class PostProcessingError(Exception):
649     """Post Processing exception.
650
651     This exception may be raised by PostProcessor's .run() method to
652     indicate an error in the postprocessing task.
653     """
654
655     def __init__(self, msg):
656         self.msg = msg
657
658
659 class MaxDownloadsReached(Exception):
660     """ --max-downloads limit has been reached. """
661     pass
662
663
664 class UnavailableVideoError(Exception):
665     """Unavailable Format exception.
666
667     This exception will be thrown when a video is requested
668     in a format that is not available for that video.
669     """
670     pass
671
672
673 class ContentTooShortError(Exception):
674     """Content Too Short exception.
675
676     This exception may be raised by FileDownloader objects when a file they
677     download is too small for what the server announced first, indicating
678     the connection was probably interrupted.
679     """
680
681     def __init__(self, downloaded, expected):
682         # Both in bytes
683         self.downloaded = downloaded
684         self.expected = expected
685
686
687 def _create_http_connection(ydl_handler, http_class, is_https, *args, **kwargs):
688     # Working around python 2 bug (see http://bugs.python.org/issue17849) by limiting
689     # expected HTTP responses to meet HTTP/1.0 or later (see also
690     # https://github.com/rg3/youtube-dl/issues/6727)
691     if sys.version_info < (3, 0):
692         kwargs[b'strict'] = True
693     hc = http_class(*args, **kwargs)
694     source_address = ydl_handler._params.get('source_address')
695     if source_address is not None:
696         sa = (source_address, 0)
697         if hasattr(hc, 'source_address'):  # Python 2.7+
698             hc.source_address = sa
699         else:  # Python 2.6
700             def _hc_connect(self, *args, **kwargs):
701                 sock = compat_socket_create_connection(
702                     (self.host, self.port), self.timeout, sa)
703                 if is_https:
704                     self.sock = ssl.wrap_socket(
705                         sock, self.key_file, self.cert_file,
706                         ssl_version=ssl.PROTOCOL_TLSv1)
707                 else:
708                     self.sock = sock
709             hc.connect = functools.partial(_hc_connect, hc)
710
711     return hc
712
713
714 def handle_youtubedl_headers(headers):
715     filtered_headers = headers
716
717     if 'Youtubedl-no-compression' in filtered_headers:
718         filtered_headers = dict((k, v) for k, v in filtered_headers.items() if k.lower() != 'accept-encoding')
719         del filtered_headers['Youtubedl-no-compression']
720
721     return filtered_headers
722
723
724 class YoutubeDLHandler(compat_urllib_request.HTTPHandler):
725     """Handler for HTTP requests and responses.
726
727     This class, when installed with an OpenerDirector, automatically adds
728     the standard headers to every HTTP request and handles gzipped and
729     deflated responses from web servers. If compression is to be avoided in
730     a particular request, the original request in the program code only has
731     to include the HTTP header "Youtubedl-no-compression", which will be
732     removed before making the real request.
733
734     Part of this code was copied from:
735
736     http://techknack.net/python-urllib2-handlers/
737
738     Andrew Rowls, the author of that code, agreed to release it to the
739     public domain.
740     """
741
742     def __init__(self, params, *args, **kwargs):
743         compat_urllib_request.HTTPHandler.__init__(self, *args, **kwargs)
744         self._params = params
745
746     def http_open(self, req):
747         return self.do_open(functools.partial(
748             _create_http_connection, self, compat_http_client.HTTPConnection, False),
749             req)
750
751     @staticmethod
752     def deflate(data):
753         try:
754             return zlib.decompress(data, -zlib.MAX_WBITS)
755         except zlib.error:
756             return zlib.decompress(data)
757
758     @staticmethod
759     def addinfourl_wrapper(stream, headers, url, code):
760         if hasattr(compat_urllib_request.addinfourl, 'getcode'):
761             return compat_urllib_request.addinfourl(stream, headers, url, code)
762         ret = compat_urllib_request.addinfourl(stream, headers, url)
763         ret.code = code
764         return ret
765
766     def http_request(self, req):
767         # According to RFC 3986, URLs can not contain non-ASCII characters, however this is not
768         # always respected by websites, some tend to give out URLs with non percent-encoded
769         # non-ASCII characters (see telemb.py, ard.py [#3412])
770         # urllib chokes on URLs with non-ASCII characters (see http://bugs.python.org/issue3991)
771         # To work around aforementioned issue we will replace request's original URL with
772         # percent-encoded one
773         # Since redirects are also affected (e.g. http://www.southpark.de/alle-episoden/s18e09)
774         # the code of this workaround has been moved here from YoutubeDL.urlopen()
775         url = req.get_full_url()
776         url_escaped = escape_url(url)
777
778         # Substitute URL if any change after escaping
779         if url != url_escaped:
780             req_type = HEADRequest if req.get_method() == 'HEAD' else compat_urllib_request.Request
781             new_req = req_type(
782                 url_escaped, data=req.data, headers=req.headers,
783                 origin_req_host=req.origin_req_host, unverifiable=req.unverifiable)
784             new_req.timeout = req.timeout
785             req = new_req
786
787         for h, v in std_headers.items():
788             # Capitalize is needed because of Python bug 2275: http://bugs.python.org/issue2275
789             # The dict keys are capitalized because of this bug by urllib
790             if h.capitalize() not in req.headers:
791                 req.add_header(h, v)
792
793         req.headers = handle_youtubedl_headers(req.headers)
794
795         if sys.version_info < (2, 7) and '#' in req.get_full_url():
796             # Python 2.6 is brain-dead when it comes to fragments
797             req._Request__original = req._Request__original.partition('#')[0]
798             req._Request__r_type = req._Request__r_type.partition('#')[0]
799
800         return req
801
802     def http_response(self, req, resp):
803         old_resp = resp
804         # gzip
805         if resp.headers.get('Content-encoding', '') == 'gzip':
806             content = resp.read()
807             gz = gzip.GzipFile(fileobj=io.BytesIO(content), mode='rb')
808             try:
809                 uncompressed = io.BytesIO(gz.read())
810             except IOError as original_ioerror:
811                 # There may be junk add the end of the file
812                 # See http://stackoverflow.com/q/4928560/35070 for details
813                 for i in range(1, 1024):
814                     try:
815                         gz = gzip.GzipFile(fileobj=io.BytesIO(content[:-i]), mode='rb')
816                         uncompressed = io.BytesIO(gz.read())
817                     except IOError:
818                         continue
819                     break
820                 else:
821                     raise original_ioerror
822             resp = self.addinfourl_wrapper(uncompressed, old_resp.headers, old_resp.url, old_resp.code)
823             resp.msg = old_resp.msg
824             del resp.headers['Content-encoding']
825         # deflate
826         if resp.headers.get('Content-encoding', '') == 'deflate':
827             gz = io.BytesIO(self.deflate(resp.read()))
828             resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code)
829             resp.msg = old_resp.msg
830             del resp.headers['Content-encoding']
831         # Percent-encode redirect URL of Location HTTP header to satisfy RFC 3986 (see
832         # https://github.com/rg3/youtube-dl/issues/6457).
833         if 300 <= resp.code < 400:
834             location = resp.headers.get('Location')
835             if location:
836                 # As of RFC 2616 default charset is iso-8859-1 that is respected by python 3
837                 if sys.version_info >= (3, 0):
838                     location = location.encode('iso-8859-1').decode('utf-8')
839                 location_escaped = escape_url(location)
840                 if location != location_escaped:
841                     del resp.headers['Location']
842                     resp.headers['Location'] = location_escaped
843         return resp
844
845     https_request = http_request
846     https_response = http_response
847
848
849 class YoutubeDLHTTPSHandler(compat_urllib_request.HTTPSHandler):
850     def __init__(self, params, https_conn_class=None, *args, **kwargs):
851         compat_urllib_request.HTTPSHandler.__init__(self, *args, **kwargs)
852         self._https_conn_class = https_conn_class or compat_http_client.HTTPSConnection
853         self._params = params
854
855     def https_open(self, req):
856         kwargs = {}
857         if hasattr(self, '_context'):  # python > 2.6
858             kwargs['context'] = self._context
859         if hasattr(self, '_check_hostname'):  # python 3.x
860             kwargs['check_hostname'] = self._check_hostname
861         return self.do_open(functools.partial(
862             _create_http_connection, self, self._https_conn_class, True),
863             req, **kwargs)
864
865
866 class YoutubeDLCookieProcessor(compat_urllib_request.HTTPCookieProcessor):
867     def __init__(self, cookiejar=None):
868         compat_urllib_request.HTTPCookieProcessor.__init__(self, cookiejar)
869
870     def http_response(self, request, response):
871         # Python 2 will choke on next HTTP request in row if there are non-ASCII
872         # characters in Set-Cookie HTTP header of last response (see
873         # https://github.com/rg3/youtube-dl/issues/6769).
874         # In order to at least prevent crashing we will percent encode Set-Cookie
875         # header before HTTPCookieProcessor starts processing it.
876         # if sys.version_info < (3, 0) and response.headers:
877         #     for set_cookie_header in ('Set-Cookie', 'Set-Cookie2'):
878         #         set_cookie = response.headers.get(set_cookie_header)
879         #         if set_cookie:
880         #             set_cookie_escaped = compat_urllib_parse.quote(set_cookie, b"%/;:@&=+$,!~*'()?#[] ")
881         #             if set_cookie != set_cookie_escaped:
882         #                 del response.headers[set_cookie_header]
883         #                 response.headers[set_cookie_header] = set_cookie_escaped
884         return compat_urllib_request.HTTPCookieProcessor.http_response(self, request, response)
885
886     https_request = compat_urllib_request.HTTPCookieProcessor.http_request
887     https_response = http_response
888
889
890 def parse_iso8601(date_str, delimiter='T', timezone=None):
891     """ Return a UNIX timestamp from the given date """
892
893     if date_str is None:
894         return None
895
896     date_str = re.sub(r'\.[0-9]+', '', date_str)
897
898     if timezone is None:
899         m = re.search(
900             r'(?:Z$| ?(?P<sign>\+|-)(?P<hours>[0-9]{2}):?(?P<minutes>[0-9]{2})$)',
901             date_str)
902         if not m:
903             timezone = datetime.timedelta()
904         else:
905             date_str = date_str[:-len(m.group(0))]
906             if not m.group('sign'):
907                 timezone = datetime.timedelta()
908             else:
909                 sign = 1 if m.group('sign') == '+' else -1
910                 timezone = datetime.timedelta(
911                     hours=sign * int(m.group('hours')),
912                     minutes=sign * int(m.group('minutes')))
913     try:
914         date_format = '%Y-%m-%d{0}%H:%M:%S'.format(delimiter)
915         dt = datetime.datetime.strptime(date_str, date_format) - timezone
916         return calendar.timegm(dt.timetuple())
917     except ValueError:
918         pass
919
920
921 def unified_strdate(date_str, day_first=True):
922     """Return a string with the date in the format YYYYMMDD"""
923
924     if date_str is None:
925         return None
926     upload_date = None
927     # Replace commas
928     date_str = date_str.replace(',', ' ')
929     # %z (UTC offset) is only supported in python>=3.2
930     if not re.match(r'^[0-9]{1,2}-[0-9]{1,2}-[0-9]{4}$', date_str):
931         date_str = re.sub(r' ?(\+|-)[0-9]{2}:?[0-9]{2}$', '', date_str)
932     # Remove AM/PM + timezone
933     date_str = re.sub(r'(?i)\s*(?:AM|PM)(?:\s+[A-Z]+)?', '', date_str)
934
935     format_expressions = [
936         '%d %B %Y',
937         '%d %b %Y',
938         '%B %d %Y',
939         '%b %d %Y',
940         '%b %dst %Y %I:%M',
941         '%b %dnd %Y %I:%M',
942         '%b %dth %Y %I:%M',
943         '%Y %m %d',
944         '%Y-%m-%d',
945         '%Y/%m/%d',
946         '%Y/%m/%d %H:%M:%S',
947         '%Y-%m-%d %H:%M:%S',
948         '%Y-%m-%d %H:%M:%S.%f',
949         '%d.%m.%Y %H:%M',
950         '%d.%m.%Y %H.%M',
951         '%Y-%m-%dT%H:%M:%SZ',
952         '%Y-%m-%dT%H:%M:%S.%fZ',
953         '%Y-%m-%dT%H:%M:%S.%f0Z',
954         '%Y-%m-%dT%H:%M:%S',
955         '%Y-%m-%dT%H:%M:%S.%f',
956         '%Y-%m-%dT%H:%M',
957     ]
958     if day_first:
959         format_expressions.extend([
960             '%d-%m-%Y',
961             '%d.%m.%Y',
962             '%d/%m/%Y',
963             '%d/%m/%y',
964             '%d/%m/%Y %H:%M:%S',
965         ])
966     else:
967         format_expressions.extend([
968             '%m-%d-%Y',
969             '%m.%d.%Y',
970             '%m/%d/%Y',
971             '%m/%d/%y',
972             '%m/%d/%Y %H:%M:%S',
973         ])
974     for expression in format_expressions:
975         try:
976             upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
977         except ValueError:
978             pass
979     if upload_date is None:
980         timetuple = email.utils.parsedate_tz(date_str)
981         if timetuple:
982             upload_date = datetime.datetime(*timetuple[:6]).strftime('%Y%m%d')
983     if upload_date is not None:
984         return compat_str(upload_date)
985
986
987 def determine_ext(url, default_ext='unknown_video'):
988     if url is None:
989         return default_ext
990     guess = url.partition('?')[0].rpartition('.')[2]
991     if re.match(r'^[A-Za-z0-9]+$', guess):
992         return guess
993     # Try extract ext from URLs like http://example.com/foo/bar.mp4/?download
994     elif guess.rstrip('/') in KNOWN_EXTENSIONS:
995         return guess.rstrip('/')
996     else:
997         return default_ext
998
999
1000 def subtitles_filename(filename, sub_lang, sub_format):
1001     return filename.rsplit('.', 1)[0] + '.' + sub_lang + '.' + sub_format
1002
1003
1004 def date_from_str(date_str):
1005     """
1006     Return a datetime object from a string in the format YYYYMMDD or
1007     (now|today)[+-][0-9](day|week|month|year)(s)?"""
1008     today = datetime.date.today()
1009     if date_str in ('now', 'today'):
1010         return today
1011     if date_str == 'yesterday':
1012         return today - datetime.timedelta(days=1)
1013     match = re.match('(now|today)(?P<sign>[+-])(?P<time>\d+)(?P<unit>day|week|month|year)(s)?', date_str)
1014     if match is not None:
1015         sign = match.group('sign')
1016         time = int(match.group('time'))
1017         if sign == '-':
1018             time = -time
1019         unit = match.group('unit')
1020         # A bad approximation?
1021         if unit == 'month':
1022             unit = 'day'
1023             time *= 30
1024         elif unit == 'year':
1025             unit = 'day'
1026             time *= 365
1027         unit += 's'
1028         delta = datetime.timedelta(**{unit: time})
1029         return today + delta
1030     return datetime.datetime.strptime(date_str, '%Y%m%d').date()
1031
1032
1033 def hyphenate_date(date_str):
1034     """
1035     Convert a date in 'YYYYMMDD' format to 'YYYY-MM-DD' format"""
1036     match = re.match(r'^(\d\d\d\d)(\d\d)(\d\d)$', date_str)
1037     if match is not None:
1038         return '-'.join(match.groups())
1039     else:
1040         return date_str
1041
1042
1043 class DateRange(object):
1044     """Represents a time interval between two dates"""
1045
1046     def __init__(self, start=None, end=None):
1047         """start and end must be strings in the format accepted by date"""
1048         if start is not None:
1049             self.start = date_from_str(start)
1050         else:
1051             self.start = datetime.datetime.min.date()
1052         if end is not None:
1053             self.end = date_from_str(end)
1054         else:
1055             self.end = datetime.datetime.max.date()
1056         if self.start > self.end:
1057             raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
1058
1059     @classmethod
1060     def day(cls, day):
1061         """Returns a range that only contains the given day"""
1062         return cls(day, day)
1063
1064     def __contains__(self, date):
1065         """Check if the date is in the range"""
1066         if not isinstance(date, datetime.date):
1067             date = date_from_str(date)
1068         return self.start <= date <= self.end
1069
1070     def __str__(self):
1071         return '%s - %s' % (self.start.isoformat(), self.end.isoformat())
1072
1073
1074 def platform_name():
1075     """ Returns the platform name as a compat_str """
1076     res = platform.platform()
1077     if isinstance(res, bytes):
1078         res = res.decode(preferredencoding())
1079
1080     assert isinstance(res, compat_str)
1081     return res
1082
1083
1084 def _windows_write_string(s, out):
1085     """ Returns True if the string was written using special methods,
1086     False if it has yet to be written out."""
1087     # Adapted from http://stackoverflow.com/a/3259271/35070
1088
1089     import ctypes
1090     import ctypes.wintypes
1091
1092     WIN_OUTPUT_IDS = {
1093         1: -11,
1094         2: -12,
1095     }
1096
1097     try:
1098         fileno = out.fileno()
1099     except AttributeError:
1100         # If the output stream doesn't have a fileno, it's virtual
1101         return False
1102     except io.UnsupportedOperation:
1103         # Some strange Windows pseudo files?
1104         return False
1105     if fileno not in WIN_OUTPUT_IDS:
1106         return False
1107
1108     GetStdHandle = ctypes.WINFUNCTYPE(
1109         ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD)(
1110         (b'GetStdHandle', ctypes.windll.kernel32))
1111     h = GetStdHandle(WIN_OUTPUT_IDS[fileno])
1112
1113     WriteConsoleW = ctypes.WINFUNCTYPE(
1114         ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE, ctypes.wintypes.LPWSTR,
1115         ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD),
1116         ctypes.wintypes.LPVOID)((b'WriteConsoleW', ctypes.windll.kernel32))
1117     written = ctypes.wintypes.DWORD(0)
1118
1119     GetFileType = ctypes.WINFUNCTYPE(ctypes.wintypes.DWORD, ctypes.wintypes.DWORD)((b'GetFileType', ctypes.windll.kernel32))
1120     FILE_TYPE_CHAR = 0x0002
1121     FILE_TYPE_REMOTE = 0x8000
1122     GetConsoleMode = ctypes.WINFUNCTYPE(
1123         ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE,
1124         ctypes.POINTER(ctypes.wintypes.DWORD))(
1125         (b'GetConsoleMode', ctypes.windll.kernel32))
1126     INVALID_HANDLE_VALUE = ctypes.wintypes.DWORD(-1).value
1127
1128     def not_a_console(handle):
1129         if handle == INVALID_HANDLE_VALUE or handle is None:
1130             return True
1131         return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR or
1132                 GetConsoleMode(handle, ctypes.byref(ctypes.wintypes.DWORD())) == 0)
1133
1134     if not_a_console(h):
1135         return False
1136
1137     def next_nonbmp_pos(s):
1138         try:
1139             return next(i for i, c in enumerate(s) if ord(c) > 0xffff)
1140         except StopIteration:
1141             return len(s)
1142
1143     while s:
1144         count = min(next_nonbmp_pos(s), 1024)
1145
1146         ret = WriteConsoleW(
1147             h, s, count if count else 2, ctypes.byref(written), None)
1148         if ret == 0:
1149             raise OSError('Failed to write string')
1150         if not count:  # We just wrote a non-BMP character
1151             assert written.value == 2
1152             s = s[1:]
1153         else:
1154             assert written.value > 0
1155             s = s[written.value:]
1156     return True
1157
1158
1159 def write_string(s, out=None, encoding=None):
1160     if out is None:
1161         out = sys.stderr
1162     assert type(s) == compat_str
1163
1164     if sys.platform == 'win32' and encoding is None and hasattr(out, 'fileno'):
1165         if _windows_write_string(s, out):
1166             return
1167
1168     if ('b' in getattr(out, 'mode', '') or
1169             sys.version_info[0] < 3):  # Python 2 lies about mode of sys.stderr
1170         byt = s.encode(encoding or preferredencoding(), 'ignore')
1171         out.write(byt)
1172     elif hasattr(out, 'buffer'):
1173         enc = encoding or getattr(out, 'encoding', None) or preferredencoding()
1174         byt = s.encode(enc, 'ignore')
1175         out.buffer.write(byt)
1176     else:
1177         out.write(s)
1178     out.flush()
1179
1180
1181 def bytes_to_intlist(bs):
1182     if not bs:
1183         return []
1184     if isinstance(bs[0], int):  # Python 3
1185         return list(bs)
1186     else:
1187         return [ord(c) for c in bs]
1188
1189
1190 def intlist_to_bytes(xs):
1191     if not xs:
1192         return b''
1193     return struct_pack('%dB' % len(xs), *xs)
1194
1195
1196 # Cross-platform file locking
1197 if sys.platform == 'win32':
1198     import ctypes.wintypes
1199     import msvcrt
1200
1201     class OVERLAPPED(ctypes.Structure):
1202         _fields_ = [
1203             ('Internal', ctypes.wintypes.LPVOID),
1204             ('InternalHigh', ctypes.wintypes.LPVOID),
1205             ('Offset', ctypes.wintypes.DWORD),
1206             ('OffsetHigh', ctypes.wintypes.DWORD),
1207             ('hEvent', ctypes.wintypes.HANDLE),
1208         ]
1209
1210     kernel32 = ctypes.windll.kernel32
1211     LockFileEx = kernel32.LockFileEx
1212     LockFileEx.argtypes = [
1213         ctypes.wintypes.HANDLE,     # hFile
1214         ctypes.wintypes.DWORD,      # dwFlags
1215         ctypes.wintypes.DWORD,      # dwReserved
1216         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
1217         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
1218         ctypes.POINTER(OVERLAPPED)  # Overlapped
1219     ]
1220     LockFileEx.restype = ctypes.wintypes.BOOL
1221     UnlockFileEx = kernel32.UnlockFileEx
1222     UnlockFileEx.argtypes = [
1223         ctypes.wintypes.HANDLE,     # hFile
1224         ctypes.wintypes.DWORD,      # dwReserved
1225         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
1226         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
1227         ctypes.POINTER(OVERLAPPED)  # Overlapped
1228     ]
1229     UnlockFileEx.restype = ctypes.wintypes.BOOL
1230     whole_low = 0xffffffff
1231     whole_high = 0x7fffffff
1232
1233     def _lock_file(f, exclusive):
1234         overlapped = OVERLAPPED()
1235         overlapped.Offset = 0
1236         overlapped.OffsetHigh = 0
1237         overlapped.hEvent = 0
1238         f._lock_file_overlapped_p = ctypes.pointer(overlapped)
1239         handle = msvcrt.get_osfhandle(f.fileno())
1240         if not LockFileEx(handle, 0x2 if exclusive else 0x0, 0,
1241                           whole_low, whole_high, f._lock_file_overlapped_p):
1242             raise OSError('Locking file failed: %r' % ctypes.FormatError())
1243
1244     def _unlock_file(f):
1245         assert f._lock_file_overlapped_p
1246         handle = msvcrt.get_osfhandle(f.fileno())
1247         if not UnlockFileEx(handle, 0,
1248                             whole_low, whole_high, f._lock_file_overlapped_p):
1249             raise OSError('Unlocking file failed: %r' % ctypes.FormatError())
1250
1251 else:
1252     # Some platforms, such as Jython, is missing fcntl
1253     try:
1254         import fcntl
1255
1256         def _lock_file(f, exclusive):
1257             fcntl.flock(f, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
1258
1259         def _unlock_file(f):
1260             fcntl.flock(f, fcntl.LOCK_UN)
1261     except ImportError:
1262         UNSUPPORTED_MSG = 'file locking is not supported on this platform'
1263
1264         def _lock_file(f, exclusive):
1265             raise IOError(UNSUPPORTED_MSG)
1266
1267         def _unlock_file(f):
1268             raise IOError(UNSUPPORTED_MSG)
1269
1270
1271 class locked_file(object):
1272     def __init__(self, filename, mode, encoding=None):
1273         assert mode in ['r', 'a', 'w']
1274         self.f = io.open(filename, mode, encoding=encoding)
1275         self.mode = mode
1276
1277     def __enter__(self):
1278         exclusive = self.mode != 'r'
1279         try:
1280             _lock_file(self.f, exclusive)
1281         except IOError:
1282             self.f.close()
1283             raise
1284         return self
1285
1286     def __exit__(self, etype, value, traceback):
1287         try:
1288             _unlock_file(self.f)
1289         finally:
1290             self.f.close()
1291
1292     def __iter__(self):
1293         return iter(self.f)
1294
1295     def write(self, *args):
1296         return self.f.write(*args)
1297
1298     def read(self, *args):
1299         return self.f.read(*args)
1300
1301
1302 def get_filesystem_encoding():
1303     encoding = sys.getfilesystemencoding()
1304     return encoding if encoding is not None else 'utf-8'
1305
1306
1307 def shell_quote(args):
1308     quoted_args = []
1309     encoding = get_filesystem_encoding()
1310     for a in args:
1311         if isinstance(a, bytes):
1312             # We may get a filename encoded with 'encodeFilename'
1313             a = a.decode(encoding)
1314         quoted_args.append(pipes.quote(a))
1315     return ' '.join(quoted_args)
1316
1317
1318 def smuggle_url(url, data):
1319     """ Pass additional data in a URL for internal use. """
1320
1321     sdata = compat_urllib_parse.urlencode(
1322         {'__youtubedl_smuggle': json.dumps(data)})
1323     return url + '#' + sdata
1324
1325
1326 def unsmuggle_url(smug_url, default=None):
1327     if '#__youtubedl_smuggle' not in smug_url:
1328         return smug_url, default
1329     url, _, sdata = smug_url.rpartition('#')
1330     jsond = compat_parse_qs(sdata)['__youtubedl_smuggle'][0]
1331     data = json.loads(jsond)
1332     return url, data
1333
1334
1335 def format_bytes(bytes):
1336     if bytes is None:
1337         return 'N/A'
1338     if type(bytes) is str:
1339         bytes = float(bytes)
1340     if bytes == 0.0:
1341         exponent = 0
1342     else:
1343         exponent = int(math.log(bytes, 1024.0))
1344     suffix = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'][exponent]
1345     converted = float(bytes) / float(1024 ** exponent)
1346     return '%.2f%s' % (converted, suffix)
1347
1348
1349 def lookup_unit_table(unit_table, s):
1350     units_re = '|'.join(re.escape(u) for u in unit_table)
1351     m = re.match(
1352         r'(?P<num>[0-9]+(?:[,.][0-9]*)?)\s*(?P<unit>%s)' % units_re, s)
1353     if not m:
1354         return None
1355     num_str = m.group('num').replace(',', '.')
1356     mult = unit_table[m.group('unit')]
1357     return int(float(num_str) * mult)
1358
1359
1360 def parse_filesize(s):
1361     if s is None:
1362         return None
1363
1364     # The lower-case forms are of course incorrect and unofficial,
1365     # but we support those too
1366     _UNIT_TABLE = {
1367         'B': 1,
1368         'b': 1,
1369         'KiB': 1024,
1370         'KB': 1000,
1371         'kB': 1024,
1372         'Kb': 1000,
1373         'MiB': 1024 ** 2,
1374         'MB': 1000 ** 2,
1375         'mB': 1024 ** 2,
1376         'Mb': 1000 ** 2,
1377         'GiB': 1024 ** 3,
1378         'GB': 1000 ** 3,
1379         'gB': 1024 ** 3,
1380         'Gb': 1000 ** 3,
1381         'TiB': 1024 ** 4,
1382         'TB': 1000 ** 4,
1383         'tB': 1024 ** 4,
1384         'Tb': 1000 ** 4,
1385         'PiB': 1024 ** 5,
1386         'PB': 1000 ** 5,
1387         'pB': 1024 ** 5,
1388         'Pb': 1000 ** 5,
1389         'EiB': 1024 ** 6,
1390         'EB': 1000 ** 6,
1391         'eB': 1024 ** 6,
1392         'Eb': 1000 ** 6,
1393         'ZiB': 1024 ** 7,
1394         'ZB': 1000 ** 7,
1395         'zB': 1024 ** 7,
1396         'Zb': 1000 ** 7,
1397         'YiB': 1024 ** 8,
1398         'YB': 1000 ** 8,
1399         'yB': 1024 ** 8,
1400         'Yb': 1000 ** 8,
1401     }
1402
1403     return lookup_unit_table(_UNIT_TABLE, s)
1404
1405
1406 def parse_count(s):
1407     if s is None:
1408         return None
1409
1410     s = s.strip()
1411
1412     if re.match(r'^[\d,.]+$', s):
1413         return str_to_int(s)
1414
1415     _UNIT_TABLE = {
1416         'k': 1000,
1417         'K': 1000,
1418         'm': 1000 ** 2,
1419         'M': 1000 ** 2,
1420         'kk': 1000 ** 2,
1421         'KK': 1000 ** 2,
1422     }
1423
1424     return lookup_unit_table(_UNIT_TABLE, s)
1425
1426
1427 def month_by_name(name):
1428     """ Return the number of a month by (locale-independently) English name """
1429
1430     try:
1431         return ENGLISH_MONTH_NAMES.index(name) + 1
1432     except ValueError:
1433         return None
1434
1435
1436 def month_by_abbreviation(abbrev):
1437     """ Return the number of a month by (locale-independently) English
1438         abbreviations """
1439
1440     try:
1441         return [s[:3] for s in ENGLISH_MONTH_NAMES].index(abbrev) + 1
1442     except ValueError:
1443         return None
1444
1445
1446 def fix_xml_ampersands(xml_str):
1447     """Replace all the '&' by '&amp;' in XML"""
1448     return re.sub(
1449         r'&(?!amp;|lt;|gt;|apos;|quot;|#x[0-9a-fA-F]{,4};|#[0-9]{,4};)',
1450         '&amp;',
1451         xml_str)
1452
1453
1454 def setproctitle(title):
1455     assert isinstance(title, compat_str)
1456
1457     # ctypes in Jython is not complete
1458     # http://bugs.jython.org/issue2148
1459     if sys.platform.startswith('java'):
1460         return
1461
1462     try:
1463         libc = ctypes.cdll.LoadLibrary('libc.so.6')
1464     except OSError:
1465         return
1466     title_bytes = title.encode('utf-8')
1467     buf = ctypes.create_string_buffer(len(title_bytes))
1468     buf.value = title_bytes
1469     try:
1470         libc.prctl(15, buf, 0, 0, 0)
1471     except AttributeError:
1472         return  # Strange libc, just skip this
1473
1474
1475 def remove_start(s, start):
1476     if s.startswith(start):
1477         return s[len(start):]
1478     return s
1479
1480
1481 def remove_end(s, end):
1482     if s.endswith(end):
1483         return s[:-len(end)]
1484     return s
1485
1486
1487 def remove_quotes(s):
1488     if s is None or len(s) < 2:
1489         return s
1490     for quote in ('"', "'", ):
1491         if s[0] == quote and s[-1] == quote:
1492             return s[1:-1]
1493     return s
1494
1495
1496 def url_basename(url):
1497     path = compat_urlparse.urlparse(url).path
1498     return path.strip('/').split('/')[-1]
1499
1500
1501 class HEADRequest(compat_urllib_request.Request):
1502     def get_method(self):
1503         return 'HEAD'
1504
1505
1506 def int_or_none(v, scale=1, default=None, get_attr=None, invscale=1):
1507     if get_attr:
1508         if v is not None:
1509             v = getattr(v, get_attr, None)
1510     if v == '':
1511         v = None
1512     if v is None:
1513         return default
1514     try:
1515         return int(v) * invscale // scale
1516     except ValueError:
1517         return default
1518
1519
1520 def str_or_none(v, default=None):
1521     return default if v is None else compat_str(v)
1522
1523
1524 def str_to_int(int_str):
1525     """ A more relaxed version of int_or_none """
1526     if int_str is None:
1527         return None
1528     int_str = re.sub(r'[,\.\+]', '', int_str)
1529     return int(int_str)
1530
1531
1532 def float_or_none(v, scale=1, invscale=1, default=None):
1533     if v is None:
1534         return default
1535     try:
1536         return float(v) * invscale / scale
1537     except ValueError:
1538         return default
1539
1540
1541 def parse_duration(s):
1542     if not isinstance(s, compat_basestring):
1543         return None
1544
1545     s = s.strip()
1546
1547     m = re.match(
1548         r'''(?ix)(?:P?T)?
1549         (?:
1550             (?P<only_mins>[0-9.]+)\s*(?:mins?\.?|minutes?)\s*|
1551             (?P<only_hours>[0-9.]+)\s*(?:hours?)|
1552
1553             \s*(?P<hours_reversed>[0-9]+)\s*(?:[:h]|hours?)\s*(?P<mins_reversed>[0-9]+)\s*(?:[:m]|mins?\.?|minutes?)\s*|
1554             (?:
1555                 (?:
1556                     (?:(?P<days>[0-9]+)\s*(?:[:d]|days?)\s*)?
1557                     (?P<hours>[0-9]+)\s*(?:[:h]|hours?)\s*
1558                 )?
1559                 (?P<mins>[0-9]+)\s*(?:[:m]|mins?|minutes?)\s*
1560             )?
1561             (?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?\s*(?:s|secs?|seconds?)?
1562         )$''', s)
1563     if not m:
1564         return None
1565     res = 0
1566     if m.group('only_mins'):
1567         return float_or_none(m.group('only_mins'), invscale=60)
1568     if m.group('only_hours'):
1569         return float_or_none(m.group('only_hours'), invscale=60 * 60)
1570     if m.group('secs'):
1571         res += int(m.group('secs'))
1572     if m.group('mins_reversed'):
1573         res += int(m.group('mins_reversed')) * 60
1574     if m.group('mins'):
1575         res += int(m.group('mins')) * 60
1576     if m.group('hours'):
1577         res += int(m.group('hours')) * 60 * 60
1578     if m.group('hours_reversed'):
1579         res += int(m.group('hours_reversed')) * 60 * 60
1580     if m.group('days'):
1581         res += int(m.group('days')) * 24 * 60 * 60
1582     if m.group('ms'):
1583         res += float(m.group('ms'))
1584     return res
1585
1586
1587 def prepend_extension(filename, ext, expected_real_ext=None):
1588     name, real_ext = os.path.splitext(filename)
1589     return (
1590         '{0}.{1}{2}'.format(name, ext, real_ext)
1591         if not expected_real_ext or real_ext[1:] == expected_real_ext
1592         else '{0}.{1}'.format(filename, ext))
1593
1594
1595 def replace_extension(filename, ext, expected_real_ext=None):
1596     name, real_ext = os.path.splitext(filename)
1597     return '{0}.{1}'.format(
1598         name if not expected_real_ext or real_ext[1:] == expected_real_ext else filename,
1599         ext)
1600
1601
1602 def check_executable(exe, args=[]):
1603     """ Checks if the given binary is installed somewhere in PATH, and returns its name.
1604     args can be a list of arguments for a short output (like -version) """
1605     try:
1606         subprocess.Popen([exe] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
1607     except OSError:
1608         return False
1609     return exe
1610
1611
1612 def get_exe_version(exe, args=['--version'],
1613                     version_re=None, unrecognized='present'):
1614     """ Returns the version of the specified executable,
1615     or False if the executable is not present """
1616     try:
1617         out, _ = subprocess.Popen(
1618             [encodeArgument(exe)] + args,
1619             stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()
1620     except OSError:
1621         return False
1622     if isinstance(out, bytes):  # Python 2.x
1623         out = out.decode('ascii', 'ignore')
1624     return detect_exe_version(out, version_re, unrecognized)
1625
1626
1627 def detect_exe_version(output, version_re=None, unrecognized='present'):
1628     assert isinstance(output, compat_str)
1629     if version_re is None:
1630         version_re = r'version\s+([-0-9._a-zA-Z]+)'
1631     m = re.search(version_re, output)
1632     if m:
1633         return m.group(1)
1634     else:
1635         return unrecognized
1636
1637
1638 class PagedList(object):
1639     def __len__(self):
1640         # This is only useful for tests
1641         return len(self.getslice())
1642
1643
1644 class OnDemandPagedList(PagedList):
1645     def __init__(self, pagefunc, pagesize, use_cache=False):
1646         self._pagefunc = pagefunc
1647         self._pagesize = pagesize
1648         self._use_cache = use_cache
1649         if use_cache:
1650             self._cache = {}
1651
1652     def getslice(self, start=0, end=None):
1653         res = []
1654         for pagenum in itertools.count(start // self._pagesize):
1655             firstid = pagenum * self._pagesize
1656             nextfirstid = pagenum * self._pagesize + self._pagesize
1657             if start >= nextfirstid:
1658                 continue
1659
1660             page_results = None
1661             if self._use_cache:
1662                 page_results = self._cache.get(pagenum)
1663             if page_results is None:
1664                 page_results = list(self._pagefunc(pagenum))
1665             if self._use_cache:
1666                 self._cache[pagenum] = page_results
1667
1668             startv = (
1669                 start % self._pagesize
1670                 if firstid <= start < nextfirstid
1671                 else 0)
1672
1673             endv = (
1674                 ((end - 1) % self._pagesize) + 1
1675                 if (end is not None and firstid <= end <= nextfirstid)
1676                 else None)
1677
1678             if startv != 0 or endv is not None:
1679                 page_results = page_results[startv:endv]
1680             res.extend(page_results)
1681
1682             # A little optimization - if current page is not "full", ie. does
1683             # not contain page_size videos then we can assume that this page
1684             # is the last one - there are no more ids on further pages -
1685             # i.e. no need to query again.
1686             if len(page_results) + startv < self._pagesize:
1687                 break
1688
1689             # If we got the whole page, but the next page is not interesting,
1690             # break out early as well
1691             if end == nextfirstid:
1692                 break
1693         return res
1694
1695
1696 class InAdvancePagedList(PagedList):
1697     def __init__(self, pagefunc, pagecount, pagesize):
1698         self._pagefunc = pagefunc
1699         self._pagecount = pagecount
1700         self._pagesize = pagesize
1701
1702     def getslice(self, start=0, end=None):
1703         res = []
1704         start_page = start // self._pagesize
1705         end_page = (
1706             self._pagecount if end is None else (end // self._pagesize + 1))
1707         skip_elems = start - start_page * self._pagesize
1708         only_more = None if end is None else end - start
1709         for pagenum in range(start_page, end_page):
1710             page = list(self._pagefunc(pagenum))
1711             if skip_elems:
1712                 page = page[skip_elems:]
1713                 skip_elems = None
1714             if only_more is not None:
1715                 if len(page) < only_more:
1716                     only_more -= len(page)
1717                 else:
1718                     page = page[:only_more]
1719                     res.extend(page)
1720                     break
1721             res.extend(page)
1722         return res
1723
1724
1725 def uppercase_escape(s):
1726     unicode_escape = codecs.getdecoder('unicode_escape')
1727     return re.sub(
1728         r'\\U[0-9a-fA-F]{8}',
1729         lambda m: unicode_escape(m.group(0))[0],
1730         s)
1731
1732
1733 def lowercase_escape(s):
1734     unicode_escape = codecs.getdecoder('unicode_escape')
1735     return re.sub(
1736         r'\\u[0-9a-fA-F]{4}',
1737         lambda m: unicode_escape(m.group(0))[0],
1738         s)
1739
1740
1741 def escape_rfc3986(s):
1742     """Escape non-ASCII characters as suggested by RFC 3986"""
1743     if sys.version_info < (3, 0) and isinstance(s, compat_str):
1744         s = s.encode('utf-8')
1745     return compat_urllib_parse.quote(s, b"%/;:@&=+$,!~*'()?#[]")
1746
1747
1748 def escape_url(url):
1749     """Escape URL as suggested by RFC 3986"""
1750     url_parsed = compat_urllib_parse_urlparse(url)
1751     return url_parsed._replace(
1752         path=escape_rfc3986(url_parsed.path),
1753         params=escape_rfc3986(url_parsed.params),
1754         query=escape_rfc3986(url_parsed.query),
1755         fragment=escape_rfc3986(url_parsed.fragment)
1756     ).geturl()
1757
1758 try:
1759     struct.pack('!I', 0)
1760 except TypeError:
1761     # In Python 2.6 (and some 2.7 versions), struct requires a bytes argument
1762     def struct_pack(spec, *args):
1763         if isinstance(spec, compat_str):
1764             spec = spec.encode('ascii')
1765         return struct.pack(spec, *args)
1766
1767     def struct_unpack(spec, *args):
1768         if isinstance(spec, compat_str):
1769             spec = spec.encode('ascii')
1770         return struct.unpack(spec, *args)
1771 else:
1772     struct_pack = struct.pack
1773     struct_unpack = struct.unpack
1774
1775
1776 def read_batch_urls(batch_fd):
1777     def fixup(url):
1778         if not isinstance(url, compat_str):
1779             url = url.decode('utf-8', 'replace')
1780         BOM_UTF8 = '\xef\xbb\xbf'
1781         if url.startswith(BOM_UTF8):
1782             url = url[len(BOM_UTF8):]
1783         url = url.strip()
1784         if url.startswith(('#', ';', ']')):
1785             return False
1786         return url
1787
1788     with contextlib.closing(batch_fd) as fd:
1789         return [url for url in map(fixup, fd) if url]
1790
1791
1792 def urlencode_postdata(*args, **kargs):
1793     return compat_urllib_parse.urlencode(*args, **kargs).encode('ascii')
1794
1795
1796 def update_url_query(url, query):
1797     parsed_url = compat_urlparse.urlparse(url)
1798     qs = compat_parse_qs(parsed_url.query)
1799     qs.update(query)
1800     qs = encode_dict(qs)
1801     return compat_urlparse.urlunparse(parsed_url._replace(
1802         query=compat_urllib_parse.urlencode(qs, True)))
1803
1804
1805 def encode_dict(d, encoding='utf-8'):
1806     def encode(v):
1807         return v.encode(encoding) if isinstance(v, compat_basestring) else v
1808     return dict((encode(k), encode(v)) for k, v in d.items())
1809
1810
1811 def dict_get(d, key_or_keys, default=None, skip_false_values=True):
1812     if isinstance(key_or_keys, (list, tuple)):
1813         for key in key_or_keys:
1814             if key not in d or d[key] is None or skip_false_values and not d[key]:
1815                 continue
1816             return d[key]
1817         return default
1818     return d.get(key_or_keys, default)
1819
1820
1821 def encode_compat_str(string, encoding=preferredencoding(), errors='strict'):
1822     return string if isinstance(string, compat_str) else compat_str(string, encoding, errors)
1823
1824
1825 US_RATINGS = {
1826     'G': 0,
1827     'PG': 10,
1828     'PG-13': 13,
1829     'R': 16,
1830     'NC': 18,
1831 }
1832
1833
1834 def parse_age_limit(s):
1835     if s is None:
1836         return None
1837     m = re.match(r'^(?P<age>\d{1,2})\+?$', s)
1838     return int(m.group('age')) if m else US_RATINGS.get(s)
1839
1840
1841 def strip_jsonp(code):
1842     return re.sub(
1843         r'(?s)^[a-zA-Z0-9_.]+\s*\(\s*(.*)\);?\s*?(?://[^\n]*)*$', r'\1', code)
1844
1845
1846 def js_to_json(code):
1847     def fix_kv(m):
1848         v = m.group(0)
1849         if v in ('true', 'false', 'null'):
1850             return v
1851         if v.startswith('"'):
1852             v = re.sub(r"\\'", "'", v[1:-1])
1853         elif v.startswith("'"):
1854             v = v[1:-1]
1855             v = re.sub(r"\\\\|\\'|\"", lambda m: {
1856                 '\\\\': '\\\\',
1857                 "\\'": "'",
1858                 '"': '\\"',
1859             }[m.group(0)], v)
1860         return '"%s"' % v
1861
1862     res = re.sub(r'''(?x)
1863         "(?:[^"\\]*(?:\\\\|\\['"nu]))*[^"\\]*"|
1864         '(?:[^'\\]*(?:\\\\|\\['"nu]))*[^'\\]*'|
1865         [a-zA-Z_][.a-zA-Z_0-9]*
1866         ''', fix_kv, code)
1867     res = re.sub(r',(\s*[\]}])', lambda m: m.group(1), res)
1868     return res
1869
1870
1871 def qualities(quality_ids):
1872     """ Get a numeric quality value out of a list of possible values """
1873     def q(qid):
1874         try:
1875             return quality_ids.index(qid)
1876         except ValueError:
1877             return -1
1878     return q
1879
1880
1881 DEFAULT_OUTTMPL = '%(title)s-%(id)s.%(ext)s'
1882
1883
1884 def limit_length(s, length):
1885     """ Add ellipses to overly long strings """
1886     if s is None:
1887         return None
1888     ELLIPSES = '...'
1889     if len(s) > length:
1890         return s[:length - len(ELLIPSES)] + ELLIPSES
1891     return s
1892
1893
1894 def version_tuple(v):
1895     return tuple(int(e) for e in re.split(r'[-.]', v))
1896
1897
1898 def is_outdated_version(version, limit, assume_new=True):
1899     if not version:
1900         return not assume_new
1901     try:
1902         return version_tuple(version) < version_tuple(limit)
1903     except ValueError:
1904         return not assume_new
1905
1906
1907 def ytdl_is_updateable():
1908     """ Returns if youtube-dl can be updated with -U """
1909     from zipimport import zipimporter
1910
1911     return isinstance(globals().get('__loader__'), zipimporter) or hasattr(sys, 'frozen')
1912
1913
1914 def args_to_str(args):
1915     # Get a short string representation for a subprocess command
1916     return ' '.join(shlex_quote(a) for a in args)
1917
1918
1919 def error_to_compat_str(err):
1920     err_str = str(err)
1921     # On python 2 error byte string must be decoded with proper
1922     # encoding rather than ascii
1923     if sys.version_info[0] < 3:
1924         err_str = err_str.decode(preferredencoding())
1925     return err_str
1926
1927
1928 def mimetype2ext(mt):
1929     ext = {
1930         'audio/mp4': 'm4a',
1931     }.get(mt)
1932     if ext is not None:
1933         return ext
1934
1935     _, _, res = mt.rpartition('/')
1936
1937     return {
1938         '3gpp': '3gp',
1939         'smptett+xml': 'tt',
1940         'srt': 'srt',
1941         'ttaf+xml': 'dfxp',
1942         'ttml+xml': 'ttml',
1943         'vtt': 'vtt',
1944         'x-flv': 'flv',
1945         'x-mp4-fragmented': 'mp4',
1946         'x-ms-wmv': 'wmv',
1947     }.get(res, res)
1948
1949
1950 def urlhandle_detect_ext(url_handle):
1951     try:
1952         url_handle.headers
1953         getheader = lambda h: url_handle.headers[h]
1954     except AttributeError:  # Python < 3
1955         getheader = url_handle.info().getheader
1956
1957     cd = getheader('Content-Disposition')
1958     if cd:
1959         m = re.match(r'attachment;\s*filename="(?P<filename>[^"]+)"', cd)
1960         if m:
1961             e = determine_ext(m.group('filename'), default_ext=None)
1962             if e:
1963                 return e
1964
1965     return mimetype2ext(getheader('Content-Type'))
1966
1967
1968 def encode_data_uri(data, mime_type):
1969     return 'data:%s;base64,%s' % (mime_type, base64.b64encode(data).decode('ascii'))
1970
1971
1972 def age_restricted(content_limit, age_limit):
1973     """ Returns True iff the content should be blocked """
1974
1975     if age_limit is None:  # No limit set
1976         return False
1977     if content_limit is None:
1978         return False  # Content available for everyone
1979     return age_limit < content_limit
1980
1981
1982 def is_html(first_bytes):
1983     """ Detect whether a file contains HTML by examining its first bytes. """
1984
1985     BOMS = [
1986         (b'\xef\xbb\xbf', 'utf-8'),
1987         (b'\x00\x00\xfe\xff', 'utf-32-be'),
1988         (b'\xff\xfe\x00\x00', 'utf-32-le'),
1989         (b'\xff\xfe', 'utf-16-le'),
1990         (b'\xfe\xff', 'utf-16-be'),
1991     ]
1992     for bom, enc in BOMS:
1993         if first_bytes.startswith(bom):
1994             s = first_bytes[len(bom):].decode(enc, 'replace')
1995             break
1996     else:
1997         s = first_bytes.decode('utf-8', 'replace')
1998
1999     return re.match(r'^\s*<', s)
2000
2001
2002 def determine_protocol(info_dict):
2003     protocol = info_dict.get('protocol')
2004     if protocol is not None:
2005         return protocol
2006
2007     url = info_dict['url']
2008     if url.startswith('rtmp'):
2009         return 'rtmp'
2010     elif url.startswith('mms'):
2011         return 'mms'
2012     elif url.startswith('rtsp'):
2013         return 'rtsp'
2014
2015     ext = determine_ext(url)
2016     if ext == 'm3u8':
2017         return 'm3u8'
2018     elif ext == 'f4m':
2019         return 'f4m'
2020
2021     return compat_urllib_parse_urlparse(url).scheme
2022
2023
2024 def render_table(header_row, data):
2025     """ Render a list of rows, each as a list of values """
2026     table = [header_row] + data
2027     max_lens = [max(len(compat_str(v)) for v in col) for col in zip(*table)]
2028     format_str = ' '.join('%-' + compat_str(ml + 1) + 's' for ml in max_lens[:-1]) + '%s'
2029     return '\n'.join(format_str % tuple(row) for row in table)
2030
2031
2032 def _match_one(filter_part, dct):
2033     COMPARISON_OPERATORS = {
2034         '<': operator.lt,
2035         '<=': operator.le,
2036         '>': operator.gt,
2037         '>=': operator.ge,
2038         '=': operator.eq,
2039         '!=': operator.ne,
2040     }
2041     operator_rex = re.compile(r'''(?x)\s*
2042         (?P<key>[a-z_]+)
2043         \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
2044         (?:
2045             (?P<intval>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)|
2046             (?P<strval>(?![0-9.])[a-z0-9A-Z]*)
2047         )
2048         \s*$
2049         ''' % '|'.join(map(re.escape, COMPARISON_OPERATORS.keys())))
2050     m = operator_rex.search(filter_part)
2051     if m:
2052         op = COMPARISON_OPERATORS[m.group('op')]
2053         if m.group('strval') is not None:
2054             if m.group('op') not in ('=', '!='):
2055                 raise ValueError(
2056                     'Operator %s does not support string values!' % m.group('op'))
2057             comparison_value = m.group('strval')
2058         else:
2059             try:
2060                 comparison_value = int(m.group('intval'))
2061             except ValueError:
2062                 comparison_value = parse_filesize(m.group('intval'))
2063                 if comparison_value is None:
2064                     comparison_value = parse_filesize(m.group('intval') + 'B')
2065                 if comparison_value is None:
2066                     raise ValueError(
2067                         'Invalid integer value %r in filter part %r' % (
2068                             m.group('intval'), filter_part))
2069         actual_value = dct.get(m.group('key'))
2070         if actual_value is None:
2071             return m.group('none_inclusive')
2072         return op(actual_value, comparison_value)
2073
2074     UNARY_OPERATORS = {
2075         '': lambda v: v is not None,
2076         '!': lambda v: v is None,
2077     }
2078     operator_rex = re.compile(r'''(?x)\s*
2079         (?P<op>%s)\s*(?P<key>[a-z_]+)
2080         \s*$
2081         ''' % '|'.join(map(re.escape, UNARY_OPERATORS.keys())))
2082     m = operator_rex.search(filter_part)
2083     if m:
2084         op = UNARY_OPERATORS[m.group('op')]
2085         actual_value = dct.get(m.group('key'))
2086         return op(actual_value)
2087
2088     raise ValueError('Invalid filter part %r' % filter_part)
2089
2090
2091 def match_str(filter_str, dct):
2092     """ Filter a dictionary with a simple string syntax. Returns True (=passes filter) or false """
2093
2094     return all(
2095         _match_one(filter_part, dct) for filter_part in filter_str.split('&'))
2096
2097
2098 def match_filter_func(filter_str):
2099     def _match_func(info_dict):
2100         if match_str(filter_str, info_dict):
2101             return None
2102         else:
2103             video_title = info_dict.get('title', info_dict.get('id', 'video'))
2104             return '%s does not pass filter %s, skipping ..' % (video_title, filter_str)
2105     return _match_func
2106
2107
2108 def parse_dfxp_time_expr(time_expr):
2109     if not time_expr:
2110         return
2111
2112     mobj = re.match(r'^(?P<time_offset>\d+(?:\.\d+)?)s?$', time_expr)
2113     if mobj:
2114         return float(mobj.group('time_offset'))
2115
2116     mobj = re.match(r'^(\d+):(\d\d):(\d\d(?:(?:\.|:)\d+)?)$', time_expr)
2117     if mobj:
2118         return 3600 * int(mobj.group(1)) + 60 * int(mobj.group(2)) + float(mobj.group(3).replace(':', '.'))
2119
2120
2121 def srt_subtitles_timecode(seconds):
2122     return '%02d:%02d:%02d,%03d' % (seconds / 3600, (seconds % 3600) / 60, seconds % 60, (seconds % 1) * 1000)
2123
2124
2125 def dfxp2srt(dfxp_data):
2126     _x = functools.partial(xpath_with_ns, ns_map={
2127         'ttml': 'http://www.w3.org/ns/ttml',
2128         'ttaf1': 'http://www.w3.org/2006/10/ttaf1',
2129     })
2130
2131     class TTMLPElementParser(object):
2132         out = ''
2133
2134         def start(self, tag, attrib):
2135             if tag in (_x('ttml:br'), _x('ttaf1:br'), 'br'):
2136                 self.out += '\n'
2137
2138         def end(self, tag):
2139             pass
2140
2141         def data(self, data):
2142             self.out += data
2143
2144         def close(self):
2145             return self.out.strip()
2146
2147     def parse_node(node):
2148         target = TTMLPElementParser()
2149         parser = xml.etree.ElementTree.XMLParser(target=target)
2150         parser.feed(xml.etree.ElementTree.tostring(node))
2151         return parser.close()
2152
2153     dfxp = compat_etree_fromstring(dfxp_data.encode('utf-8'))
2154     out = []
2155     paras = dfxp.findall(_x('.//ttml:p')) or dfxp.findall(_x('.//ttaf1:p')) or dfxp.findall('.//p')
2156
2157     if not paras:
2158         raise ValueError('Invalid dfxp/TTML subtitle')
2159
2160     for para, index in zip(paras, itertools.count(1)):
2161         begin_time = parse_dfxp_time_expr(para.attrib.get('begin'))
2162         end_time = parse_dfxp_time_expr(para.attrib.get('end'))
2163         dur = parse_dfxp_time_expr(para.attrib.get('dur'))
2164         if begin_time is None:
2165             continue
2166         if not end_time:
2167             if not dur:
2168                 continue
2169             end_time = begin_time + dur
2170         out.append('%d\n%s --> %s\n%s\n\n' % (
2171             index,
2172             srt_subtitles_timecode(begin_time),
2173             srt_subtitles_timecode(end_time),
2174             parse_node(para)))
2175
2176     return ''.join(out)
2177
2178
2179 def cli_option(params, command_option, param):
2180     param = params.get(param)
2181     return [command_option, param] if param is not None else []
2182
2183
2184 def cli_bool_option(params, command_option, param, true_value='true', false_value='false', separator=None):
2185     param = params.get(param)
2186     assert isinstance(param, bool)
2187     if separator:
2188         return [command_option + separator + (true_value if param else false_value)]
2189     return [command_option, true_value if param else false_value]
2190
2191
2192 def cli_valueless_option(params, command_option, param, expected_value=True):
2193     param = params.get(param)
2194     return [command_option] if param == expected_value else []
2195
2196
2197 def cli_configuration_args(params, param, default=[]):
2198     ex_args = params.get(param)
2199     if ex_args is None:
2200         return default
2201     assert isinstance(ex_args, list)
2202     return ex_args
2203
2204
2205 class ISO639Utils(object):
2206     # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
2207     _lang_map = {
2208         'aa': 'aar',
2209         'ab': 'abk',
2210         'ae': 'ave',
2211         'af': 'afr',
2212         'ak': 'aka',
2213         'am': 'amh',
2214         'an': 'arg',
2215         'ar': 'ara',
2216         'as': 'asm',
2217         'av': 'ava',
2218         'ay': 'aym',
2219         'az': 'aze',
2220         'ba': 'bak',
2221         'be': 'bel',
2222         'bg': 'bul',
2223         'bh': 'bih',
2224         'bi': 'bis',
2225         'bm': 'bam',
2226         'bn': 'ben',
2227         'bo': 'bod',
2228         'br': 'bre',
2229         'bs': 'bos',
2230         'ca': 'cat',
2231         'ce': 'che',
2232         'ch': 'cha',
2233         'co': 'cos',
2234         'cr': 'cre',
2235         'cs': 'ces',
2236         'cu': 'chu',
2237         'cv': 'chv',
2238         'cy': 'cym',
2239         'da': 'dan',
2240         'de': 'deu',
2241         'dv': 'div',
2242         'dz': 'dzo',
2243         'ee': 'ewe',
2244         'el': 'ell',
2245         'en': 'eng',
2246         'eo': 'epo',
2247         'es': 'spa',
2248         'et': 'est',
2249         'eu': 'eus',
2250         'fa': 'fas',
2251         'ff': 'ful',
2252         'fi': 'fin',
2253         'fj': 'fij',
2254         'fo': 'fao',
2255         'fr': 'fra',
2256         'fy': 'fry',
2257         'ga': 'gle',
2258         'gd': 'gla',
2259         'gl': 'glg',
2260         'gn': 'grn',
2261         'gu': 'guj',
2262         'gv': 'glv',
2263         'ha': 'hau',
2264         'he': 'heb',
2265         'hi': 'hin',
2266         'ho': 'hmo',
2267         'hr': 'hrv',
2268         'ht': 'hat',
2269         'hu': 'hun',
2270         'hy': 'hye',
2271         'hz': 'her',
2272         'ia': 'ina',
2273         'id': 'ind',
2274         'ie': 'ile',
2275         'ig': 'ibo',
2276         'ii': 'iii',
2277         'ik': 'ipk',
2278         'io': 'ido',
2279         'is': 'isl',
2280         'it': 'ita',
2281         'iu': 'iku',
2282         'ja': 'jpn',
2283         'jv': 'jav',
2284         'ka': 'kat',
2285         'kg': 'kon',
2286         'ki': 'kik',
2287         'kj': 'kua',
2288         'kk': 'kaz',
2289         'kl': 'kal',
2290         'km': 'khm',
2291         'kn': 'kan',
2292         'ko': 'kor',
2293         'kr': 'kau',
2294         'ks': 'kas',
2295         'ku': 'kur',
2296         'kv': 'kom',
2297         'kw': 'cor',
2298         'ky': 'kir',
2299         'la': 'lat',
2300         'lb': 'ltz',
2301         'lg': 'lug',
2302         'li': 'lim',
2303         'ln': 'lin',
2304         'lo': 'lao',
2305         'lt': 'lit',
2306         'lu': 'lub',
2307         'lv': 'lav',
2308         'mg': 'mlg',
2309         'mh': 'mah',
2310         'mi': 'mri',
2311         'mk': 'mkd',
2312         'ml': 'mal',
2313         'mn': 'mon',
2314         'mr': 'mar',
2315         'ms': 'msa',
2316         'mt': 'mlt',
2317         'my': 'mya',
2318         'na': 'nau',
2319         'nb': 'nob',
2320         'nd': 'nde',
2321         'ne': 'nep',
2322         'ng': 'ndo',
2323         'nl': 'nld',
2324         'nn': 'nno',
2325         'no': 'nor',
2326         'nr': 'nbl',
2327         'nv': 'nav',
2328         'ny': 'nya',
2329         'oc': 'oci',
2330         'oj': 'oji',
2331         'om': 'orm',
2332         'or': 'ori',
2333         'os': 'oss',
2334         'pa': 'pan',
2335         'pi': 'pli',
2336         'pl': 'pol',
2337         'ps': 'pus',
2338         'pt': 'por',
2339         'qu': 'que',
2340         'rm': 'roh',
2341         'rn': 'run',
2342         'ro': 'ron',
2343         'ru': 'rus',
2344         'rw': 'kin',
2345         'sa': 'san',
2346         'sc': 'srd',
2347         'sd': 'snd',
2348         'se': 'sme',
2349         'sg': 'sag',
2350         'si': 'sin',
2351         'sk': 'slk',
2352         'sl': 'slv',
2353         'sm': 'smo',
2354         'sn': 'sna',
2355         'so': 'som',
2356         'sq': 'sqi',
2357         'sr': 'srp',
2358         'ss': 'ssw',
2359         'st': 'sot',
2360         'su': 'sun',
2361         'sv': 'swe',
2362         'sw': 'swa',
2363         'ta': 'tam',
2364         'te': 'tel',
2365         'tg': 'tgk',
2366         'th': 'tha',
2367         'ti': 'tir',
2368         'tk': 'tuk',
2369         'tl': 'tgl',
2370         'tn': 'tsn',
2371         'to': 'ton',
2372         'tr': 'tur',
2373         'ts': 'tso',
2374         'tt': 'tat',
2375         'tw': 'twi',
2376         'ty': 'tah',
2377         'ug': 'uig',
2378         'uk': 'ukr',
2379         'ur': 'urd',
2380         'uz': 'uzb',
2381         've': 'ven',
2382         'vi': 'vie',
2383         'vo': 'vol',
2384         'wa': 'wln',
2385         'wo': 'wol',
2386         'xh': 'xho',
2387         'yi': 'yid',
2388         'yo': 'yor',
2389         'za': 'zha',
2390         'zh': 'zho',
2391         'zu': 'zul',
2392     }
2393
2394     @classmethod
2395     def short2long(cls, code):
2396         """Convert language code from ISO 639-1 to ISO 639-2/T"""
2397         return cls._lang_map.get(code[:2])
2398
2399     @classmethod
2400     def long2short(cls, code):
2401         """Convert language code from ISO 639-2/T to ISO 639-1"""
2402         for short_name, long_name in cls._lang_map.items():
2403             if long_name == code:
2404                 return short_name
2405
2406
2407 class ISO3166Utils(object):
2408     # From http://data.okfn.org/data/core/country-list
2409     _country_map = {
2410         'AF': 'Afghanistan',
2411         'AX': 'Ã…land Islands',
2412         'AL': 'Albania',
2413         'DZ': 'Algeria',
2414         'AS': 'American Samoa',
2415         'AD': 'Andorra',
2416         'AO': 'Angola',
2417         'AI': 'Anguilla',
2418         'AQ': 'Antarctica',
2419         'AG': 'Antigua and Barbuda',
2420         'AR': 'Argentina',
2421         'AM': 'Armenia',
2422         'AW': 'Aruba',
2423         'AU': 'Australia',
2424         'AT': 'Austria',
2425         'AZ': 'Azerbaijan',
2426         'BS': 'Bahamas',
2427         'BH': 'Bahrain',
2428         'BD': 'Bangladesh',
2429         'BB': 'Barbados',
2430         'BY': 'Belarus',
2431         'BE': 'Belgium',
2432         'BZ': 'Belize',
2433         'BJ': 'Benin',
2434         'BM': 'Bermuda',
2435         'BT': 'Bhutan',
2436         'BO': 'Bolivia, Plurinational State of',
2437         'BQ': 'Bonaire, Sint Eustatius and Saba',
2438         'BA': 'Bosnia and Herzegovina',
2439         'BW': 'Botswana',
2440         'BV': 'Bouvet Island',
2441         'BR': 'Brazil',
2442         'IO': 'British Indian Ocean Territory',
2443         'BN': 'Brunei Darussalam',
2444         'BG': 'Bulgaria',
2445         'BF': 'Burkina Faso',
2446         'BI': 'Burundi',
2447         'KH': 'Cambodia',
2448         'CM': 'Cameroon',
2449         'CA': 'Canada',
2450         'CV': 'Cape Verde',
2451         'KY': 'Cayman Islands',
2452         'CF': 'Central African Republic',
2453         'TD': 'Chad',
2454         'CL': 'Chile',
2455         'CN': 'China',
2456         'CX': 'Christmas Island',
2457         'CC': 'Cocos (Keeling) Islands',
2458         'CO': 'Colombia',
2459         'KM': 'Comoros',
2460         'CG': 'Congo',
2461         'CD': 'Congo, the Democratic Republic of the',
2462         'CK': 'Cook Islands',
2463         'CR': 'Costa Rica',
2464         'CI': 'Côte d\'Ivoire',
2465         'HR': 'Croatia',
2466         'CU': 'Cuba',
2467         'CW': 'Curaçao',
2468         'CY': 'Cyprus',
2469         'CZ': 'Czech Republic',
2470         'DK': 'Denmark',
2471         'DJ': 'Djibouti',
2472         'DM': 'Dominica',
2473         'DO': 'Dominican Republic',
2474         'EC': 'Ecuador',
2475         'EG': 'Egypt',
2476         'SV': 'El Salvador',
2477         'GQ': 'Equatorial Guinea',
2478         'ER': 'Eritrea',
2479         'EE': 'Estonia',
2480         'ET': 'Ethiopia',
2481         'FK': 'Falkland Islands (Malvinas)',
2482         'FO': 'Faroe Islands',
2483         'FJ': 'Fiji',
2484         'FI': 'Finland',
2485         'FR': 'France',
2486         'GF': 'French Guiana',
2487         'PF': 'French Polynesia',
2488         'TF': 'French Southern Territories',
2489         'GA': 'Gabon',
2490         'GM': 'Gambia',
2491         'GE': 'Georgia',
2492         'DE': 'Germany',
2493         'GH': 'Ghana',
2494         'GI': 'Gibraltar',
2495         'GR': 'Greece',
2496         'GL': 'Greenland',
2497         'GD': 'Grenada',
2498         'GP': 'Guadeloupe',
2499         'GU': 'Guam',
2500         'GT': 'Guatemala',
2501         'GG': 'Guernsey',
2502         'GN': 'Guinea',
2503         'GW': 'Guinea-Bissau',
2504         'GY': 'Guyana',
2505         'HT': 'Haiti',
2506         'HM': 'Heard Island and McDonald Islands',
2507         'VA': 'Holy See (Vatican City State)',
2508         'HN': 'Honduras',
2509         'HK': 'Hong Kong',
2510         'HU': 'Hungary',
2511         'IS': 'Iceland',
2512         'IN': 'India',
2513         'ID': 'Indonesia',
2514         'IR': 'Iran, Islamic Republic of',
2515         'IQ': 'Iraq',
2516         'IE': 'Ireland',
2517         'IM': 'Isle of Man',
2518         'IL': 'Israel',
2519         'IT': 'Italy',
2520         'JM': 'Jamaica',
2521         'JP': 'Japan',
2522         'JE': 'Jersey',
2523         'JO': 'Jordan',
2524         'KZ': 'Kazakhstan',
2525         'KE': 'Kenya',
2526         'KI': 'Kiribati',
2527         'KP': 'Korea, Democratic People\'s Republic of',
2528         'KR': 'Korea, Republic of',
2529         'KW': 'Kuwait',
2530         'KG': 'Kyrgyzstan',
2531         'LA': 'Lao People\'s Democratic Republic',
2532         'LV': 'Latvia',
2533         'LB': 'Lebanon',
2534         'LS': 'Lesotho',
2535         'LR': 'Liberia',
2536         'LY': 'Libya',
2537         'LI': 'Liechtenstein',
2538         'LT': 'Lithuania',
2539         'LU': 'Luxembourg',
2540         'MO': 'Macao',
2541         'MK': 'Macedonia, the Former Yugoslav Republic of',
2542         'MG': 'Madagascar',
2543         'MW': 'Malawi',
2544         'MY': 'Malaysia',
2545         'MV': 'Maldives',
2546         'ML': 'Mali',
2547         'MT': 'Malta',
2548         'MH': 'Marshall Islands',
2549         'MQ': 'Martinique',
2550         'MR': 'Mauritania',
2551         'MU': 'Mauritius',
2552         'YT': 'Mayotte',
2553         'MX': 'Mexico',
2554         'FM': 'Micronesia, Federated States of',
2555         'MD': 'Moldova, Republic of',
2556         'MC': 'Monaco',
2557         'MN': 'Mongolia',
2558         'ME': 'Montenegro',
2559         'MS': 'Montserrat',
2560         'MA': 'Morocco',
2561         'MZ': 'Mozambique',
2562         'MM': 'Myanmar',
2563         'NA': 'Namibia',
2564         'NR': 'Nauru',
2565         'NP': 'Nepal',
2566         'NL': 'Netherlands',
2567         'NC': 'New Caledonia',
2568         'NZ': 'New Zealand',
2569         'NI': 'Nicaragua',
2570         'NE': 'Niger',
2571         'NG': 'Nigeria',
2572         'NU': 'Niue',
2573         'NF': 'Norfolk Island',
2574         'MP': 'Northern Mariana Islands',
2575         'NO': 'Norway',
2576         'OM': 'Oman',
2577         'PK': 'Pakistan',
2578         'PW': 'Palau',
2579         'PS': 'Palestine, State of',
2580         'PA': 'Panama',
2581         'PG': 'Papua New Guinea',
2582         'PY': 'Paraguay',
2583         'PE': 'Peru',
2584         'PH': 'Philippines',
2585         'PN': 'Pitcairn',
2586         'PL': 'Poland',
2587         'PT': 'Portugal',
2588         'PR': 'Puerto Rico',
2589         'QA': 'Qatar',
2590         'RE': 'Réunion',
2591         'RO': 'Romania',
2592         'RU': 'Russian Federation',
2593         'RW': 'Rwanda',
2594         'BL': 'Saint Barthélemy',
2595         'SH': 'Saint Helena, Ascension and Tristan da Cunha',
2596         'KN': 'Saint Kitts and Nevis',
2597         'LC': 'Saint Lucia',
2598         'MF': 'Saint Martin (French part)',
2599         'PM': 'Saint Pierre and Miquelon',
2600         'VC': 'Saint Vincent and the Grenadines',
2601         'WS': 'Samoa',
2602         'SM': 'San Marino',
2603         'ST': 'Sao Tome and Principe',
2604         'SA': 'Saudi Arabia',
2605         'SN': 'Senegal',
2606         'RS': 'Serbia',
2607         'SC': 'Seychelles',
2608         'SL': 'Sierra Leone',
2609         'SG': 'Singapore',
2610         'SX': 'Sint Maarten (Dutch part)',
2611         'SK': 'Slovakia',
2612         'SI': 'Slovenia',
2613         'SB': 'Solomon Islands',
2614         'SO': 'Somalia',
2615         'ZA': 'South Africa',
2616         'GS': 'South Georgia and the South Sandwich Islands',
2617         'SS': 'South Sudan',
2618         'ES': 'Spain',
2619         'LK': 'Sri Lanka',
2620         'SD': 'Sudan',
2621         'SR': 'Suriname',
2622         'SJ': 'Svalbard and Jan Mayen',
2623         'SZ': 'Swaziland',
2624         'SE': 'Sweden',
2625         'CH': 'Switzerland',
2626         'SY': 'Syrian Arab Republic',
2627         'TW': 'Taiwan, Province of China',
2628         'TJ': 'Tajikistan',
2629         'TZ': 'Tanzania, United Republic of',
2630         'TH': 'Thailand',
2631         'TL': 'Timor-Leste',
2632         'TG': 'Togo',
2633         'TK': 'Tokelau',
2634         'TO': 'Tonga',
2635         'TT': 'Trinidad and Tobago',
2636         'TN': 'Tunisia',
2637         'TR': 'Turkey',
2638         'TM': 'Turkmenistan',
2639         'TC': 'Turks and Caicos Islands',
2640         'TV': 'Tuvalu',
2641         'UG': 'Uganda',
2642         'UA': 'Ukraine',
2643         'AE': 'United Arab Emirates',
2644         'GB': 'United Kingdom',
2645         'US': 'United States',
2646         'UM': 'United States Minor Outlying Islands',
2647         'UY': 'Uruguay',
2648         'UZ': 'Uzbekistan',
2649         'VU': 'Vanuatu',
2650         'VE': 'Venezuela, Bolivarian Republic of',
2651         'VN': 'Viet Nam',
2652         'VG': 'Virgin Islands, British',
2653         'VI': 'Virgin Islands, U.S.',
2654         'WF': 'Wallis and Futuna',
2655         'EH': 'Western Sahara',
2656         'YE': 'Yemen',
2657         'ZM': 'Zambia',
2658         'ZW': 'Zimbabwe',
2659     }
2660
2661     @classmethod
2662     def short2full(cls, code):
2663         """Convert an ISO 3166-2 country code to the corresponding full name"""
2664         return cls._country_map.get(code.upper())
2665
2666
2667 class PerRequestProxyHandler(compat_urllib_request.ProxyHandler):
2668     def __init__(self, proxies=None):
2669         # Set default handlers
2670         for type in ('http', 'https'):
2671             setattr(self, '%s_open' % type,
2672                     lambda r, proxy='__noproxy__', type=type, meth=self.proxy_open:
2673                         meth(r, proxy, type))
2674         return compat_urllib_request.ProxyHandler.__init__(self, proxies)
2675
2676     def proxy_open(self, req, proxy, type):
2677         req_proxy = req.headers.get('Ytdl-request-proxy')
2678         if req_proxy is not None:
2679             proxy = req_proxy
2680             del req.headers['Ytdl-request-proxy']
2681
2682         if proxy == '__noproxy__':
2683             return None  # No Proxy
2684         return compat_urllib_request.ProxyHandler.proxy_open(
2685             self, req, proxy, type)
2686
2687
2688 def ohdave_rsa_encrypt(data, exponent, modulus):
2689     '''
2690     Implement OHDave's RSA algorithm. See http://www.ohdave.com/rsa/
2691
2692     Input:
2693         data: data to encrypt, bytes-like object
2694         exponent, modulus: parameter e and N of RSA algorithm, both integer
2695     Output: hex string of encrypted data
2696
2697     Limitation: supports one block encryption only
2698     '''
2699
2700     payload = int(binascii.hexlify(data[::-1]), 16)
2701     encrypted = pow(payload, exponent, modulus)
2702     return '%x' % encrypted
2703
2704
2705 def encode_base_n(num, n, table=None):
2706     FULL_TABLE = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
2707     if not table:
2708         table = FULL_TABLE[:n]
2709
2710     if n > len(table):
2711         raise ValueError('base %d exceeds table length %d' % (n, len(table)))
2712
2713     if num == 0:
2714         return table[0]
2715
2716     ret = ''
2717     while num:
2718         ret = table[num % n] + ret
2719         num = num // n
2720     return ret
2721
2722
2723 def decode_packed_codes(code):
2724     mobj = re.search(
2725         r"}\('(.+)',(\d+),(\d+),'([^']+)'\.split\('\|'\)",
2726         code)
2727     obfucasted_code, base, count, symbols = mobj.groups()
2728     base = int(base)
2729     count = int(count)
2730     symbols = symbols.split('|')
2731     symbol_table = {}
2732
2733     while count:
2734         count -= 1
2735         base_n_count = encode_base_n(count, base)
2736         symbol_table[base_n_count] = symbols[count] or base_n_count
2737
2738     return re.sub(
2739         r'\b(\w+)\b', lambda mobj: symbol_table[mobj.group(0)],
2740         obfucasted_code)