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