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