[utils] Use update_Request in http_request
[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 = update_Request(req, url=url_escaped)
782
783         for h, v in std_headers.items():
784             # Capitalize is needed because of Python bug 2275: http://bugs.python.org/issue2275
785             # The dict keys are capitalized because of this bug by urllib
786             if h.capitalize() not in req.headers:
787                 req.add_header(h, v)
788
789         req.headers = handle_youtubedl_headers(req.headers)
790
791         if sys.version_info < (2, 7) and '#' in req.get_full_url():
792             # Python 2.6 is brain-dead when it comes to fragments
793             req._Request__original = req._Request__original.partition('#')[0]
794             req._Request__r_type = req._Request__r_type.partition('#')[0]
795
796         return req
797
798     def http_response(self, req, resp):
799         old_resp = resp
800         # gzip
801         if resp.headers.get('Content-encoding', '') == 'gzip':
802             content = resp.read()
803             gz = gzip.GzipFile(fileobj=io.BytesIO(content), mode='rb')
804             try:
805                 uncompressed = io.BytesIO(gz.read())
806             except IOError as original_ioerror:
807                 # There may be junk add the end of the file
808                 # See http://stackoverflow.com/q/4928560/35070 for details
809                 for i in range(1, 1024):
810                     try:
811                         gz = gzip.GzipFile(fileobj=io.BytesIO(content[:-i]), mode='rb')
812                         uncompressed = io.BytesIO(gz.read())
813                     except IOError:
814                         continue
815                     break
816                 else:
817                     raise original_ioerror
818             resp = self.addinfourl_wrapper(uncompressed, old_resp.headers, old_resp.url, old_resp.code)
819             resp.msg = old_resp.msg
820             del resp.headers['Content-encoding']
821         # deflate
822         if resp.headers.get('Content-encoding', '') == 'deflate':
823             gz = io.BytesIO(self.deflate(resp.read()))
824             resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code)
825             resp.msg = old_resp.msg
826             del resp.headers['Content-encoding']
827         # Percent-encode redirect URL of Location HTTP header to satisfy RFC 3986 (see
828         # https://github.com/rg3/youtube-dl/issues/6457).
829         if 300 <= resp.code < 400:
830             location = resp.headers.get('Location')
831             if location:
832                 # As of RFC 2616 default charset is iso-8859-1 that is respected by python 3
833                 if sys.version_info >= (3, 0):
834                     location = location.encode('iso-8859-1').decode('utf-8')
835                 location_escaped = escape_url(location)
836                 if location != location_escaped:
837                     del resp.headers['Location']
838                     resp.headers['Location'] = location_escaped
839         return resp
840
841     https_request = http_request
842     https_response = http_response
843
844
845 class YoutubeDLHTTPSHandler(compat_urllib_request.HTTPSHandler):
846     def __init__(self, params, https_conn_class=None, *args, **kwargs):
847         compat_urllib_request.HTTPSHandler.__init__(self, *args, **kwargs)
848         self._https_conn_class = https_conn_class or compat_http_client.HTTPSConnection
849         self._params = params
850
851     def https_open(self, req):
852         kwargs = {}
853         if hasattr(self, '_context'):  # python > 2.6
854             kwargs['context'] = self._context
855         if hasattr(self, '_check_hostname'):  # python 3.x
856             kwargs['check_hostname'] = self._check_hostname
857         return self.do_open(functools.partial(
858             _create_http_connection, self, self._https_conn_class, True),
859             req, **kwargs)
860
861
862 class YoutubeDLCookieProcessor(compat_urllib_request.HTTPCookieProcessor):
863     def __init__(self, cookiejar=None):
864         compat_urllib_request.HTTPCookieProcessor.__init__(self, cookiejar)
865
866     def http_response(self, request, response):
867         # Python 2 will choke on next HTTP request in row if there are non-ASCII
868         # characters in Set-Cookie HTTP header of last response (see
869         # https://github.com/rg3/youtube-dl/issues/6769).
870         # In order to at least prevent crashing we will percent encode Set-Cookie
871         # header before HTTPCookieProcessor starts processing it.
872         # if sys.version_info < (3, 0) and response.headers:
873         #     for set_cookie_header in ('Set-Cookie', 'Set-Cookie2'):
874         #         set_cookie = response.headers.get(set_cookie_header)
875         #         if set_cookie:
876         #             set_cookie_escaped = compat_urllib_parse.quote(set_cookie, b"%/;:@&=+$,!~*'()?#[] ")
877         #             if set_cookie != set_cookie_escaped:
878         #                 del response.headers[set_cookie_header]
879         #                 response.headers[set_cookie_header] = set_cookie_escaped
880         return compat_urllib_request.HTTPCookieProcessor.http_response(self, request, response)
881
882     https_request = compat_urllib_request.HTTPCookieProcessor.http_request
883     https_response = http_response
884
885
886 def parse_iso8601(date_str, delimiter='T', timezone=None):
887     """ Return a UNIX timestamp from the given date """
888
889     if date_str is None:
890         return None
891
892     date_str = re.sub(r'\.[0-9]+', '', date_str)
893
894     if timezone is None:
895         m = re.search(
896             r'(?:Z$| ?(?P<sign>\+|-)(?P<hours>[0-9]{2}):?(?P<minutes>[0-9]{2})$)',
897             date_str)
898         if not m:
899             timezone = datetime.timedelta()
900         else:
901             date_str = date_str[:-len(m.group(0))]
902             if not m.group('sign'):
903                 timezone = datetime.timedelta()
904             else:
905                 sign = 1 if m.group('sign') == '+' else -1
906                 timezone = datetime.timedelta(
907                     hours=sign * int(m.group('hours')),
908                     minutes=sign * int(m.group('minutes')))
909     try:
910         date_format = '%Y-%m-%d{0}%H:%M:%S'.format(delimiter)
911         dt = datetime.datetime.strptime(date_str, date_format) - timezone
912         return calendar.timegm(dt.timetuple())
913     except ValueError:
914         pass
915
916
917 def unified_strdate(date_str, day_first=True):
918     """Return a string with the date in the format YYYYMMDD"""
919
920     if date_str is None:
921         return None
922     upload_date = None
923     # Replace commas
924     date_str = date_str.replace(',', ' ')
925     # %z (UTC offset) is only supported in python>=3.2
926     if not re.match(r'^[0-9]{1,2}-[0-9]{1,2}-[0-9]{4}$', date_str):
927         date_str = re.sub(r' ?(\+|-)[0-9]{2}:?[0-9]{2}$', '', date_str)
928     # Remove AM/PM + timezone
929     date_str = re.sub(r'(?i)\s*(?:AM|PM)(?:\s+[A-Z]+)?', '', date_str)
930
931     format_expressions = [
932         '%d %B %Y',
933         '%d %b %Y',
934         '%B %d %Y',
935         '%b %d %Y',
936         '%b %dst %Y %I:%M',
937         '%b %dnd %Y %I:%M',
938         '%b %dth %Y %I:%M',
939         '%Y %m %d',
940         '%Y-%m-%d',
941         '%Y/%m/%d',
942         '%Y/%m/%d %H:%M:%S',
943         '%Y-%m-%d %H:%M:%S',
944         '%Y-%m-%d %H:%M:%S.%f',
945         '%d.%m.%Y %H:%M',
946         '%d.%m.%Y %H.%M',
947         '%Y-%m-%dT%H:%M:%SZ',
948         '%Y-%m-%dT%H:%M:%S.%fZ',
949         '%Y-%m-%dT%H:%M:%S.%f0Z',
950         '%Y-%m-%dT%H:%M:%S',
951         '%Y-%m-%dT%H:%M:%S.%f',
952         '%Y-%m-%dT%H:%M',
953     ]
954     if day_first:
955         format_expressions.extend([
956             '%d-%m-%Y',
957             '%d.%m.%Y',
958             '%d/%m/%Y',
959             '%d/%m/%y',
960             '%d/%m/%Y %H:%M:%S',
961         ])
962     else:
963         format_expressions.extend([
964             '%m-%d-%Y',
965             '%m.%d.%Y',
966             '%m/%d/%Y',
967             '%m/%d/%y',
968             '%m/%d/%Y %H:%M:%S',
969         ])
970     for expression in format_expressions:
971         try:
972             upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
973         except ValueError:
974             pass
975     if upload_date is None:
976         timetuple = email.utils.parsedate_tz(date_str)
977         if timetuple:
978             upload_date = datetime.datetime(*timetuple[:6]).strftime('%Y%m%d')
979     if upload_date is not None:
980         return compat_str(upload_date)
981
982
983 def determine_ext(url, default_ext='unknown_video'):
984     if url is None:
985         return default_ext
986     guess = url.partition('?')[0].rpartition('.')[2]
987     if re.match(r'^[A-Za-z0-9]+$', guess):
988         return guess
989     # Try extract ext from URLs like http://example.com/foo/bar.mp4/?download
990     elif guess.rstrip('/') in KNOWN_EXTENSIONS:
991         return guess.rstrip('/')
992     else:
993         return default_ext
994
995
996 def subtitles_filename(filename, sub_lang, sub_format):
997     return filename.rsplit('.', 1)[0] + '.' + sub_lang + '.' + sub_format
998
999
1000 def date_from_str(date_str):
1001     """
1002     Return a datetime object from a string in the format YYYYMMDD or
1003     (now|today)[+-][0-9](day|week|month|year)(s)?"""
1004     today = datetime.date.today()
1005     if date_str in ('now', 'today'):
1006         return today
1007     if date_str == 'yesterday':
1008         return today - datetime.timedelta(days=1)
1009     match = re.match('(now|today)(?P<sign>[+-])(?P<time>\d+)(?P<unit>day|week|month|year)(s)?', date_str)
1010     if match is not None:
1011         sign = match.group('sign')
1012         time = int(match.group('time'))
1013         if sign == '-':
1014             time = -time
1015         unit = match.group('unit')
1016         # A bad approximation?
1017         if unit == 'month':
1018             unit = 'day'
1019             time *= 30
1020         elif unit == 'year':
1021             unit = 'day'
1022             time *= 365
1023         unit += 's'
1024         delta = datetime.timedelta(**{unit: time})
1025         return today + delta
1026     return datetime.datetime.strptime(date_str, '%Y%m%d').date()
1027
1028
1029 def hyphenate_date(date_str):
1030     """
1031     Convert a date in 'YYYYMMDD' format to 'YYYY-MM-DD' format"""
1032     match = re.match(r'^(\d\d\d\d)(\d\d)(\d\d)$', date_str)
1033     if match is not None:
1034         return '-'.join(match.groups())
1035     else:
1036         return date_str
1037
1038
1039 class DateRange(object):
1040     """Represents a time interval between two dates"""
1041
1042     def __init__(self, start=None, end=None):
1043         """start and end must be strings in the format accepted by date"""
1044         if start is not None:
1045             self.start = date_from_str(start)
1046         else:
1047             self.start = datetime.datetime.min.date()
1048         if end is not None:
1049             self.end = date_from_str(end)
1050         else:
1051             self.end = datetime.datetime.max.date()
1052         if self.start > self.end:
1053             raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
1054
1055     @classmethod
1056     def day(cls, day):
1057         """Returns a range that only contains the given day"""
1058         return cls(day, day)
1059
1060     def __contains__(self, date):
1061         """Check if the date is in the range"""
1062         if not isinstance(date, datetime.date):
1063             date = date_from_str(date)
1064         return self.start <= date <= self.end
1065
1066     def __str__(self):
1067         return '%s - %s' % (self.start.isoformat(), self.end.isoformat())
1068
1069
1070 def platform_name():
1071     """ Returns the platform name as a compat_str """
1072     res = platform.platform()
1073     if isinstance(res, bytes):
1074         res = res.decode(preferredencoding())
1075
1076     assert isinstance(res, compat_str)
1077     return res
1078
1079
1080 def _windows_write_string(s, out):
1081     """ Returns True if the string was written using special methods,
1082     False if it has yet to be written out."""
1083     # Adapted from http://stackoverflow.com/a/3259271/35070
1084
1085     import ctypes
1086     import ctypes.wintypes
1087
1088     WIN_OUTPUT_IDS = {
1089         1: -11,
1090         2: -12,
1091     }
1092
1093     try:
1094         fileno = out.fileno()
1095     except AttributeError:
1096         # If the output stream doesn't have a fileno, it's virtual
1097         return False
1098     except io.UnsupportedOperation:
1099         # Some strange Windows pseudo files?
1100         return False
1101     if fileno not in WIN_OUTPUT_IDS:
1102         return False
1103
1104     GetStdHandle = ctypes.WINFUNCTYPE(
1105         ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD)(
1106         (b'GetStdHandle', ctypes.windll.kernel32))
1107     h = GetStdHandle(WIN_OUTPUT_IDS[fileno])
1108
1109     WriteConsoleW = ctypes.WINFUNCTYPE(
1110         ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE, ctypes.wintypes.LPWSTR,
1111         ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD),
1112         ctypes.wintypes.LPVOID)((b'WriteConsoleW', ctypes.windll.kernel32))
1113     written = ctypes.wintypes.DWORD(0)
1114
1115     GetFileType = ctypes.WINFUNCTYPE(ctypes.wintypes.DWORD, ctypes.wintypes.DWORD)((b'GetFileType', ctypes.windll.kernel32))
1116     FILE_TYPE_CHAR = 0x0002
1117     FILE_TYPE_REMOTE = 0x8000
1118     GetConsoleMode = ctypes.WINFUNCTYPE(
1119         ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE,
1120         ctypes.POINTER(ctypes.wintypes.DWORD))(
1121         (b'GetConsoleMode', ctypes.windll.kernel32))
1122     INVALID_HANDLE_VALUE = ctypes.wintypes.DWORD(-1).value
1123
1124     def not_a_console(handle):
1125         if handle == INVALID_HANDLE_VALUE or handle is None:
1126             return True
1127         return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR or
1128                 GetConsoleMode(handle, ctypes.byref(ctypes.wintypes.DWORD())) == 0)
1129
1130     if not_a_console(h):
1131         return False
1132
1133     def next_nonbmp_pos(s):
1134         try:
1135             return next(i for i, c in enumerate(s) if ord(c) > 0xffff)
1136         except StopIteration:
1137             return len(s)
1138
1139     while s:
1140         count = min(next_nonbmp_pos(s), 1024)
1141
1142         ret = WriteConsoleW(
1143             h, s, count if count else 2, ctypes.byref(written), None)
1144         if ret == 0:
1145             raise OSError('Failed to write string')
1146         if not count:  # We just wrote a non-BMP character
1147             assert written.value == 2
1148             s = s[1:]
1149         else:
1150             assert written.value > 0
1151             s = s[written.value:]
1152     return True
1153
1154
1155 def write_string(s, out=None, encoding=None):
1156     if out is None:
1157         out = sys.stderr
1158     assert type(s) == compat_str
1159
1160     if sys.platform == 'win32' and encoding is None and hasattr(out, 'fileno'):
1161         if _windows_write_string(s, out):
1162             return
1163
1164     if ('b' in getattr(out, 'mode', '') or
1165             sys.version_info[0] < 3):  # Python 2 lies about mode of sys.stderr
1166         byt = s.encode(encoding or preferredencoding(), 'ignore')
1167         out.write(byt)
1168     elif hasattr(out, 'buffer'):
1169         enc = encoding or getattr(out, 'encoding', None) or preferredencoding()
1170         byt = s.encode(enc, 'ignore')
1171         out.buffer.write(byt)
1172     else:
1173         out.write(s)
1174     out.flush()
1175
1176
1177 def bytes_to_intlist(bs):
1178     if not bs:
1179         return []
1180     if isinstance(bs[0], int):  # Python 3
1181         return list(bs)
1182     else:
1183         return [ord(c) for c in bs]
1184
1185
1186 def intlist_to_bytes(xs):
1187     if not xs:
1188         return b''
1189     return struct_pack('%dB' % len(xs), *xs)
1190
1191
1192 # Cross-platform file locking
1193 if sys.platform == 'win32':
1194     import ctypes.wintypes
1195     import msvcrt
1196
1197     class OVERLAPPED(ctypes.Structure):
1198         _fields_ = [
1199             ('Internal', ctypes.wintypes.LPVOID),
1200             ('InternalHigh', ctypes.wintypes.LPVOID),
1201             ('Offset', ctypes.wintypes.DWORD),
1202             ('OffsetHigh', ctypes.wintypes.DWORD),
1203             ('hEvent', ctypes.wintypes.HANDLE),
1204         ]
1205
1206     kernel32 = ctypes.windll.kernel32
1207     LockFileEx = kernel32.LockFileEx
1208     LockFileEx.argtypes = [
1209         ctypes.wintypes.HANDLE,     # hFile
1210         ctypes.wintypes.DWORD,      # dwFlags
1211         ctypes.wintypes.DWORD,      # dwReserved
1212         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
1213         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
1214         ctypes.POINTER(OVERLAPPED)  # Overlapped
1215     ]
1216     LockFileEx.restype = ctypes.wintypes.BOOL
1217     UnlockFileEx = kernel32.UnlockFileEx
1218     UnlockFileEx.argtypes = [
1219         ctypes.wintypes.HANDLE,     # hFile
1220         ctypes.wintypes.DWORD,      # dwReserved
1221         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
1222         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
1223         ctypes.POINTER(OVERLAPPED)  # Overlapped
1224     ]
1225     UnlockFileEx.restype = ctypes.wintypes.BOOL
1226     whole_low = 0xffffffff
1227     whole_high = 0x7fffffff
1228
1229     def _lock_file(f, exclusive):
1230         overlapped = OVERLAPPED()
1231         overlapped.Offset = 0
1232         overlapped.OffsetHigh = 0
1233         overlapped.hEvent = 0
1234         f._lock_file_overlapped_p = ctypes.pointer(overlapped)
1235         handle = msvcrt.get_osfhandle(f.fileno())
1236         if not LockFileEx(handle, 0x2 if exclusive else 0x0, 0,
1237                           whole_low, whole_high, f._lock_file_overlapped_p):
1238             raise OSError('Locking file failed: %r' % ctypes.FormatError())
1239
1240     def _unlock_file(f):
1241         assert f._lock_file_overlapped_p
1242         handle = msvcrt.get_osfhandle(f.fileno())
1243         if not UnlockFileEx(handle, 0,
1244                             whole_low, whole_high, f._lock_file_overlapped_p):
1245             raise OSError('Unlocking file failed: %r' % ctypes.FormatError())
1246
1247 else:
1248     # Some platforms, such as Jython, is missing fcntl
1249     try:
1250         import fcntl
1251
1252         def _lock_file(f, exclusive):
1253             fcntl.flock(f, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
1254
1255         def _unlock_file(f):
1256             fcntl.flock(f, fcntl.LOCK_UN)
1257     except ImportError:
1258         UNSUPPORTED_MSG = 'file locking is not supported on this platform'
1259
1260         def _lock_file(f, exclusive):
1261             raise IOError(UNSUPPORTED_MSG)
1262
1263         def _unlock_file(f):
1264             raise IOError(UNSUPPORTED_MSG)
1265
1266
1267 class locked_file(object):
1268     def __init__(self, filename, mode, encoding=None):
1269         assert mode in ['r', 'a', 'w']
1270         self.f = io.open(filename, mode, encoding=encoding)
1271         self.mode = mode
1272
1273     def __enter__(self):
1274         exclusive = self.mode != 'r'
1275         try:
1276             _lock_file(self.f, exclusive)
1277         except IOError:
1278             self.f.close()
1279             raise
1280         return self
1281
1282     def __exit__(self, etype, value, traceback):
1283         try:
1284             _unlock_file(self.f)
1285         finally:
1286             self.f.close()
1287
1288     def __iter__(self):
1289         return iter(self.f)
1290
1291     def write(self, *args):
1292         return self.f.write(*args)
1293
1294     def read(self, *args):
1295         return self.f.read(*args)
1296
1297
1298 def get_filesystem_encoding():
1299     encoding = sys.getfilesystemencoding()
1300     return encoding if encoding is not None else 'utf-8'
1301
1302
1303 def shell_quote(args):
1304     quoted_args = []
1305     encoding = get_filesystem_encoding()
1306     for a in args:
1307         if isinstance(a, bytes):
1308             # We may get a filename encoded with 'encodeFilename'
1309             a = a.decode(encoding)
1310         quoted_args.append(pipes.quote(a))
1311     return ' '.join(quoted_args)
1312
1313
1314 def smuggle_url(url, data):
1315     """ Pass additional data in a URL for internal use. """
1316
1317     sdata = compat_urllib_parse_urlencode(
1318         {'__youtubedl_smuggle': json.dumps(data)})
1319     return url + '#' + sdata
1320
1321
1322 def unsmuggle_url(smug_url, default=None):
1323     if '#__youtubedl_smuggle' not in smug_url:
1324         return smug_url, default
1325     url, _, sdata = smug_url.rpartition('#')
1326     jsond = compat_parse_qs(sdata)['__youtubedl_smuggle'][0]
1327     data = json.loads(jsond)
1328     return url, data
1329
1330
1331 def format_bytes(bytes):
1332     if bytes is None:
1333         return 'N/A'
1334     if type(bytes) is str:
1335         bytes = float(bytes)
1336     if bytes == 0.0:
1337         exponent = 0
1338     else:
1339         exponent = int(math.log(bytes, 1024.0))
1340     suffix = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'][exponent]
1341     converted = float(bytes) / float(1024 ** exponent)
1342     return '%.2f%s' % (converted, suffix)
1343
1344
1345 def lookup_unit_table(unit_table, s):
1346     units_re = '|'.join(re.escape(u) for u in unit_table)
1347     m = re.match(
1348         r'(?P<num>[0-9]+(?:[,.][0-9]*)?)\s*(?P<unit>%s)\b' % units_re, s)
1349     if not m:
1350         return None
1351     num_str = m.group('num').replace(',', '.')
1352     mult = unit_table[m.group('unit')]
1353     return int(float(num_str) * mult)
1354
1355
1356 def parse_filesize(s):
1357     if s is None:
1358         return None
1359
1360     # The lower-case forms are of course incorrect and unofficial,
1361     # but we support those too
1362     _UNIT_TABLE = {
1363         'B': 1,
1364         'b': 1,
1365         'KiB': 1024,
1366         'KB': 1000,
1367         'kB': 1024,
1368         'Kb': 1000,
1369         'MiB': 1024 ** 2,
1370         'MB': 1000 ** 2,
1371         'mB': 1024 ** 2,
1372         'Mb': 1000 ** 2,
1373         'GiB': 1024 ** 3,
1374         'GB': 1000 ** 3,
1375         'gB': 1024 ** 3,
1376         'Gb': 1000 ** 3,
1377         'TiB': 1024 ** 4,
1378         'TB': 1000 ** 4,
1379         'tB': 1024 ** 4,
1380         'Tb': 1000 ** 4,
1381         'PiB': 1024 ** 5,
1382         'PB': 1000 ** 5,
1383         'pB': 1024 ** 5,
1384         'Pb': 1000 ** 5,
1385         'EiB': 1024 ** 6,
1386         'EB': 1000 ** 6,
1387         'eB': 1024 ** 6,
1388         'Eb': 1000 ** 6,
1389         'ZiB': 1024 ** 7,
1390         'ZB': 1000 ** 7,
1391         'zB': 1024 ** 7,
1392         'Zb': 1000 ** 7,
1393         'YiB': 1024 ** 8,
1394         'YB': 1000 ** 8,
1395         'yB': 1024 ** 8,
1396         'Yb': 1000 ** 8,
1397     }
1398
1399     return lookup_unit_table(_UNIT_TABLE, s)
1400
1401
1402 def parse_count(s):
1403     if s is None:
1404         return None
1405
1406     s = s.strip()
1407
1408     if re.match(r'^[\d,.]+$', s):
1409         return str_to_int(s)
1410
1411     _UNIT_TABLE = {
1412         'k': 1000,
1413         'K': 1000,
1414         'm': 1000 ** 2,
1415         'M': 1000 ** 2,
1416         'kk': 1000 ** 2,
1417         'KK': 1000 ** 2,
1418     }
1419
1420     return lookup_unit_table(_UNIT_TABLE, s)
1421
1422
1423 def month_by_name(name):
1424     """ Return the number of a month by (locale-independently) English name """
1425
1426     try:
1427         return ENGLISH_MONTH_NAMES.index(name) + 1
1428     except ValueError:
1429         return None
1430
1431
1432 def month_by_abbreviation(abbrev):
1433     """ Return the number of a month by (locale-independently) English
1434         abbreviations """
1435
1436     try:
1437         return [s[:3] for s in ENGLISH_MONTH_NAMES].index(abbrev) + 1
1438     except ValueError:
1439         return None
1440
1441
1442 def fix_xml_ampersands(xml_str):
1443     """Replace all the '&' by '&amp;' in XML"""
1444     return re.sub(
1445         r'&(?!amp;|lt;|gt;|apos;|quot;|#x[0-9a-fA-F]{,4};|#[0-9]{,4};)',
1446         '&amp;',
1447         xml_str)
1448
1449
1450 def setproctitle(title):
1451     assert isinstance(title, compat_str)
1452
1453     # ctypes in Jython is not complete
1454     # http://bugs.jython.org/issue2148
1455     if sys.platform.startswith('java'):
1456         return
1457
1458     try:
1459         libc = ctypes.cdll.LoadLibrary('libc.so.6')
1460     except OSError:
1461         return
1462     title_bytes = title.encode('utf-8')
1463     buf = ctypes.create_string_buffer(len(title_bytes))
1464     buf.value = title_bytes
1465     try:
1466         libc.prctl(15, buf, 0, 0, 0)
1467     except AttributeError:
1468         return  # Strange libc, just skip this
1469
1470
1471 def remove_start(s, start):
1472     if s.startswith(start):
1473         return s[len(start):]
1474     return s
1475
1476
1477 def remove_end(s, end):
1478     if s.endswith(end):
1479         return s[:-len(end)]
1480     return s
1481
1482
1483 def remove_quotes(s):
1484     if s is None or len(s) < 2:
1485         return s
1486     for quote in ('"', "'", ):
1487         if s[0] == quote and s[-1] == quote:
1488             return s[1:-1]
1489     return s
1490
1491
1492 def url_basename(url):
1493     path = compat_urlparse.urlparse(url).path
1494     return path.strip('/').split('/')[-1]
1495
1496
1497 class HEADRequest(compat_urllib_request.Request):
1498     def get_method(self):
1499         return 'HEAD'
1500
1501
1502 def int_or_none(v, scale=1, default=None, get_attr=None, invscale=1):
1503     if get_attr:
1504         if v is not None:
1505             v = getattr(v, get_attr, None)
1506     if v == '':
1507         v = None
1508     if v is None:
1509         return default
1510     try:
1511         return int(v) * invscale // scale
1512     except ValueError:
1513         return default
1514
1515
1516 def str_or_none(v, default=None):
1517     return default if v is None else compat_str(v)
1518
1519
1520 def str_to_int(int_str):
1521     """ A more relaxed version of int_or_none """
1522     if int_str is None:
1523         return None
1524     int_str = re.sub(r'[,\.\+]', '', int_str)
1525     return int(int_str)
1526
1527
1528 def float_or_none(v, scale=1, invscale=1, default=None):
1529     if v is None:
1530         return default
1531     try:
1532         return float(v) * invscale / scale
1533     except ValueError:
1534         return default
1535
1536
1537 def parse_duration(s):
1538     if not isinstance(s, compat_basestring):
1539         return None
1540
1541     s = s.strip()
1542
1543     m = re.match(
1544         r'''(?ix)(?:P?T)?
1545         (?:
1546             (?P<only_mins>[0-9.]+)\s*(?:mins?\.?|minutes?)\s*|
1547             (?P<only_hours>[0-9.]+)\s*(?:hours?)|
1548
1549             \s*(?P<hours_reversed>[0-9]+)\s*(?:[:h]|hours?)\s*(?P<mins_reversed>[0-9]+)\s*(?:[:m]|mins?\.?|minutes?)\s*|
1550             (?:
1551                 (?:
1552                     (?:(?P<days>[0-9]+)\s*(?:[:d]|days?)\s*)?
1553                     (?P<hours>[0-9]+)\s*(?:[:h]|hours?)\s*
1554                 )?
1555                 (?P<mins>[0-9]+)\s*(?:[:m]|mins?|minutes?)\s*
1556             )?
1557             (?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?\s*(?:s|secs?|seconds?)?
1558         )$''', s)
1559     if not m:
1560         return None
1561     res = 0
1562     if m.group('only_mins'):
1563         return float_or_none(m.group('only_mins'), invscale=60)
1564     if m.group('only_hours'):
1565         return float_or_none(m.group('only_hours'), invscale=60 * 60)
1566     if m.group('secs'):
1567         res += int(m.group('secs'))
1568     if m.group('mins_reversed'):
1569         res += int(m.group('mins_reversed')) * 60
1570     if m.group('mins'):
1571         res += int(m.group('mins')) * 60
1572     if m.group('hours'):
1573         res += int(m.group('hours')) * 60 * 60
1574     if m.group('hours_reversed'):
1575         res += int(m.group('hours_reversed')) * 60 * 60
1576     if m.group('days'):
1577         res += int(m.group('days')) * 24 * 60 * 60
1578     if m.group('ms'):
1579         res += float(m.group('ms'))
1580     return res
1581
1582
1583 def prepend_extension(filename, ext, expected_real_ext=None):
1584     name, real_ext = os.path.splitext(filename)
1585     return (
1586         '{0}.{1}{2}'.format(name, ext, real_ext)
1587         if not expected_real_ext or real_ext[1:] == expected_real_ext
1588         else '{0}.{1}'.format(filename, ext))
1589
1590
1591 def replace_extension(filename, ext, expected_real_ext=None):
1592     name, real_ext = os.path.splitext(filename)
1593     return '{0}.{1}'.format(
1594         name if not expected_real_ext or real_ext[1:] == expected_real_ext else filename,
1595         ext)
1596
1597
1598 def check_executable(exe, args=[]):
1599     """ Checks if the given binary is installed somewhere in PATH, and returns its name.
1600     args can be a list of arguments for a short output (like -version) """
1601     try:
1602         subprocess.Popen([exe] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
1603     except OSError:
1604         return False
1605     return exe
1606
1607
1608 def get_exe_version(exe, args=['--version'],
1609                     version_re=None, unrecognized='present'):
1610     """ Returns the version of the specified executable,
1611     or False if the executable is not present """
1612     try:
1613         out, _ = subprocess.Popen(
1614             [encodeArgument(exe)] + args,
1615             stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()
1616     except OSError:
1617         return False
1618     if isinstance(out, bytes):  # Python 2.x
1619         out = out.decode('ascii', 'ignore')
1620     return detect_exe_version(out, version_re, unrecognized)
1621
1622
1623 def detect_exe_version(output, version_re=None, unrecognized='present'):
1624     assert isinstance(output, compat_str)
1625     if version_re is None:
1626         version_re = r'version\s+([-0-9._a-zA-Z]+)'
1627     m = re.search(version_re, output)
1628     if m:
1629         return m.group(1)
1630     else:
1631         return unrecognized
1632
1633
1634 class PagedList(object):
1635     def __len__(self):
1636         # This is only useful for tests
1637         return len(self.getslice())
1638
1639
1640 class OnDemandPagedList(PagedList):
1641     def __init__(self, pagefunc, pagesize, use_cache=False):
1642         self._pagefunc = pagefunc
1643         self._pagesize = pagesize
1644         self._use_cache = use_cache
1645         if use_cache:
1646             self._cache = {}
1647
1648     def getslice(self, start=0, end=None):
1649         res = []
1650         for pagenum in itertools.count(start // self._pagesize):
1651             firstid = pagenum * self._pagesize
1652             nextfirstid = pagenum * self._pagesize + self._pagesize
1653             if start >= nextfirstid:
1654                 continue
1655
1656             page_results = None
1657             if self._use_cache:
1658                 page_results = self._cache.get(pagenum)
1659             if page_results is None:
1660                 page_results = list(self._pagefunc(pagenum))
1661             if self._use_cache:
1662                 self._cache[pagenum] = page_results
1663
1664             startv = (
1665                 start % self._pagesize
1666                 if firstid <= start < nextfirstid
1667                 else 0)
1668
1669             endv = (
1670                 ((end - 1) % self._pagesize) + 1
1671                 if (end is not None and firstid <= end <= nextfirstid)
1672                 else None)
1673
1674             if startv != 0 or endv is not None:
1675                 page_results = page_results[startv:endv]
1676             res.extend(page_results)
1677
1678             # A little optimization - if current page is not "full", ie. does
1679             # not contain page_size videos then we can assume that this page
1680             # is the last one - there are no more ids on further pages -
1681             # i.e. no need to query again.
1682             if len(page_results) + startv < self._pagesize:
1683                 break
1684
1685             # If we got the whole page, but the next page is not interesting,
1686             # break out early as well
1687             if end == nextfirstid:
1688                 break
1689         return res
1690
1691
1692 class InAdvancePagedList(PagedList):
1693     def __init__(self, pagefunc, pagecount, pagesize):
1694         self._pagefunc = pagefunc
1695         self._pagecount = pagecount
1696         self._pagesize = pagesize
1697
1698     def getslice(self, start=0, end=None):
1699         res = []
1700         start_page = start // self._pagesize
1701         end_page = (
1702             self._pagecount if end is None else (end // self._pagesize + 1))
1703         skip_elems = start - start_page * self._pagesize
1704         only_more = None if end is None else end - start
1705         for pagenum in range(start_page, end_page):
1706             page = list(self._pagefunc(pagenum))
1707             if skip_elems:
1708                 page = page[skip_elems:]
1709                 skip_elems = None
1710             if only_more is not None:
1711                 if len(page) < only_more:
1712                     only_more -= len(page)
1713                 else:
1714                     page = page[:only_more]
1715                     res.extend(page)
1716                     break
1717             res.extend(page)
1718         return res
1719
1720
1721 def uppercase_escape(s):
1722     unicode_escape = codecs.getdecoder('unicode_escape')
1723     return re.sub(
1724         r'\\U[0-9a-fA-F]{8}',
1725         lambda m: unicode_escape(m.group(0))[0],
1726         s)
1727
1728
1729 def lowercase_escape(s):
1730     unicode_escape = codecs.getdecoder('unicode_escape')
1731     return re.sub(
1732         r'\\u[0-9a-fA-F]{4}',
1733         lambda m: unicode_escape(m.group(0))[0],
1734         s)
1735
1736
1737 def escape_rfc3986(s):
1738     """Escape non-ASCII characters as suggested by RFC 3986"""
1739     if sys.version_info < (3, 0) and isinstance(s, compat_str):
1740         s = s.encode('utf-8')
1741     return compat_urllib_parse.quote(s, b"%/;:@&=+$,!~*'()?#[]")
1742
1743
1744 def escape_url(url):
1745     """Escape URL as suggested by RFC 3986"""
1746     url_parsed = compat_urllib_parse_urlparse(url)
1747     return url_parsed._replace(
1748         netloc=url_parsed.netloc.encode('idna').decode('ascii'),
1749         path=escape_rfc3986(url_parsed.path),
1750         params=escape_rfc3986(url_parsed.params),
1751         query=escape_rfc3986(url_parsed.query),
1752         fragment=escape_rfc3986(url_parsed.fragment)
1753     ).geturl()
1754
1755 try:
1756     struct.pack('!I', 0)
1757 except TypeError:
1758     # In Python 2.6 and 2.7.x < 2.7.7, struct requires a bytes argument
1759     # See https://bugs.python.org/issue19099
1760     def struct_pack(spec, *args):
1761         if isinstance(spec, compat_str):
1762             spec = spec.encode('ascii')
1763         return struct.pack(spec, *args)
1764
1765     def struct_unpack(spec, *args):
1766         if isinstance(spec, compat_str):
1767             spec = spec.encode('ascii')
1768         return struct.unpack(spec, *args)
1769 else:
1770     struct_pack = struct.pack
1771     struct_unpack = struct.unpack
1772
1773
1774 def read_batch_urls(batch_fd):
1775     def fixup(url):
1776         if not isinstance(url, compat_str):
1777             url = url.decode('utf-8', 'replace')
1778         BOM_UTF8 = '\xef\xbb\xbf'
1779         if url.startswith(BOM_UTF8):
1780             url = url[len(BOM_UTF8):]
1781         url = url.strip()
1782         if url.startswith(('#', ';', ']')):
1783             return False
1784         return url
1785
1786     with contextlib.closing(batch_fd) as fd:
1787         return [url for url in map(fixup, fd) if url]
1788
1789
1790 def urlencode_postdata(*args, **kargs):
1791     return compat_urllib_parse_urlencode(*args, **kargs).encode('ascii')
1792
1793
1794 def update_url_query(url, query):
1795     parsed_url = compat_urlparse.urlparse(url)
1796     qs = compat_parse_qs(parsed_url.query)
1797     qs.update(query)
1798     return compat_urlparse.urlunparse(parsed_url._replace(
1799         query=compat_urllib_parse_urlencode(qs, True)))
1800
1801
1802 def update_Request(req, url=None, data=None, headers={}, query={}):
1803     req_headers = req.headers.copy()
1804     req_headers.update(headers)
1805     req_data = data or req.data
1806     req_url = update_url_query(url or req.get_full_url(), query)
1807     req_type = HEADRequest if req.get_method() == 'HEAD' else compat_urllib_request.Request
1808     new_req = req_type(
1809         req_url, data=req_data, headers=req_headers,
1810         origin_req_host=req.origin_req_host, unverifiable=req.unverifiable)
1811     if hasattr(req, 'timeout'):
1812         new_req.timeout = req.timeout
1813     return new_req
1814
1815
1816 def dict_get(d, key_or_keys, default=None, skip_false_values=True):
1817     if isinstance(key_or_keys, (list, tuple)):
1818         for key in key_or_keys:
1819             if key not in d or d[key] is None or skip_false_values and not d[key]:
1820                 continue
1821             return d[key]
1822         return default
1823     return d.get(key_or_keys, default)
1824
1825
1826 def encode_compat_str(string, encoding=preferredencoding(), errors='strict'):
1827     return string if isinstance(string, compat_str) else compat_str(string, encoding, errors)
1828
1829
1830 US_RATINGS = {
1831     'G': 0,
1832     'PG': 10,
1833     'PG-13': 13,
1834     'R': 16,
1835     'NC': 18,
1836 }
1837
1838
1839 def parse_age_limit(s):
1840     if s is None:
1841         return None
1842     m = re.match(r'^(?P<age>\d{1,2})\+?$', s)
1843     return int(m.group('age')) if m else US_RATINGS.get(s)
1844
1845
1846 def strip_jsonp(code):
1847     return re.sub(
1848         r'(?s)^[a-zA-Z0-9_.]+\s*\(\s*(.*)\);?\s*?(?://[^\n]*)*$', r'\1', code)
1849
1850
1851 def js_to_json(code):
1852     def fix_kv(m):
1853         v = m.group(0)
1854         if v in ('true', 'false', 'null'):
1855             return v
1856         if v.startswith('"'):
1857             v = re.sub(r"\\'", "'", v[1:-1])
1858         elif v.startswith("'"):
1859             v = v[1:-1]
1860             v = re.sub(r"\\\\|\\'|\"", lambda m: {
1861                 '\\\\': '\\\\',
1862                 "\\'": "'",
1863                 '"': '\\"',
1864             }[m.group(0)], v)
1865         return '"%s"' % v
1866
1867     res = re.sub(r'''(?x)
1868         "(?:[^"\\]*(?:\\\\|\\['"nu]))*[^"\\]*"|
1869         '(?:[^'\\]*(?:\\\\|\\['"nu]))*[^'\\]*'|
1870         [a-zA-Z_][.a-zA-Z_0-9]*
1871         ''', fix_kv, code)
1872     res = re.sub(r',(\s*[\]}])', lambda m: m.group(1), res)
1873     return res
1874
1875
1876 def qualities(quality_ids):
1877     """ Get a numeric quality value out of a list of possible values """
1878     def q(qid):
1879         try:
1880             return quality_ids.index(qid)
1881         except ValueError:
1882             return -1
1883     return q
1884
1885
1886 DEFAULT_OUTTMPL = '%(title)s-%(id)s.%(ext)s'
1887
1888
1889 def limit_length(s, length):
1890     """ Add ellipses to overly long strings """
1891     if s is None:
1892         return None
1893     ELLIPSES = '...'
1894     if len(s) > length:
1895         return s[:length - len(ELLIPSES)] + ELLIPSES
1896     return s
1897
1898
1899 def version_tuple(v):
1900     return tuple(int(e) for e in re.split(r'[-.]', v))
1901
1902
1903 def is_outdated_version(version, limit, assume_new=True):
1904     if not version:
1905         return not assume_new
1906     try:
1907         return version_tuple(version) < version_tuple(limit)
1908     except ValueError:
1909         return not assume_new
1910
1911
1912 def ytdl_is_updateable():
1913     """ Returns if youtube-dl can be updated with -U """
1914     from zipimport import zipimporter
1915
1916     return isinstance(globals().get('__loader__'), zipimporter) or hasattr(sys, 'frozen')
1917
1918
1919 def args_to_str(args):
1920     # Get a short string representation for a subprocess command
1921     return ' '.join(shlex_quote(a) for a in args)
1922
1923
1924 def error_to_compat_str(err):
1925     err_str = str(err)
1926     # On python 2 error byte string must be decoded with proper
1927     # encoding rather than ascii
1928     if sys.version_info[0] < 3:
1929         err_str = err_str.decode(preferredencoding())
1930     return err_str
1931
1932
1933 def mimetype2ext(mt):
1934     ext = {
1935         'audio/mp4': 'm4a',
1936     }.get(mt)
1937     if ext is not None:
1938         return ext
1939
1940     _, _, res = mt.rpartition('/')
1941
1942     return {
1943         '3gpp': '3gp',
1944         'smptett+xml': 'tt',
1945         'srt': 'srt',
1946         'ttaf+xml': 'dfxp',
1947         'ttml+xml': 'ttml',
1948         'vtt': 'vtt',
1949         'x-flv': 'flv',
1950         'x-mp4-fragmented': 'mp4',
1951         'x-ms-wmv': 'wmv',
1952     }.get(res, res)
1953
1954
1955 def urlhandle_detect_ext(url_handle):
1956     try:
1957         url_handle.headers
1958         getheader = lambda h: url_handle.headers[h]
1959     except AttributeError:  # Python < 3
1960         getheader = url_handle.info().getheader
1961
1962     cd = getheader('Content-Disposition')
1963     if cd:
1964         m = re.match(r'attachment;\s*filename="(?P<filename>[^"]+)"', cd)
1965         if m:
1966             e = determine_ext(m.group('filename'), default_ext=None)
1967             if e:
1968                 return e
1969
1970     return mimetype2ext(getheader('Content-Type'))
1971
1972
1973 def encode_data_uri(data, mime_type):
1974     return 'data:%s;base64,%s' % (mime_type, base64.b64encode(data).decode('ascii'))
1975
1976
1977 def age_restricted(content_limit, age_limit):
1978     """ Returns True iff the content should be blocked """
1979
1980     if age_limit is None:  # No limit set
1981         return False
1982     if content_limit is None:
1983         return False  # Content available for everyone
1984     return age_limit < content_limit
1985
1986
1987 def is_html(first_bytes):
1988     """ Detect whether a file contains HTML by examining its first bytes. """
1989
1990     BOMS = [
1991         (b'\xef\xbb\xbf', 'utf-8'),
1992         (b'\x00\x00\xfe\xff', 'utf-32-be'),
1993         (b'\xff\xfe\x00\x00', 'utf-32-le'),
1994         (b'\xff\xfe', 'utf-16-le'),
1995         (b'\xfe\xff', 'utf-16-be'),
1996     ]
1997     for bom, enc in BOMS:
1998         if first_bytes.startswith(bom):
1999             s = first_bytes[len(bom):].decode(enc, 'replace')
2000             break
2001     else:
2002         s = first_bytes.decode('utf-8', 'replace')
2003
2004     return re.match(r'^\s*<', s)
2005
2006
2007 def determine_protocol(info_dict):
2008     protocol = info_dict.get('protocol')
2009     if protocol is not None:
2010         return protocol
2011
2012     url = info_dict['url']
2013     if url.startswith('rtmp'):
2014         return 'rtmp'
2015     elif url.startswith('mms'):
2016         return 'mms'
2017     elif url.startswith('rtsp'):
2018         return 'rtsp'
2019
2020     ext = determine_ext(url)
2021     if ext == 'm3u8':
2022         return 'm3u8'
2023     elif ext == 'f4m':
2024         return 'f4m'
2025
2026     return compat_urllib_parse_urlparse(url).scheme
2027
2028
2029 def render_table(header_row, data):
2030     """ Render a list of rows, each as a list of values """
2031     table = [header_row] + data
2032     max_lens = [max(len(compat_str(v)) for v in col) for col in zip(*table)]
2033     format_str = ' '.join('%-' + compat_str(ml + 1) + 's' for ml in max_lens[:-1]) + '%s'
2034     return '\n'.join(format_str % tuple(row) for row in table)
2035
2036
2037 def _match_one(filter_part, dct):
2038     COMPARISON_OPERATORS = {
2039         '<': operator.lt,
2040         '<=': operator.le,
2041         '>': operator.gt,
2042         '>=': operator.ge,
2043         '=': operator.eq,
2044         '!=': operator.ne,
2045     }
2046     operator_rex = re.compile(r'''(?x)\s*
2047         (?P<key>[a-z_]+)
2048         \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
2049         (?:
2050             (?P<intval>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)|
2051             (?P<strval>(?![0-9.])[a-z0-9A-Z]*)
2052         )
2053         \s*$
2054         ''' % '|'.join(map(re.escape, COMPARISON_OPERATORS.keys())))
2055     m = operator_rex.search(filter_part)
2056     if m:
2057         op = COMPARISON_OPERATORS[m.group('op')]
2058         if m.group('strval') is not None:
2059             if m.group('op') not in ('=', '!='):
2060                 raise ValueError(
2061                     'Operator %s does not support string values!' % m.group('op'))
2062             comparison_value = m.group('strval')
2063         else:
2064             try:
2065                 comparison_value = int(m.group('intval'))
2066             except ValueError:
2067                 comparison_value = parse_filesize(m.group('intval'))
2068                 if comparison_value is None:
2069                     comparison_value = parse_filesize(m.group('intval') + 'B')
2070                 if comparison_value is None:
2071                     raise ValueError(
2072                         'Invalid integer value %r in filter part %r' % (
2073                             m.group('intval'), filter_part))
2074         actual_value = dct.get(m.group('key'))
2075         if actual_value is None:
2076             return m.group('none_inclusive')
2077         return op(actual_value, comparison_value)
2078
2079     UNARY_OPERATORS = {
2080         '': lambda v: v is not None,
2081         '!': lambda v: v is None,
2082     }
2083     operator_rex = re.compile(r'''(?x)\s*
2084         (?P<op>%s)\s*(?P<key>[a-z_]+)
2085         \s*$
2086         ''' % '|'.join(map(re.escape, UNARY_OPERATORS.keys())))
2087     m = operator_rex.search(filter_part)
2088     if m:
2089         op = UNARY_OPERATORS[m.group('op')]
2090         actual_value = dct.get(m.group('key'))
2091         return op(actual_value)
2092
2093     raise ValueError('Invalid filter part %r' % filter_part)
2094
2095
2096 def match_str(filter_str, dct):
2097     """ Filter a dictionary with a simple string syntax. Returns True (=passes filter) or false """
2098
2099     return all(
2100         _match_one(filter_part, dct) for filter_part in filter_str.split('&'))
2101
2102
2103 def match_filter_func(filter_str):
2104     def _match_func(info_dict):
2105         if match_str(filter_str, info_dict):
2106             return None
2107         else:
2108             video_title = info_dict.get('title', info_dict.get('id', 'video'))
2109             return '%s does not pass filter %s, skipping ..' % (video_title, filter_str)
2110     return _match_func
2111
2112
2113 def parse_dfxp_time_expr(time_expr):
2114     if not time_expr:
2115         return
2116
2117     mobj = re.match(r'^(?P<time_offset>\d+(?:\.\d+)?)s?$', time_expr)
2118     if mobj:
2119         return float(mobj.group('time_offset'))
2120
2121     mobj = re.match(r'^(\d+):(\d\d):(\d\d(?:(?:\.|:)\d+)?)$', time_expr)
2122     if mobj:
2123         return 3600 * int(mobj.group(1)) + 60 * int(mobj.group(2)) + float(mobj.group(3).replace(':', '.'))
2124
2125
2126 def srt_subtitles_timecode(seconds):
2127     return '%02d:%02d:%02d,%03d' % (seconds / 3600, (seconds % 3600) / 60, seconds % 60, (seconds % 1) * 1000)
2128
2129
2130 def dfxp2srt(dfxp_data):
2131     _x = functools.partial(xpath_with_ns, ns_map={
2132         'ttml': 'http://www.w3.org/ns/ttml',
2133         'ttaf1': 'http://www.w3.org/2006/10/ttaf1',
2134     })
2135
2136     class TTMLPElementParser(object):
2137         out = ''
2138
2139         def start(self, tag, attrib):
2140             if tag in (_x('ttml:br'), _x('ttaf1:br'), 'br'):
2141                 self.out += '\n'
2142
2143         def end(self, tag):
2144             pass
2145
2146         def data(self, data):
2147             self.out += data
2148
2149         def close(self):
2150             return self.out.strip()
2151
2152     def parse_node(node):
2153         target = TTMLPElementParser()
2154         parser = xml.etree.ElementTree.XMLParser(target=target)
2155         parser.feed(xml.etree.ElementTree.tostring(node))
2156         return parser.close()
2157
2158     dfxp = compat_etree_fromstring(dfxp_data.encode('utf-8'))
2159     out = []
2160     paras = dfxp.findall(_x('.//ttml:p')) or dfxp.findall(_x('.//ttaf1:p')) or dfxp.findall('.//p')
2161
2162     if not paras:
2163         raise ValueError('Invalid dfxp/TTML subtitle')
2164
2165     for para, index in zip(paras, itertools.count(1)):
2166         begin_time = parse_dfxp_time_expr(para.attrib.get('begin'))
2167         end_time = parse_dfxp_time_expr(para.attrib.get('end'))
2168         dur = parse_dfxp_time_expr(para.attrib.get('dur'))
2169         if begin_time is None:
2170             continue
2171         if not end_time:
2172             if not dur:
2173                 continue
2174             end_time = begin_time + dur
2175         out.append('%d\n%s --> %s\n%s\n\n' % (
2176             index,
2177             srt_subtitles_timecode(begin_time),
2178             srt_subtitles_timecode(end_time),
2179             parse_node(para)))
2180
2181     return ''.join(out)
2182
2183
2184 def cli_option(params, command_option, param):
2185     param = params.get(param)
2186     return [command_option, param] if param is not None else []
2187
2188
2189 def cli_bool_option(params, command_option, param, true_value='true', false_value='false', separator=None):
2190     param = params.get(param)
2191     assert isinstance(param, bool)
2192     if separator:
2193         return [command_option + separator + (true_value if param else false_value)]
2194     return [command_option, true_value if param else false_value]
2195
2196
2197 def cli_valueless_option(params, command_option, param, expected_value=True):
2198     param = params.get(param)
2199     return [command_option] if param == expected_value else []
2200
2201
2202 def cli_configuration_args(params, param, default=[]):
2203     ex_args = params.get(param)
2204     if ex_args is None:
2205         return default
2206     assert isinstance(ex_args, list)
2207     return ex_args
2208
2209
2210 class ISO639Utils(object):
2211     # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
2212     _lang_map = {
2213         'aa': 'aar',
2214         'ab': 'abk',
2215         'ae': 'ave',
2216         'af': 'afr',
2217         'ak': 'aka',
2218         'am': 'amh',
2219         'an': 'arg',
2220         'ar': 'ara',
2221         'as': 'asm',
2222         'av': 'ava',
2223         'ay': 'aym',
2224         'az': 'aze',
2225         'ba': 'bak',
2226         'be': 'bel',
2227         'bg': 'bul',
2228         'bh': 'bih',
2229         'bi': 'bis',
2230         'bm': 'bam',
2231         'bn': 'ben',
2232         'bo': 'bod',
2233         'br': 'bre',
2234         'bs': 'bos',
2235         'ca': 'cat',
2236         'ce': 'che',
2237         'ch': 'cha',
2238         'co': 'cos',
2239         'cr': 'cre',
2240         'cs': 'ces',
2241         'cu': 'chu',
2242         'cv': 'chv',
2243         'cy': 'cym',
2244         'da': 'dan',
2245         'de': 'deu',
2246         'dv': 'div',
2247         'dz': 'dzo',
2248         'ee': 'ewe',
2249         'el': 'ell',
2250         'en': 'eng',
2251         'eo': 'epo',
2252         'es': 'spa',
2253         'et': 'est',
2254         'eu': 'eus',
2255         'fa': 'fas',
2256         'ff': 'ful',
2257         'fi': 'fin',
2258         'fj': 'fij',
2259         'fo': 'fao',
2260         'fr': 'fra',
2261         'fy': 'fry',
2262         'ga': 'gle',
2263         'gd': 'gla',
2264         'gl': 'glg',
2265         'gn': 'grn',
2266         'gu': 'guj',
2267         'gv': 'glv',
2268         'ha': 'hau',
2269         'he': 'heb',
2270         'hi': 'hin',
2271         'ho': 'hmo',
2272         'hr': 'hrv',
2273         'ht': 'hat',
2274         'hu': 'hun',
2275         'hy': 'hye',
2276         'hz': 'her',
2277         'ia': 'ina',
2278         'id': 'ind',
2279         'ie': 'ile',
2280         'ig': 'ibo',
2281         'ii': 'iii',
2282         'ik': 'ipk',
2283         'io': 'ido',
2284         'is': 'isl',
2285         'it': 'ita',
2286         'iu': 'iku',
2287         'ja': 'jpn',
2288         'jv': 'jav',
2289         'ka': 'kat',
2290         'kg': 'kon',
2291         'ki': 'kik',
2292         'kj': 'kua',
2293         'kk': 'kaz',
2294         'kl': 'kal',
2295         'km': 'khm',
2296         'kn': 'kan',
2297         'ko': 'kor',
2298         'kr': 'kau',
2299         'ks': 'kas',
2300         'ku': 'kur',
2301         'kv': 'kom',
2302         'kw': 'cor',
2303         'ky': 'kir',
2304         'la': 'lat',
2305         'lb': 'ltz',
2306         'lg': 'lug',
2307         'li': 'lim',
2308         'ln': 'lin',
2309         'lo': 'lao',
2310         'lt': 'lit',
2311         'lu': 'lub',
2312         'lv': 'lav',
2313         'mg': 'mlg',
2314         'mh': 'mah',
2315         'mi': 'mri',
2316         'mk': 'mkd',
2317         'ml': 'mal',
2318         'mn': 'mon',
2319         'mr': 'mar',
2320         'ms': 'msa',
2321         'mt': 'mlt',
2322         'my': 'mya',
2323         'na': 'nau',
2324         'nb': 'nob',
2325         'nd': 'nde',
2326         'ne': 'nep',
2327         'ng': 'ndo',
2328         'nl': 'nld',
2329         'nn': 'nno',
2330         'no': 'nor',
2331         'nr': 'nbl',
2332         'nv': 'nav',
2333         'ny': 'nya',
2334         'oc': 'oci',
2335         'oj': 'oji',
2336         'om': 'orm',
2337         'or': 'ori',
2338         'os': 'oss',
2339         'pa': 'pan',
2340         'pi': 'pli',
2341         'pl': 'pol',
2342         'ps': 'pus',
2343         'pt': 'por',
2344         'qu': 'que',
2345         'rm': 'roh',
2346         'rn': 'run',
2347         'ro': 'ron',
2348         'ru': 'rus',
2349         'rw': 'kin',
2350         'sa': 'san',
2351         'sc': 'srd',
2352         'sd': 'snd',
2353         'se': 'sme',
2354         'sg': 'sag',
2355         'si': 'sin',
2356         'sk': 'slk',
2357         'sl': 'slv',
2358         'sm': 'smo',
2359         'sn': 'sna',
2360         'so': 'som',
2361         'sq': 'sqi',
2362         'sr': 'srp',
2363         'ss': 'ssw',
2364         'st': 'sot',
2365         'su': 'sun',
2366         'sv': 'swe',
2367         'sw': 'swa',
2368         'ta': 'tam',
2369         'te': 'tel',
2370         'tg': 'tgk',
2371         'th': 'tha',
2372         'ti': 'tir',
2373         'tk': 'tuk',
2374         'tl': 'tgl',
2375         'tn': 'tsn',
2376         'to': 'ton',
2377         'tr': 'tur',
2378         'ts': 'tso',
2379         'tt': 'tat',
2380         'tw': 'twi',
2381         'ty': 'tah',
2382         'ug': 'uig',
2383         'uk': 'ukr',
2384         'ur': 'urd',
2385         'uz': 'uzb',
2386         've': 'ven',
2387         'vi': 'vie',
2388         'vo': 'vol',
2389         'wa': 'wln',
2390         'wo': 'wol',
2391         'xh': 'xho',
2392         'yi': 'yid',
2393         'yo': 'yor',
2394         'za': 'zha',
2395         'zh': 'zho',
2396         'zu': 'zul',
2397     }
2398
2399     @classmethod
2400     def short2long(cls, code):
2401         """Convert language code from ISO 639-1 to ISO 639-2/T"""
2402         return cls._lang_map.get(code[:2])
2403
2404     @classmethod
2405     def long2short(cls, code):
2406         """Convert language code from ISO 639-2/T to ISO 639-1"""
2407         for short_name, long_name in cls._lang_map.items():
2408             if long_name == code:
2409                 return short_name
2410
2411
2412 class ISO3166Utils(object):
2413     # From http://data.okfn.org/data/core/country-list
2414     _country_map = {
2415         'AF': 'Afghanistan',
2416         'AX': 'Ã…land Islands',
2417         'AL': 'Albania',
2418         'DZ': 'Algeria',
2419         'AS': 'American Samoa',
2420         'AD': 'Andorra',
2421         'AO': 'Angola',
2422         'AI': 'Anguilla',
2423         'AQ': 'Antarctica',
2424         'AG': 'Antigua and Barbuda',
2425         'AR': 'Argentina',
2426         'AM': 'Armenia',
2427         'AW': 'Aruba',
2428         'AU': 'Australia',
2429         'AT': 'Austria',
2430         'AZ': 'Azerbaijan',
2431         'BS': 'Bahamas',
2432         'BH': 'Bahrain',
2433         'BD': 'Bangladesh',
2434         'BB': 'Barbados',
2435         'BY': 'Belarus',
2436         'BE': 'Belgium',
2437         'BZ': 'Belize',
2438         'BJ': 'Benin',
2439         'BM': 'Bermuda',
2440         'BT': 'Bhutan',
2441         'BO': 'Bolivia, Plurinational State of',
2442         'BQ': 'Bonaire, Sint Eustatius and Saba',
2443         'BA': 'Bosnia and Herzegovina',
2444         'BW': 'Botswana',
2445         'BV': 'Bouvet Island',
2446         'BR': 'Brazil',
2447         'IO': 'British Indian Ocean Territory',
2448         'BN': 'Brunei Darussalam',
2449         'BG': 'Bulgaria',
2450         'BF': 'Burkina Faso',
2451         'BI': 'Burundi',
2452         'KH': 'Cambodia',
2453         'CM': 'Cameroon',
2454         'CA': 'Canada',
2455         'CV': 'Cape Verde',
2456         'KY': 'Cayman Islands',
2457         'CF': 'Central African Republic',
2458         'TD': 'Chad',
2459         'CL': 'Chile',
2460         'CN': 'China',
2461         'CX': 'Christmas Island',
2462         'CC': 'Cocos (Keeling) Islands',
2463         'CO': 'Colombia',
2464         'KM': 'Comoros',
2465         'CG': 'Congo',
2466         'CD': 'Congo, the Democratic Republic of the',
2467         'CK': 'Cook Islands',
2468         'CR': 'Costa Rica',
2469         'CI': 'Côte d\'Ivoire',
2470         'HR': 'Croatia',
2471         'CU': 'Cuba',
2472         'CW': 'Curaçao',
2473         'CY': 'Cyprus',
2474         'CZ': 'Czech Republic',
2475         'DK': 'Denmark',
2476         'DJ': 'Djibouti',
2477         'DM': 'Dominica',
2478         'DO': 'Dominican Republic',
2479         'EC': 'Ecuador',
2480         'EG': 'Egypt',
2481         'SV': 'El Salvador',
2482         'GQ': 'Equatorial Guinea',
2483         'ER': 'Eritrea',
2484         'EE': 'Estonia',
2485         'ET': 'Ethiopia',
2486         'FK': 'Falkland Islands (Malvinas)',
2487         'FO': 'Faroe Islands',
2488         'FJ': 'Fiji',
2489         'FI': 'Finland',
2490         'FR': 'France',
2491         'GF': 'French Guiana',
2492         'PF': 'French Polynesia',
2493         'TF': 'French Southern Territories',
2494         'GA': 'Gabon',
2495         'GM': 'Gambia',
2496         'GE': 'Georgia',
2497         'DE': 'Germany',
2498         'GH': 'Ghana',
2499         'GI': 'Gibraltar',
2500         'GR': 'Greece',
2501         'GL': 'Greenland',
2502         'GD': 'Grenada',
2503         'GP': 'Guadeloupe',
2504         'GU': 'Guam',
2505         'GT': 'Guatemala',
2506         'GG': 'Guernsey',
2507         'GN': 'Guinea',
2508         'GW': 'Guinea-Bissau',
2509         'GY': 'Guyana',
2510         'HT': 'Haiti',
2511         'HM': 'Heard Island and McDonald Islands',
2512         'VA': 'Holy See (Vatican City State)',
2513         'HN': 'Honduras',
2514         'HK': 'Hong Kong',
2515         'HU': 'Hungary',
2516         'IS': 'Iceland',
2517         'IN': 'India',
2518         'ID': 'Indonesia',
2519         'IR': 'Iran, Islamic Republic of',
2520         'IQ': 'Iraq',
2521         'IE': 'Ireland',
2522         'IM': 'Isle of Man',
2523         'IL': 'Israel',
2524         'IT': 'Italy',
2525         'JM': 'Jamaica',
2526         'JP': 'Japan',
2527         'JE': 'Jersey',
2528         'JO': 'Jordan',
2529         'KZ': 'Kazakhstan',
2530         'KE': 'Kenya',
2531         'KI': 'Kiribati',
2532         'KP': 'Korea, Democratic People\'s Republic of',
2533         'KR': 'Korea, Republic of',
2534         'KW': 'Kuwait',
2535         'KG': 'Kyrgyzstan',
2536         'LA': 'Lao People\'s Democratic Republic',
2537         'LV': 'Latvia',
2538         'LB': 'Lebanon',
2539         'LS': 'Lesotho',
2540         'LR': 'Liberia',
2541         'LY': 'Libya',
2542         'LI': 'Liechtenstein',
2543         'LT': 'Lithuania',
2544         'LU': 'Luxembourg',
2545         'MO': 'Macao',
2546         'MK': 'Macedonia, the Former Yugoslav Republic of',
2547         'MG': 'Madagascar',
2548         'MW': 'Malawi',
2549         'MY': 'Malaysia',
2550         'MV': 'Maldives',
2551         'ML': 'Mali',
2552         'MT': 'Malta',
2553         'MH': 'Marshall Islands',
2554         'MQ': 'Martinique',
2555         'MR': 'Mauritania',
2556         'MU': 'Mauritius',
2557         'YT': 'Mayotte',
2558         'MX': 'Mexico',
2559         'FM': 'Micronesia, Federated States of',
2560         'MD': 'Moldova, Republic of',
2561         'MC': 'Monaco',
2562         'MN': 'Mongolia',
2563         'ME': 'Montenegro',
2564         'MS': 'Montserrat',
2565         'MA': 'Morocco',
2566         'MZ': 'Mozambique',
2567         'MM': 'Myanmar',
2568         'NA': 'Namibia',
2569         'NR': 'Nauru',
2570         'NP': 'Nepal',
2571         'NL': 'Netherlands',
2572         'NC': 'New Caledonia',
2573         'NZ': 'New Zealand',
2574         'NI': 'Nicaragua',
2575         'NE': 'Niger',
2576         'NG': 'Nigeria',
2577         'NU': 'Niue',
2578         'NF': 'Norfolk Island',
2579         'MP': 'Northern Mariana Islands',
2580         'NO': 'Norway',
2581         'OM': 'Oman',
2582         'PK': 'Pakistan',
2583         'PW': 'Palau',
2584         'PS': 'Palestine, State of',
2585         'PA': 'Panama',
2586         'PG': 'Papua New Guinea',
2587         'PY': 'Paraguay',
2588         'PE': 'Peru',
2589         'PH': 'Philippines',
2590         'PN': 'Pitcairn',
2591         'PL': 'Poland',
2592         'PT': 'Portugal',
2593         'PR': 'Puerto Rico',
2594         'QA': 'Qatar',
2595         'RE': 'Réunion',
2596         'RO': 'Romania',
2597         'RU': 'Russian Federation',
2598         'RW': 'Rwanda',
2599         'BL': 'Saint Barthélemy',
2600         'SH': 'Saint Helena, Ascension and Tristan da Cunha',
2601         'KN': 'Saint Kitts and Nevis',
2602         'LC': 'Saint Lucia',
2603         'MF': 'Saint Martin (French part)',
2604         'PM': 'Saint Pierre and Miquelon',
2605         'VC': 'Saint Vincent and the Grenadines',
2606         'WS': 'Samoa',
2607         'SM': 'San Marino',
2608         'ST': 'Sao Tome and Principe',
2609         'SA': 'Saudi Arabia',
2610         'SN': 'Senegal',
2611         'RS': 'Serbia',
2612         'SC': 'Seychelles',
2613         'SL': 'Sierra Leone',
2614         'SG': 'Singapore',
2615         'SX': 'Sint Maarten (Dutch part)',
2616         'SK': 'Slovakia',
2617         'SI': 'Slovenia',
2618         'SB': 'Solomon Islands',
2619         'SO': 'Somalia',
2620         'ZA': 'South Africa',
2621         'GS': 'South Georgia and the South Sandwich Islands',
2622         'SS': 'South Sudan',
2623         'ES': 'Spain',
2624         'LK': 'Sri Lanka',
2625         'SD': 'Sudan',
2626         'SR': 'Suriname',
2627         'SJ': 'Svalbard and Jan Mayen',
2628         'SZ': 'Swaziland',
2629         'SE': 'Sweden',
2630         'CH': 'Switzerland',
2631         'SY': 'Syrian Arab Republic',
2632         'TW': 'Taiwan, Province of China',
2633         'TJ': 'Tajikistan',
2634         'TZ': 'Tanzania, United Republic of',
2635         'TH': 'Thailand',
2636         'TL': 'Timor-Leste',
2637         'TG': 'Togo',
2638         'TK': 'Tokelau',
2639         'TO': 'Tonga',
2640         'TT': 'Trinidad and Tobago',
2641         'TN': 'Tunisia',
2642         'TR': 'Turkey',
2643         'TM': 'Turkmenistan',
2644         'TC': 'Turks and Caicos Islands',
2645         'TV': 'Tuvalu',
2646         'UG': 'Uganda',
2647         'UA': 'Ukraine',
2648         'AE': 'United Arab Emirates',
2649         'GB': 'United Kingdom',
2650         'US': 'United States',
2651         'UM': 'United States Minor Outlying Islands',
2652         'UY': 'Uruguay',
2653         'UZ': 'Uzbekistan',
2654         'VU': 'Vanuatu',
2655         'VE': 'Venezuela, Bolivarian Republic of',
2656         'VN': 'Viet Nam',
2657         'VG': 'Virgin Islands, British',
2658         'VI': 'Virgin Islands, U.S.',
2659         'WF': 'Wallis and Futuna',
2660         'EH': 'Western Sahara',
2661         'YE': 'Yemen',
2662         'ZM': 'Zambia',
2663         'ZW': 'Zimbabwe',
2664     }
2665
2666     @classmethod
2667     def short2full(cls, code):
2668         """Convert an ISO 3166-2 country code to the corresponding full name"""
2669         return cls._country_map.get(code.upper())
2670
2671
2672 class PerRequestProxyHandler(compat_urllib_request.ProxyHandler):
2673     def __init__(self, proxies=None):
2674         # Set default handlers
2675         for type in ('http', 'https'):
2676             setattr(self, '%s_open' % type,
2677                     lambda r, proxy='__noproxy__', type=type, meth=self.proxy_open:
2678                         meth(r, proxy, type))
2679         return compat_urllib_request.ProxyHandler.__init__(self, proxies)
2680
2681     def proxy_open(self, req, proxy, type):
2682         req_proxy = req.headers.get('Ytdl-request-proxy')
2683         if req_proxy is not None:
2684             proxy = req_proxy
2685             del req.headers['Ytdl-request-proxy']
2686
2687         if proxy == '__noproxy__':
2688             return None  # No Proxy
2689         return compat_urllib_request.ProxyHandler.proxy_open(
2690             self, req, proxy, type)
2691
2692
2693 def ohdave_rsa_encrypt(data, exponent, modulus):
2694     '''
2695     Implement OHDave's RSA algorithm. See http://www.ohdave.com/rsa/
2696
2697     Input:
2698         data: data to encrypt, bytes-like object
2699         exponent, modulus: parameter e and N of RSA algorithm, both integer
2700     Output: hex string of encrypted data
2701
2702     Limitation: supports one block encryption only
2703     '''
2704
2705     payload = int(binascii.hexlify(data[::-1]), 16)
2706     encrypted = pow(payload, exponent, modulus)
2707     return '%x' % encrypted
2708
2709
2710 def encode_base_n(num, n, table=None):
2711     FULL_TABLE = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
2712     if not table:
2713         table = FULL_TABLE[:n]
2714
2715     if n > len(table):
2716         raise ValueError('base %d exceeds table length %d' % (n, len(table)))
2717
2718     if num == 0:
2719         return table[0]
2720
2721     ret = ''
2722     while num:
2723         ret = table[num % n] + ret
2724         num = num // n
2725     return ret
2726
2727
2728 def decode_packed_codes(code):
2729     mobj = re.search(
2730         r"}\('(.+)',(\d+),(\d+),'([^']+)'\.split\('\|'\)",
2731         code)
2732     obfucasted_code, base, count, symbols = mobj.groups()
2733     base = int(base)
2734     count = int(count)
2735     symbols = symbols.split('|')
2736     symbol_table = {}
2737
2738     while count:
2739         count -= 1
2740         base_n_count = encode_base_n(count, base)
2741         symbol_table[base_n_count] = symbols[count] or base_n_count
2742
2743     return re.sub(
2744         r'\b(\w+)\b', lambda mobj: symbol_table[mobj.group(0)],
2745         obfucasted_code)