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