[utils] Expose PACKED_CODES_RE
[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             stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()
1824     except OSError:
1825         return False
1826     if isinstance(out, bytes):  # Python 2.x
1827         out = out.decode('ascii', 'ignore')
1828     return detect_exe_version(out, version_re, unrecognized)
1829
1830
1831 def detect_exe_version(output, version_re=None, unrecognized='present'):
1832     assert isinstance(output, compat_str)
1833     if version_re is None:
1834         version_re = r'version\s+([-0-9._a-zA-Z]+)'
1835     m = re.search(version_re, output)
1836     if m:
1837         return m.group(1)
1838     else:
1839         return unrecognized
1840
1841
1842 class PagedList(object):
1843     def __len__(self):
1844         # This is only useful for tests
1845         return len(self.getslice())
1846
1847
1848 class OnDemandPagedList(PagedList):
1849     def __init__(self, pagefunc, pagesize, use_cache=False):
1850         self._pagefunc = pagefunc
1851         self._pagesize = pagesize
1852         self._use_cache = use_cache
1853         if use_cache:
1854             self._cache = {}
1855
1856     def getslice(self, start=0, end=None):
1857         res = []
1858         for pagenum in itertools.count(start // self._pagesize):
1859             firstid = pagenum * self._pagesize
1860             nextfirstid = pagenum * self._pagesize + self._pagesize
1861             if start >= nextfirstid:
1862                 continue
1863
1864             page_results = None
1865             if self._use_cache:
1866                 page_results = self._cache.get(pagenum)
1867             if page_results is None:
1868                 page_results = list(self._pagefunc(pagenum))
1869             if self._use_cache:
1870                 self._cache[pagenum] = page_results
1871
1872             startv = (
1873                 start % self._pagesize
1874                 if firstid <= start < nextfirstid
1875                 else 0)
1876
1877             endv = (
1878                 ((end - 1) % self._pagesize) + 1
1879                 if (end is not None and firstid <= end <= nextfirstid)
1880                 else None)
1881
1882             if startv != 0 or endv is not None:
1883                 page_results = page_results[startv:endv]
1884             res.extend(page_results)
1885
1886             # A little optimization - if current page is not "full", ie. does
1887             # not contain page_size videos then we can assume that this page
1888             # is the last one - there are no more ids on further pages -
1889             # i.e. no need to query again.
1890             if len(page_results) + startv < self._pagesize:
1891                 break
1892
1893             # If we got the whole page, but the next page is not interesting,
1894             # break out early as well
1895             if end == nextfirstid:
1896                 break
1897         return res
1898
1899
1900 class InAdvancePagedList(PagedList):
1901     def __init__(self, pagefunc, pagecount, pagesize):
1902         self._pagefunc = pagefunc
1903         self._pagecount = pagecount
1904         self._pagesize = pagesize
1905
1906     def getslice(self, start=0, end=None):
1907         res = []
1908         start_page = start // self._pagesize
1909         end_page = (
1910             self._pagecount if end is None else (end // self._pagesize + 1))
1911         skip_elems = start - start_page * self._pagesize
1912         only_more = None if end is None else end - start
1913         for pagenum in range(start_page, end_page):
1914             page = list(self._pagefunc(pagenum))
1915             if skip_elems:
1916                 page = page[skip_elems:]
1917                 skip_elems = None
1918             if only_more is not None:
1919                 if len(page) < only_more:
1920                     only_more -= len(page)
1921                 else:
1922                     page = page[:only_more]
1923                     res.extend(page)
1924                     break
1925             res.extend(page)
1926         return res
1927
1928
1929 def uppercase_escape(s):
1930     unicode_escape = codecs.getdecoder('unicode_escape')
1931     return re.sub(
1932         r'\\U[0-9a-fA-F]{8}',
1933         lambda m: unicode_escape(m.group(0))[0],
1934         s)
1935
1936
1937 def lowercase_escape(s):
1938     unicode_escape = codecs.getdecoder('unicode_escape')
1939     return re.sub(
1940         r'\\u[0-9a-fA-F]{4}',
1941         lambda m: unicode_escape(m.group(0))[0],
1942         s)
1943
1944
1945 def escape_rfc3986(s):
1946     """Escape non-ASCII characters as suggested by RFC 3986"""
1947     if sys.version_info < (3, 0) and isinstance(s, compat_str):
1948         s = s.encode('utf-8')
1949     return compat_urllib_parse.quote(s, b"%/;:@&=+$,!~*'()?#[]")
1950
1951
1952 def escape_url(url):
1953     """Escape URL as suggested by RFC 3986"""
1954     url_parsed = compat_urllib_parse_urlparse(url)
1955     return url_parsed._replace(
1956         netloc=url_parsed.netloc.encode('idna').decode('ascii'),
1957         path=escape_rfc3986(url_parsed.path),
1958         params=escape_rfc3986(url_parsed.params),
1959         query=escape_rfc3986(url_parsed.query),
1960         fragment=escape_rfc3986(url_parsed.fragment)
1961     ).geturl()
1962
1963
1964 def read_batch_urls(batch_fd):
1965     def fixup(url):
1966         if not isinstance(url, compat_str):
1967             url = url.decode('utf-8', 'replace')
1968         BOM_UTF8 = '\xef\xbb\xbf'
1969         if url.startswith(BOM_UTF8):
1970             url = url[len(BOM_UTF8):]
1971         url = url.strip()
1972         if url.startswith(('#', ';', ']')):
1973             return False
1974         return url
1975
1976     with contextlib.closing(batch_fd) as fd:
1977         return [url for url in map(fixup, fd) if url]
1978
1979
1980 def urlencode_postdata(*args, **kargs):
1981     return compat_urllib_parse_urlencode(*args, **kargs).encode('ascii')
1982
1983
1984 def update_url_query(url, query):
1985     if not query:
1986         return url
1987     parsed_url = compat_urlparse.urlparse(url)
1988     qs = compat_parse_qs(parsed_url.query)
1989     qs.update(query)
1990     return compat_urlparse.urlunparse(parsed_url._replace(
1991         query=compat_urllib_parse_urlencode(qs, True)))
1992
1993
1994 def update_Request(req, url=None, data=None, headers={}, query={}):
1995     req_headers = req.headers.copy()
1996     req_headers.update(headers)
1997     req_data = data or req.data
1998     req_url = update_url_query(url or req.get_full_url(), query)
1999     req_get_method = req.get_method()
2000     if req_get_method == 'HEAD':
2001         req_type = HEADRequest
2002     elif req_get_method == 'PUT':
2003         req_type = PUTRequest
2004     else:
2005         req_type = compat_urllib_request.Request
2006     new_req = req_type(
2007         req_url, data=req_data, headers=req_headers,
2008         origin_req_host=req.origin_req_host, unverifiable=req.unverifiable)
2009     if hasattr(req, 'timeout'):
2010         new_req.timeout = req.timeout
2011     return new_req
2012
2013
2014 def dict_get(d, key_or_keys, default=None, skip_false_values=True):
2015     if isinstance(key_or_keys, (list, tuple)):
2016         for key in key_or_keys:
2017             if key not in d or d[key] is None or skip_false_values and not d[key]:
2018                 continue
2019             return d[key]
2020         return default
2021     return d.get(key_or_keys, default)
2022
2023
2024 def try_get(src, getter, expected_type=None):
2025     try:
2026         v = getter(src)
2027     except (AttributeError, KeyError, TypeError, IndexError):
2028         pass
2029     else:
2030         if expected_type is None or isinstance(v, expected_type):
2031             return v
2032
2033
2034 def encode_compat_str(string, encoding=preferredencoding(), errors='strict'):
2035     return string if isinstance(string, compat_str) else compat_str(string, encoding, errors)
2036
2037
2038 US_RATINGS = {
2039     'G': 0,
2040     'PG': 10,
2041     'PG-13': 13,
2042     'R': 16,
2043     'NC': 18,
2044 }
2045
2046
2047 TV_PARENTAL_GUIDELINES = {
2048     'TV-Y': 0,
2049     'TV-Y7': 7,
2050     'TV-G': 0,
2051     'TV-PG': 0,
2052     'TV-14': 14,
2053     'TV-MA': 17,
2054 }
2055
2056
2057 def parse_age_limit(s):
2058     if type(s) == int:
2059         return s if 0 <= s <= 21 else None
2060     if not isinstance(s, compat_basestring):
2061         return None
2062     m = re.match(r'^(?P<age>\d{1,2})\+?$', s)
2063     if m:
2064         return int(m.group('age'))
2065     if s in US_RATINGS:
2066         return US_RATINGS[s]
2067     return TV_PARENTAL_GUIDELINES.get(s)
2068
2069
2070 def strip_jsonp(code):
2071     return re.sub(
2072         r'(?s)^[a-zA-Z0-9_.$]+\s*\(\s*(.*)\);?\s*?(?://[^\n]*)*$', r'\1', code)
2073
2074
2075 def js_to_json(code):
2076     def fix_kv(m):
2077         v = m.group(0)
2078         if v in ('true', 'false', 'null'):
2079             return v
2080         elif v.startswith('/*') or v == ',':
2081             return ""
2082
2083         if v[0] in ("'", '"'):
2084             v = re.sub(r'(?s)\\.|"', lambda m: {
2085                 '"': '\\"',
2086                 "\\'": "'",
2087                 '\\\n': '',
2088                 '\\x': '\\u00',
2089             }.get(m.group(0), m.group(0)), v[1:-1])
2090
2091         INTEGER_TABLE = (
2092             (r'^(0[xX][0-9a-fA-F]+)\s*:?$', 16),
2093             (r'^(0+[0-7]+)\s*:?$', 8),
2094         )
2095
2096         for regex, base in INTEGER_TABLE:
2097             im = re.match(regex, v)
2098             if im:
2099                 i = int(im.group(1), base)
2100                 return '"%d":' % i if v.endswith(':') else '%d' % i
2101
2102         return '"%s"' % v
2103
2104     return re.sub(r'''(?sx)
2105         "(?:[^"\\]*(?:\\\\|\\['"nurtbfx/\n]))*[^"\\]*"|
2106         '(?:[^'\\]*(?:\\\\|\\['"nurtbfx/\n]))*[^'\\]*'|
2107         /\*.*?\*/|,(?=\s*[\]}])|
2108         [a-zA-Z_][.a-zA-Z_0-9]*|
2109         \b(?:0[xX][0-9a-fA-F]+|0+[0-7]+)(?:\s*:)?|
2110         [0-9]+(?=\s*:)
2111         ''', fix_kv, code)
2112
2113
2114 def qualities(quality_ids):
2115     """ Get a numeric quality value out of a list of possible values """
2116     def q(qid):
2117         try:
2118             return quality_ids.index(qid)
2119         except ValueError:
2120             return -1
2121     return q
2122
2123
2124 DEFAULT_OUTTMPL = '%(title)s-%(id)s.%(ext)s'
2125
2126
2127 def limit_length(s, length):
2128     """ Add ellipses to overly long strings """
2129     if s is None:
2130         return None
2131     ELLIPSES = '...'
2132     if len(s) > length:
2133         return s[:length - len(ELLIPSES)] + ELLIPSES
2134     return s
2135
2136
2137 def version_tuple(v):
2138     return tuple(int(e) for e in re.split(r'[-.]', v))
2139
2140
2141 def is_outdated_version(version, limit, assume_new=True):
2142     if not version:
2143         return not assume_new
2144     try:
2145         return version_tuple(version) < version_tuple(limit)
2146     except ValueError:
2147         return not assume_new
2148
2149
2150 def ytdl_is_updateable():
2151     """ Returns if youtube-dl can be updated with -U """
2152     from zipimport import zipimporter
2153
2154     return isinstance(globals().get('__loader__'), zipimporter) or hasattr(sys, 'frozen')
2155
2156
2157 def args_to_str(args):
2158     # Get a short string representation for a subprocess command
2159     return ' '.join(compat_shlex_quote(a) for a in args)
2160
2161
2162 def error_to_compat_str(err):
2163     err_str = str(err)
2164     # On python 2 error byte string must be decoded with proper
2165     # encoding rather than ascii
2166     if sys.version_info[0] < 3:
2167         err_str = err_str.decode(preferredencoding())
2168     return err_str
2169
2170
2171 def mimetype2ext(mt):
2172     if mt is None:
2173         return None
2174
2175     ext = {
2176         'audio/mp4': 'm4a',
2177         # Per RFC 3003, audio/mpeg can be .mp1, .mp2 or .mp3. Here use .mp3 as
2178         # it's the most popular one
2179         'audio/mpeg': 'mp3',
2180     }.get(mt)
2181     if ext is not None:
2182         return ext
2183
2184     _, _, res = mt.rpartition('/')
2185     res = res.split(';')[0].strip().lower()
2186
2187     return {
2188         '3gpp': '3gp',
2189         'smptett+xml': 'tt',
2190         'srt': 'srt',
2191         'ttaf+xml': 'dfxp',
2192         'ttml+xml': 'ttml',
2193         'vtt': 'vtt',
2194         'x-flv': 'flv',
2195         'x-mp4-fragmented': 'mp4',
2196         'x-ms-wmv': 'wmv',
2197         'mpegurl': 'm3u8',
2198         'x-mpegurl': 'm3u8',
2199         'vnd.apple.mpegurl': 'm3u8',
2200         'dash+xml': 'mpd',
2201         'f4m': 'f4m',
2202         'f4m+xml': 'f4m',
2203         'hds+xml': 'f4m',
2204         'vnd.ms-sstr+xml': 'ism',
2205         'quicktime': 'mov',
2206     }.get(res, res)
2207
2208
2209 def parse_codecs(codecs_str):
2210     # http://tools.ietf.org/html/rfc6381
2211     if not codecs_str:
2212         return {}
2213     splited_codecs = list(filter(None, map(
2214         lambda str: str.strip(), codecs_str.strip().strip(',').split(','))))
2215     vcodec, acodec = None, None
2216     for full_codec in splited_codecs:
2217         codec = full_codec.split('.')[0]
2218         if codec in ('avc1', 'avc2', 'avc3', 'avc4', 'vp9', 'vp8', 'hev1', 'hev2', 'h263', 'h264', 'mp4v'):
2219             if not vcodec:
2220                 vcodec = full_codec
2221         elif codec in ('mp4a', 'opus', 'vorbis', 'mp3', 'aac', 'ac-3'):
2222             if not acodec:
2223                 acodec = full_codec
2224         else:
2225             write_string('WARNING: Unknown codec %s' % full_codec, sys.stderr)
2226     if not vcodec and not acodec:
2227         if len(splited_codecs) == 2:
2228             return {
2229                 'vcodec': vcodec,
2230                 'acodec': acodec,
2231             }
2232         elif len(splited_codecs) == 1:
2233             return {
2234                 'vcodec': 'none',
2235                 'acodec': vcodec,
2236             }
2237     else:
2238         return {
2239             'vcodec': vcodec or 'none',
2240             'acodec': acodec or 'none',
2241         }
2242     return {}
2243
2244
2245 def urlhandle_detect_ext(url_handle):
2246     getheader = url_handle.headers.get
2247
2248     cd = getheader('Content-Disposition')
2249     if cd:
2250         m = re.match(r'attachment;\s*filename="(?P<filename>[^"]+)"', cd)
2251         if m:
2252             e = determine_ext(m.group('filename'), default_ext=None)
2253             if e:
2254                 return e
2255
2256     return mimetype2ext(getheader('Content-Type'))
2257
2258
2259 def encode_data_uri(data, mime_type):
2260     return 'data:%s;base64,%s' % (mime_type, base64.b64encode(data).decode('ascii'))
2261
2262
2263 def age_restricted(content_limit, age_limit):
2264     """ Returns True iff the content should be blocked """
2265
2266     if age_limit is None:  # No limit set
2267         return False
2268     if content_limit is None:
2269         return False  # Content available for everyone
2270     return age_limit < content_limit
2271
2272
2273 def is_html(first_bytes):
2274     """ Detect whether a file contains HTML by examining its first bytes. """
2275
2276     BOMS = [
2277         (b'\xef\xbb\xbf', 'utf-8'),
2278         (b'\x00\x00\xfe\xff', 'utf-32-be'),
2279         (b'\xff\xfe\x00\x00', 'utf-32-le'),
2280         (b'\xff\xfe', 'utf-16-le'),
2281         (b'\xfe\xff', 'utf-16-be'),
2282     ]
2283     for bom, enc in BOMS:
2284         if first_bytes.startswith(bom):
2285             s = first_bytes[len(bom):].decode(enc, 'replace')
2286             break
2287     else:
2288         s = first_bytes.decode('utf-8', 'replace')
2289
2290     return re.match(r'^\s*<', s)
2291
2292
2293 def determine_protocol(info_dict):
2294     protocol = info_dict.get('protocol')
2295     if protocol is not None:
2296         return protocol
2297
2298     url = info_dict['url']
2299     if url.startswith('rtmp'):
2300         return 'rtmp'
2301     elif url.startswith('mms'):
2302         return 'mms'
2303     elif url.startswith('rtsp'):
2304         return 'rtsp'
2305
2306     ext = determine_ext(url)
2307     if ext == 'm3u8':
2308         return 'm3u8'
2309     elif ext == 'f4m':
2310         return 'f4m'
2311
2312     return compat_urllib_parse_urlparse(url).scheme
2313
2314
2315 def render_table(header_row, data):
2316     """ Render a list of rows, each as a list of values """
2317     table = [header_row] + data
2318     max_lens = [max(len(compat_str(v)) for v in col) for col in zip(*table)]
2319     format_str = ' '.join('%-' + compat_str(ml + 1) + 's' for ml in max_lens[:-1]) + '%s'
2320     return '\n'.join(format_str % tuple(row) for row in table)
2321
2322
2323 def _match_one(filter_part, dct):
2324     COMPARISON_OPERATORS = {
2325         '<': operator.lt,
2326         '<=': operator.le,
2327         '>': operator.gt,
2328         '>=': operator.ge,
2329         '=': operator.eq,
2330         '!=': operator.ne,
2331     }
2332     operator_rex = re.compile(r'''(?x)\s*
2333         (?P<key>[a-z_]+)
2334         \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
2335         (?:
2336             (?P<intval>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)|
2337             (?P<strval>(?![0-9.])[a-z0-9A-Z]*)
2338         )
2339         \s*$
2340         ''' % '|'.join(map(re.escape, COMPARISON_OPERATORS.keys())))
2341     m = operator_rex.search(filter_part)
2342     if m:
2343         op = COMPARISON_OPERATORS[m.group('op')]
2344         if m.group('strval') is not None:
2345             if m.group('op') not in ('=', '!='):
2346                 raise ValueError(
2347                     'Operator %s does not support string values!' % m.group('op'))
2348             comparison_value = m.group('strval')
2349         else:
2350             try:
2351                 comparison_value = int(m.group('intval'))
2352             except ValueError:
2353                 comparison_value = parse_filesize(m.group('intval'))
2354                 if comparison_value is None:
2355                     comparison_value = parse_filesize(m.group('intval') + 'B')
2356                 if comparison_value is None:
2357                     raise ValueError(
2358                         'Invalid integer value %r in filter part %r' % (
2359                             m.group('intval'), filter_part))
2360         actual_value = dct.get(m.group('key'))
2361         if actual_value is None:
2362             return m.group('none_inclusive')
2363         return op(actual_value, comparison_value)
2364
2365     UNARY_OPERATORS = {
2366         '': lambda v: v is not None,
2367         '!': lambda v: v is None,
2368     }
2369     operator_rex = re.compile(r'''(?x)\s*
2370         (?P<op>%s)\s*(?P<key>[a-z_]+)
2371         \s*$
2372         ''' % '|'.join(map(re.escape, UNARY_OPERATORS.keys())))
2373     m = operator_rex.search(filter_part)
2374     if m:
2375         op = UNARY_OPERATORS[m.group('op')]
2376         actual_value = dct.get(m.group('key'))
2377         return op(actual_value)
2378
2379     raise ValueError('Invalid filter part %r' % filter_part)
2380
2381
2382 def match_str(filter_str, dct):
2383     """ Filter a dictionary with a simple string syntax. Returns True (=passes filter) or false """
2384
2385     return all(
2386         _match_one(filter_part, dct) for filter_part in filter_str.split('&'))
2387
2388
2389 def match_filter_func(filter_str):
2390     def _match_func(info_dict):
2391         if match_str(filter_str, info_dict):
2392             return None
2393         else:
2394             video_title = info_dict.get('title', info_dict.get('id', 'video'))
2395             return '%s does not pass filter %s, skipping ..' % (video_title, filter_str)
2396     return _match_func
2397
2398
2399 def parse_dfxp_time_expr(time_expr):
2400     if not time_expr:
2401         return
2402
2403     mobj = re.match(r'^(?P<time_offset>\d+(?:\.\d+)?)s?$', time_expr)
2404     if mobj:
2405         return float(mobj.group('time_offset'))
2406
2407     mobj = re.match(r'^(\d+):(\d\d):(\d\d(?:(?:\.|:)\d+)?)$', time_expr)
2408     if mobj:
2409         return 3600 * int(mobj.group(1)) + 60 * int(mobj.group(2)) + float(mobj.group(3).replace(':', '.'))
2410
2411
2412 def srt_subtitles_timecode(seconds):
2413     return '%02d:%02d:%02d,%03d' % (seconds / 3600, (seconds % 3600) / 60, seconds % 60, (seconds % 1) * 1000)
2414
2415
2416 def dfxp2srt(dfxp_data):
2417     _x = functools.partial(xpath_with_ns, ns_map={
2418         'ttml': 'http://www.w3.org/ns/ttml',
2419         'ttaf1': 'http://www.w3.org/2006/10/ttaf1',
2420         'ttaf1_0604': 'http://www.w3.org/2006/04/ttaf1',
2421     })
2422
2423     class TTMLPElementParser(object):
2424         out = ''
2425
2426         def start(self, tag, attrib):
2427             if tag in (_x('ttml:br'), _x('ttaf1:br'), 'br'):
2428                 self.out += '\n'
2429
2430         def end(self, tag):
2431             pass
2432
2433         def data(self, data):
2434             self.out += data
2435
2436         def close(self):
2437             return self.out.strip()
2438
2439     def parse_node(node):
2440         target = TTMLPElementParser()
2441         parser = xml.etree.ElementTree.XMLParser(target=target)
2442         parser.feed(xml.etree.ElementTree.tostring(node))
2443         return parser.close()
2444
2445     dfxp = compat_etree_fromstring(dfxp_data.encode('utf-8'))
2446     out = []
2447     paras = dfxp.findall(_x('.//ttml:p')) or dfxp.findall(_x('.//ttaf1:p')) or dfxp.findall(_x('.//ttaf1_0604:p')) or dfxp.findall('.//p')
2448
2449     if not paras:
2450         raise ValueError('Invalid dfxp/TTML subtitle')
2451
2452     for para, index in zip(paras, itertools.count(1)):
2453         begin_time = parse_dfxp_time_expr(para.attrib.get('begin'))
2454         end_time = parse_dfxp_time_expr(para.attrib.get('end'))
2455         dur = parse_dfxp_time_expr(para.attrib.get('dur'))
2456         if begin_time is None:
2457             continue
2458         if not end_time:
2459             if not dur:
2460                 continue
2461             end_time = begin_time + dur
2462         out.append('%d\n%s --> %s\n%s\n\n' % (
2463             index,
2464             srt_subtitles_timecode(begin_time),
2465             srt_subtitles_timecode(end_time),
2466             parse_node(para)))
2467
2468     return ''.join(out)
2469
2470
2471 def cli_option(params, command_option, param):
2472     param = params.get(param)
2473     if param:
2474         param = compat_str(param)
2475     return [command_option, param] if param is not None else []
2476
2477
2478 def cli_bool_option(params, command_option, param, true_value='true', false_value='false', separator=None):
2479     param = params.get(param)
2480     assert isinstance(param, bool)
2481     if separator:
2482         return [command_option + separator + (true_value if param else false_value)]
2483     return [command_option, true_value if param else false_value]
2484
2485
2486 def cli_valueless_option(params, command_option, param, expected_value=True):
2487     param = params.get(param)
2488     return [command_option] if param == expected_value else []
2489
2490
2491 def cli_configuration_args(params, param, default=[]):
2492     ex_args = params.get(param)
2493     if ex_args is None:
2494         return default
2495     assert isinstance(ex_args, list)
2496     return ex_args
2497
2498
2499 class ISO639Utils(object):
2500     # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
2501     _lang_map = {
2502         'aa': 'aar',
2503         'ab': 'abk',
2504         'ae': 'ave',
2505         'af': 'afr',
2506         'ak': 'aka',
2507         'am': 'amh',
2508         'an': 'arg',
2509         'ar': 'ara',
2510         'as': 'asm',
2511         'av': 'ava',
2512         'ay': 'aym',
2513         'az': 'aze',
2514         'ba': 'bak',
2515         'be': 'bel',
2516         'bg': 'bul',
2517         'bh': 'bih',
2518         'bi': 'bis',
2519         'bm': 'bam',
2520         'bn': 'ben',
2521         'bo': 'bod',
2522         'br': 'bre',
2523         'bs': 'bos',
2524         'ca': 'cat',
2525         'ce': 'che',
2526         'ch': 'cha',
2527         'co': 'cos',
2528         'cr': 'cre',
2529         'cs': 'ces',
2530         'cu': 'chu',
2531         'cv': 'chv',
2532         'cy': 'cym',
2533         'da': 'dan',
2534         'de': 'deu',
2535         'dv': 'div',
2536         'dz': 'dzo',
2537         'ee': 'ewe',
2538         'el': 'ell',
2539         'en': 'eng',
2540         'eo': 'epo',
2541         'es': 'spa',
2542         'et': 'est',
2543         'eu': 'eus',
2544         'fa': 'fas',
2545         'ff': 'ful',
2546         'fi': 'fin',
2547         'fj': 'fij',
2548         'fo': 'fao',
2549         'fr': 'fra',
2550         'fy': 'fry',
2551         'ga': 'gle',
2552         'gd': 'gla',
2553         'gl': 'glg',
2554         'gn': 'grn',
2555         'gu': 'guj',
2556         'gv': 'glv',
2557         'ha': 'hau',
2558         'he': 'heb',
2559         'hi': 'hin',
2560         'ho': 'hmo',
2561         'hr': 'hrv',
2562         'ht': 'hat',
2563         'hu': 'hun',
2564         'hy': 'hye',
2565         'hz': 'her',
2566         'ia': 'ina',
2567         'id': 'ind',
2568         'ie': 'ile',
2569         'ig': 'ibo',
2570         'ii': 'iii',
2571         'ik': 'ipk',
2572         'io': 'ido',
2573         'is': 'isl',
2574         'it': 'ita',
2575         'iu': 'iku',
2576         'ja': 'jpn',
2577         'jv': 'jav',
2578         'ka': 'kat',
2579         'kg': 'kon',
2580         'ki': 'kik',
2581         'kj': 'kua',
2582         'kk': 'kaz',
2583         'kl': 'kal',
2584         'km': 'khm',
2585         'kn': 'kan',
2586         'ko': 'kor',
2587         'kr': 'kau',
2588         'ks': 'kas',
2589         'ku': 'kur',
2590         'kv': 'kom',
2591         'kw': 'cor',
2592         'ky': 'kir',
2593         'la': 'lat',
2594         'lb': 'ltz',
2595         'lg': 'lug',
2596         'li': 'lim',
2597         'ln': 'lin',
2598         'lo': 'lao',
2599         'lt': 'lit',
2600         'lu': 'lub',
2601         'lv': 'lav',
2602         'mg': 'mlg',
2603         'mh': 'mah',
2604         'mi': 'mri',
2605         'mk': 'mkd',
2606         'ml': 'mal',
2607         'mn': 'mon',
2608         'mr': 'mar',
2609         'ms': 'msa',
2610         'mt': 'mlt',
2611         'my': 'mya',
2612         'na': 'nau',
2613         'nb': 'nob',
2614         'nd': 'nde',
2615         'ne': 'nep',
2616         'ng': 'ndo',
2617         'nl': 'nld',
2618         'nn': 'nno',
2619         'no': 'nor',
2620         'nr': 'nbl',
2621         'nv': 'nav',
2622         'ny': 'nya',
2623         'oc': 'oci',
2624         'oj': 'oji',
2625         'om': 'orm',
2626         'or': 'ori',
2627         'os': 'oss',
2628         'pa': 'pan',
2629         'pi': 'pli',
2630         'pl': 'pol',
2631         'ps': 'pus',
2632         'pt': 'por',
2633         'qu': 'que',
2634         'rm': 'roh',
2635         'rn': 'run',
2636         'ro': 'ron',
2637         'ru': 'rus',
2638         'rw': 'kin',
2639         'sa': 'san',
2640         'sc': 'srd',
2641         'sd': 'snd',
2642         'se': 'sme',
2643         'sg': 'sag',
2644         'si': 'sin',
2645         'sk': 'slk',
2646         'sl': 'slv',
2647         'sm': 'smo',
2648         'sn': 'sna',
2649         'so': 'som',
2650         'sq': 'sqi',
2651         'sr': 'srp',
2652         'ss': 'ssw',
2653         'st': 'sot',
2654         'su': 'sun',
2655         'sv': 'swe',
2656         'sw': 'swa',
2657         'ta': 'tam',
2658         'te': 'tel',
2659         'tg': 'tgk',
2660         'th': 'tha',
2661         'ti': 'tir',
2662         'tk': 'tuk',
2663         'tl': 'tgl',
2664         'tn': 'tsn',
2665         'to': 'ton',
2666         'tr': 'tur',
2667         'ts': 'tso',
2668         'tt': 'tat',
2669         'tw': 'twi',
2670         'ty': 'tah',
2671         'ug': 'uig',
2672         'uk': 'ukr',
2673         'ur': 'urd',
2674         'uz': 'uzb',
2675         've': 'ven',
2676         'vi': 'vie',
2677         'vo': 'vol',
2678         'wa': 'wln',
2679         'wo': 'wol',
2680         'xh': 'xho',
2681         'yi': 'yid',
2682         'yo': 'yor',
2683         'za': 'zha',
2684         'zh': 'zho',
2685         'zu': 'zul',
2686     }
2687
2688     @classmethod
2689     def short2long(cls, code):
2690         """Convert language code from ISO 639-1 to ISO 639-2/T"""
2691         return cls._lang_map.get(code[:2])
2692
2693     @classmethod
2694     def long2short(cls, code):
2695         """Convert language code from ISO 639-2/T to ISO 639-1"""
2696         for short_name, long_name in cls._lang_map.items():
2697             if long_name == code:
2698                 return short_name
2699
2700
2701 class ISO3166Utils(object):
2702     # From http://data.okfn.org/data/core/country-list
2703     _country_map = {
2704         'AF': 'Afghanistan',
2705         'AX': 'Åland Islands',
2706         'AL': 'Albania',
2707         'DZ': 'Algeria',
2708         'AS': 'American Samoa',
2709         'AD': 'Andorra',
2710         'AO': 'Angola',
2711         'AI': 'Anguilla',
2712         'AQ': 'Antarctica',
2713         'AG': 'Antigua and Barbuda',
2714         'AR': 'Argentina',
2715         'AM': 'Armenia',
2716         'AW': 'Aruba',
2717         'AU': 'Australia',
2718         'AT': 'Austria',
2719         'AZ': 'Azerbaijan',
2720         'BS': 'Bahamas',
2721         'BH': 'Bahrain',
2722         'BD': 'Bangladesh',
2723         'BB': 'Barbados',
2724         'BY': 'Belarus',
2725         'BE': 'Belgium',
2726         'BZ': 'Belize',
2727         'BJ': 'Benin',
2728         'BM': 'Bermuda',
2729         'BT': 'Bhutan',
2730         'BO': 'Bolivia, Plurinational State of',
2731         'BQ': 'Bonaire, Sint Eustatius and Saba',
2732         'BA': 'Bosnia and Herzegovina',
2733         'BW': 'Botswana',
2734         'BV': 'Bouvet Island',
2735         'BR': 'Brazil',
2736         'IO': 'British Indian Ocean Territory',
2737         'BN': 'Brunei Darussalam',
2738         'BG': 'Bulgaria',
2739         'BF': 'Burkina Faso',
2740         'BI': 'Burundi',
2741         'KH': 'Cambodia',
2742         'CM': 'Cameroon',
2743         'CA': 'Canada',
2744         'CV': 'Cape Verde',
2745         'KY': 'Cayman Islands',
2746         'CF': 'Central African Republic',
2747         'TD': 'Chad',
2748         'CL': 'Chile',
2749         'CN': 'China',
2750         'CX': 'Christmas Island',
2751         'CC': 'Cocos (Keeling) Islands',
2752         'CO': 'Colombia',
2753         'KM': 'Comoros',
2754         'CG': 'Congo',
2755         'CD': 'Congo, the Democratic Republic of the',
2756         'CK': 'Cook Islands',
2757         'CR': 'Costa Rica',
2758         'CI': 'Côte d\'Ivoire',
2759         'HR': 'Croatia',
2760         'CU': 'Cuba',
2761         'CW': 'Curaçao',
2762         'CY': 'Cyprus',
2763         'CZ': 'Czech Republic',
2764         'DK': 'Denmark',
2765         'DJ': 'Djibouti',
2766         'DM': 'Dominica',
2767         'DO': 'Dominican Republic',
2768         'EC': 'Ecuador',
2769         'EG': 'Egypt',
2770         'SV': 'El Salvador',
2771         'GQ': 'Equatorial Guinea',
2772         'ER': 'Eritrea',
2773         'EE': 'Estonia',
2774         'ET': 'Ethiopia',
2775         'FK': 'Falkland Islands (Malvinas)',
2776         'FO': 'Faroe Islands',
2777         'FJ': 'Fiji',
2778         'FI': 'Finland',
2779         'FR': 'France',
2780         'GF': 'French Guiana',
2781         'PF': 'French Polynesia',
2782         'TF': 'French Southern Territories',
2783         'GA': 'Gabon',
2784         'GM': 'Gambia',
2785         'GE': 'Georgia',
2786         'DE': 'Germany',
2787         'GH': 'Ghana',
2788         'GI': 'Gibraltar',
2789         'GR': 'Greece',
2790         'GL': 'Greenland',
2791         'GD': 'Grenada',
2792         'GP': 'Guadeloupe',
2793         'GU': 'Guam',
2794         'GT': 'Guatemala',
2795         'GG': 'Guernsey',
2796         'GN': 'Guinea',
2797         'GW': 'Guinea-Bissau',
2798         'GY': 'Guyana',
2799         'HT': 'Haiti',
2800         'HM': 'Heard Island and McDonald Islands',
2801         'VA': 'Holy See (Vatican City State)',
2802         'HN': 'Honduras',
2803         'HK': 'Hong Kong',
2804         'HU': 'Hungary',
2805         'IS': 'Iceland',
2806         'IN': 'India',
2807         'ID': 'Indonesia',
2808         'IR': 'Iran, Islamic Republic of',
2809         'IQ': 'Iraq',
2810         'IE': 'Ireland',
2811         'IM': 'Isle of Man',
2812         'IL': 'Israel',
2813         'IT': 'Italy',
2814         'JM': 'Jamaica',
2815         'JP': 'Japan',
2816         'JE': 'Jersey',
2817         'JO': 'Jordan',
2818         'KZ': 'Kazakhstan',
2819         'KE': 'Kenya',
2820         'KI': 'Kiribati',
2821         'KP': 'Korea, Democratic People\'s Republic of',
2822         'KR': 'Korea, Republic of',
2823         'KW': 'Kuwait',
2824         'KG': 'Kyrgyzstan',
2825         'LA': 'Lao People\'s Democratic Republic',
2826         'LV': 'Latvia',
2827         'LB': 'Lebanon',
2828         'LS': 'Lesotho',
2829         'LR': 'Liberia',
2830         'LY': 'Libya',
2831         'LI': 'Liechtenstein',
2832         'LT': 'Lithuania',
2833         'LU': 'Luxembourg',
2834         'MO': 'Macao',
2835         'MK': 'Macedonia, the Former Yugoslav Republic of',
2836         'MG': 'Madagascar',
2837         'MW': 'Malawi',
2838         'MY': 'Malaysia',
2839         'MV': 'Maldives',
2840         'ML': 'Mali',
2841         'MT': 'Malta',
2842         'MH': 'Marshall Islands',
2843         'MQ': 'Martinique',
2844         'MR': 'Mauritania',
2845         'MU': 'Mauritius',
2846         'YT': 'Mayotte',
2847         'MX': 'Mexico',
2848         'FM': 'Micronesia, Federated States of',
2849         'MD': 'Moldova, Republic of',
2850         'MC': 'Monaco',
2851         'MN': 'Mongolia',
2852         'ME': 'Montenegro',
2853         'MS': 'Montserrat',
2854         'MA': 'Morocco',
2855         'MZ': 'Mozambique',
2856         'MM': 'Myanmar',
2857         'NA': 'Namibia',
2858         'NR': 'Nauru',
2859         'NP': 'Nepal',
2860         'NL': 'Netherlands',
2861         'NC': 'New Caledonia',
2862         'NZ': 'New Zealand',
2863         'NI': 'Nicaragua',
2864         'NE': 'Niger',
2865         'NG': 'Nigeria',
2866         'NU': 'Niue',
2867         'NF': 'Norfolk Island',
2868         'MP': 'Northern Mariana Islands',
2869         'NO': 'Norway',
2870         'OM': 'Oman',
2871         'PK': 'Pakistan',
2872         'PW': 'Palau',
2873         'PS': 'Palestine, State of',
2874         'PA': 'Panama',
2875         'PG': 'Papua New Guinea',
2876         'PY': 'Paraguay',
2877         'PE': 'Peru',
2878         'PH': 'Philippines',
2879         'PN': 'Pitcairn',
2880         'PL': 'Poland',
2881         'PT': 'Portugal',
2882         'PR': 'Puerto Rico',
2883         'QA': 'Qatar',
2884         'RE': 'Réunion',
2885         'RO': 'Romania',
2886         'RU': 'Russian Federation',
2887         'RW': 'Rwanda',
2888         'BL': 'Saint Barthélemy',
2889         'SH': 'Saint Helena, Ascension and Tristan da Cunha',
2890         'KN': 'Saint Kitts and Nevis',
2891         'LC': 'Saint Lucia',
2892         'MF': 'Saint Martin (French part)',
2893         'PM': 'Saint Pierre and Miquelon',
2894         'VC': 'Saint Vincent and the Grenadines',
2895         'WS': 'Samoa',
2896         'SM': 'San Marino',
2897         'ST': 'Sao Tome and Principe',
2898         'SA': 'Saudi Arabia',
2899         'SN': 'Senegal',
2900         'RS': 'Serbia',
2901         'SC': 'Seychelles',
2902         'SL': 'Sierra Leone',
2903         'SG': 'Singapore',
2904         'SX': 'Sint Maarten (Dutch part)',
2905         'SK': 'Slovakia',
2906         'SI': 'Slovenia',
2907         'SB': 'Solomon Islands',
2908         'SO': 'Somalia',
2909         'ZA': 'South Africa',
2910         'GS': 'South Georgia and the South Sandwich Islands',
2911         'SS': 'South Sudan',
2912         'ES': 'Spain',
2913         'LK': 'Sri Lanka',
2914         'SD': 'Sudan',
2915         'SR': 'Suriname',
2916         'SJ': 'Svalbard and Jan Mayen',
2917         'SZ': 'Swaziland',
2918         'SE': 'Sweden',
2919         'CH': 'Switzerland',
2920         'SY': 'Syrian Arab Republic',
2921         'TW': 'Taiwan, Province of China',
2922         'TJ': 'Tajikistan',
2923         'TZ': 'Tanzania, United Republic of',
2924         'TH': 'Thailand',
2925         'TL': 'Timor-Leste',
2926         'TG': 'Togo',
2927         'TK': 'Tokelau',
2928         'TO': 'Tonga',
2929         'TT': 'Trinidad and Tobago',
2930         'TN': 'Tunisia',
2931         'TR': 'Turkey',
2932         'TM': 'Turkmenistan',
2933         'TC': 'Turks and Caicos Islands',
2934         'TV': 'Tuvalu',
2935         'UG': 'Uganda',
2936         'UA': 'Ukraine',
2937         'AE': 'United Arab Emirates',
2938         'GB': 'United Kingdom',
2939         'US': 'United States',
2940         'UM': 'United States Minor Outlying Islands',
2941         'UY': 'Uruguay',
2942         'UZ': 'Uzbekistan',
2943         'VU': 'Vanuatu',
2944         'VE': 'Venezuela, Bolivarian Republic of',
2945         'VN': 'Viet Nam',
2946         'VG': 'Virgin Islands, British',
2947         'VI': 'Virgin Islands, U.S.',
2948         'WF': 'Wallis and Futuna',
2949         'EH': 'Western Sahara',
2950         'YE': 'Yemen',
2951         'ZM': 'Zambia',
2952         'ZW': 'Zimbabwe',
2953     }
2954
2955     @classmethod
2956     def short2full(cls, code):
2957         """Convert an ISO 3166-2 country code to the corresponding full name"""
2958         return cls._country_map.get(code.upper())
2959
2960
2961 class PerRequestProxyHandler(compat_urllib_request.ProxyHandler):
2962     def __init__(self, proxies=None):
2963         # Set default handlers
2964         for type in ('http', 'https'):
2965             setattr(self, '%s_open' % type,
2966                     lambda r, proxy='__noproxy__', type=type, meth=self.proxy_open:
2967                         meth(r, proxy, type))
2968         return compat_urllib_request.ProxyHandler.__init__(self, proxies)
2969
2970     def proxy_open(self, req, proxy, type):
2971         req_proxy = req.headers.get('Ytdl-request-proxy')
2972         if req_proxy is not None:
2973             proxy = req_proxy
2974             del req.headers['Ytdl-request-proxy']
2975
2976         if proxy == '__noproxy__':
2977             return None  # No Proxy
2978         if compat_urlparse.urlparse(proxy).scheme.lower() in ('socks', 'socks4', 'socks4a', 'socks5'):
2979             req.add_header('Ytdl-socks-proxy', proxy)
2980             # youtube-dl's http/https handlers do wrapping the socket with socks
2981             return None
2982         return compat_urllib_request.ProxyHandler.proxy_open(
2983             self, req, proxy, type)
2984
2985
2986 def ohdave_rsa_encrypt(data, exponent, modulus):
2987     '''
2988     Implement OHDave's RSA algorithm. See http://www.ohdave.com/rsa/
2989
2990     Input:
2991         data: data to encrypt, bytes-like object
2992         exponent, modulus: parameter e and N of RSA algorithm, both integer
2993     Output: hex string of encrypted data
2994
2995     Limitation: supports one block encryption only
2996     '''
2997
2998     payload = int(binascii.hexlify(data[::-1]), 16)
2999     encrypted = pow(payload, exponent, modulus)
3000     return '%x' % encrypted
3001
3002
3003 def encode_base_n(num, n, table=None):
3004     FULL_TABLE = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
3005     if not table:
3006         table = FULL_TABLE[:n]
3007
3008     if n > len(table):
3009         raise ValueError('base %d exceeds table length %d' % (n, len(table)))
3010
3011     if num == 0:
3012         return table[0]
3013
3014     ret = ''
3015     while num:
3016         ret = table[num % n] + ret
3017         num = num // n
3018     return ret
3019
3020
3021 def decode_packed_codes(code):
3022     mobj = re.search(PACKED_CODES_RE, code)
3023     obfucasted_code, base, count, symbols = mobj.groups()
3024     base = int(base)
3025     count = int(count)
3026     symbols = symbols.split('|')
3027     symbol_table = {}
3028
3029     while count:
3030         count -= 1
3031         base_n_count = encode_base_n(count, base)
3032         symbol_table[base_n_count] = symbols[count] or base_n_count
3033
3034     return re.sub(
3035         r'\b(\w+)\b', lambda mobj: symbol_table[mobj.group(0)],
3036         obfucasted_code)
3037
3038
3039 def parse_m3u8_attributes(attrib):
3040     info = {}
3041     for (key, val) in re.findall(r'(?P<key>[A-Z0-9-]+)=(?P<val>"[^"]+"|[^",]+)(?:,|$)', attrib):
3042         if val.startswith('"'):
3043             val = val[1:-1]
3044         info[key] = val
3045     return info
3046
3047
3048 def urshift(val, n):
3049     return val >> n if val >= 0 else (val + 0x100000000) >> n
3050
3051
3052 # Based on png2str() written by @gdkchan and improved by @yokrysty
3053 # Originally posted at https://github.com/rg3/youtube-dl/issues/9706
3054 def decode_png(png_data):
3055     # Reference: https://www.w3.org/TR/PNG/
3056     header = png_data[8:]
3057
3058     if png_data[:8] != b'\x89PNG\x0d\x0a\x1a\x0a' or header[4:8] != b'IHDR':
3059         raise IOError('Not a valid PNG file.')
3060
3061     int_map = {1: '>B', 2: '>H', 4: '>I'}
3062     unpack_integer = lambda x: compat_struct_unpack(int_map[len(x)], x)[0]
3063
3064     chunks = []
3065
3066     while header:
3067         length = unpack_integer(header[:4])
3068         header = header[4:]
3069
3070         chunk_type = header[:4]
3071         header = header[4:]
3072
3073         chunk_data = header[:length]
3074         header = header[length:]
3075
3076         header = header[4:]  # Skip CRC
3077
3078         chunks.append({
3079             'type': chunk_type,
3080             'length': length,
3081             'data': chunk_data
3082         })
3083
3084     ihdr = chunks[0]['data']
3085
3086     width = unpack_integer(ihdr[:4])
3087     height = unpack_integer(ihdr[4:8])
3088
3089     idat = b''
3090
3091     for chunk in chunks:
3092         if chunk['type'] == b'IDAT':
3093             idat += chunk['data']
3094
3095     if not idat:
3096         raise IOError('Unable to read PNG data.')
3097
3098     decompressed_data = bytearray(zlib.decompress(idat))
3099
3100     stride = width * 3
3101     pixels = []
3102
3103     def _get_pixel(idx):
3104         x = idx % stride
3105         y = idx // stride
3106         return pixels[y][x]
3107
3108     for y in range(height):
3109         basePos = y * (1 + stride)
3110         filter_type = decompressed_data[basePos]
3111
3112         current_row = []
3113
3114         pixels.append(current_row)
3115
3116         for x in range(stride):
3117             color = decompressed_data[1 + basePos + x]
3118             basex = y * stride + x
3119             left = 0
3120             up = 0
3121
3122             if x > 2:
3123                 left = _get_pixel(basex - 3)
3124             if y > 0:
3125                 up = _get_pixel(basex - stride)
3126
3127             if filter_type == 1:  # Sub
3128                 color = (color + left) & 0xff
3129             elif filter_type == 2:  # Up
3130                 color = (color + up) & 0xff
3131             elif filter_type == 3:  # Average
3132                 color = (color + ((left + up) >> 1)) & 0xff
3133             elif filter_type == 4:  # Paeth
3134                 a = left
3135                 b = up
3136                 c = 0
3137
3138                 if x > 2 and y > 0:
3139                     c = _get_pixel(basex - stride - 3)
3140
3141                 p = a + b - c
3142
3143                 pa = abs(p - a)
3144                 pb = abs(p - b)
3145                 pc = abs(p - c)
3146
3147                 if pa <= pb and pa <= pc:
3148                     color = (color + a) & 0xff
3149                 elif pb <= pc:
3150                     color = (color + b) & 0xff
3151                 else:
3152                     color = (color + c) & 0xff
3153
3154             current_row.append(color)
3155
3156     return width, height, pixels
3157
3158
3159 def write_xattr(path, key, value):
3160     # This mess below finds the best xattr tool for the job
3161     try:
3162         # try the pyxattr module...
3163         import xattr
3164
3165         if hasattr(xattr, 'set'):  # pyxattr
3166             # Unicode arguments are not supported in python-pyxattr until
3167             # version 0.5.0
3168             # See https://github.com/rg3/youtube-dl/issues/5498
3169             pyxattr_required_version = '0.5.0'
3170             if version_tuple(xattr.__version__) < version_tuple(pyxattr_required_version):
3171                 # TODO: fallback to CLI tools
3172                 raise XAttrUnavailableError(
3173                     'python-pyxattr is detected but is too old. '
3174                     'youtube-dl requires %s or above while your version is %s. '
3175                     'Falling back to other xattr implementations' % (
3176                         pyxattr_required_version, xattr.__version__))
3177
3178             setxattr = xattr.set
3179         else:  # xattr
3180             setxattr = xattr.setxattr
3181
3182         try:
3183             setxattr(path, key, value)
3184         except EnvironmentError as e:
3185             raise XAttrMetadataError(e.errno, e.strerror)
3186
3187     except ImportError:
3188         if compat_os_name == 'nt':
3189             # Write xattrs to NTFS Alternate Data Streams:
3190             # http://en.wikipedia.org/wiki/NTFS#Alternate_data_streams_.28ADS.29
3191             assert ':' not in key
3192             assert os.path.exists(path)
3193
3194             ads_fn = path + ':' + key
3195             try:
3196                 with open(ads_fn, 'wb') as f:
3197                     f.write(value)
3198             except EnvironmentError as e:
3199                 raise XAttrMetadataError(e.errno, e.strerror)
3200         else:
3201             user_has_setfattr = check_executable('setfattr', ['--version'])
3202             user_has_xattr = check_executable('xattr', ['-h'])
3203
3204             if user_has_setfattr or user_has_xattr:
3205
3206                 value = value.decode('utf-8')
3207                 if user_has_setfattr:
3208                     executable = 'setfattr'
3209                     opts = ['-n', key, '-v', value]
3210                 elif user_has_xattr:
3211                     executable = 'xattr'
3212                     opts = ['-w', key, value]
3213
3214                 cmd = ([encodeFilename(executable, True)] +
3215                        [encodeArgument(o) for o in opts] +
3216                        [encodeFilename(path, True)])
3217
3218                 try:
3219                     p = subprocess.Popen(
3220                         cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
3221                 except EnvironmentError as e:
3222                     raise XAttrMetadataError(e.errno, e.strerror)
3223                 stdout, stderr = p.communicate()
3224                 stderr = stderr.decode('utf-8', 'replace')
3225                 if p.returncode != 0:
3226                     raise XAttrMetadataError(p.returncode, stderr)
3227
3228             else:
3229                 # On Unix, and can't find pyxattr, setfattr, or xattr.
3230                 if sys.platform.startswith('linux'):
3231                     raise XAttrUnavailableError(
3232                         "Couldn't find a tool to set the xattrs. "
3233                         "Install either the python 'pyxattr' or 'xattr' "
3234                         "modules, or the GNU 'attr' package "
3235                         "(which contains the 'setfattr' tool).")
3236                 else:
3237                     raise XAttrUnavailableError(
3238                         "Couldn't find a tool to set the xattrs. "
3239                         "Install either the python 'xattr' module, "
3240                         "or the 'xattr' binary.")