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