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