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