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