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