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