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