Merge pull request #8821 from remitamine/list-thumbnails-order
[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 lookup_unit_table(unit_table, s):
1320     units_re = '|'.join(re.escape(u) for u in unit_table)
1321     m = re.match(
1322         r'(?P<num>[0-9]+(?:[,.][0-9]*)?)\s*(?P<unit>%s)' % units_re, s)
1323     if not m:
1324         return None
1325     num_str = m.group('num').replace(',', '.')
1326     mult = unit_table[m.group('unit')]
1327     return int(float(num_str) * mult)
1328
1329
1330 def parse_filesize(s):
1331     if s is None:
1332         return None
1333
1334     # The lower-case forms are of course incorrect and unofficial,
1335     # but we support those too
1336     _UNIT_TABLE = {
1337         'B': 1,
1338         'b': 1,
1339         'KiB': 1024,
1340         'KB': 1000,
1341         'kB': 1024,
1342         'Kb': 1000,
1343         'MiB': 1024 ** 2,
1344         'MB': 1000 ** 2,
1345         'mB': 1024 ** 2,
1346         'Mb': 1000 ** 2,
1347         'GiB': 1024 ** 3,
1348         'GB': 1000 ** 3,
1349         'gB': 1024 ** 3,
1350         'Gb': 1000 ** 3,
1351         'TiB': 1024 ** 4,
1352         'TB': 1000 ** 4,
1353         'tB': 1024 ** 4,
1354         'Tb': 1000 ** 4,
1355         'PiB': 1024 ** 5,
1356         'PB': 1000 ** 5,
1357         'pB': 1024 ** 5,
1358         'Pb': 1000 ** 5,
1359         'EiB': 1024 ** 6,
1360         'EB': 1000 ** 6,
1361         'eB': 1024 ** 6,
1362         'Eb': 1000 ** 6,
1363         'ZiB': 1024 ** 7,
1364         'ZB': 1000 ** 7,
1365         'zB': 1024 ** 7,
1366         'Zb': 1000 ** 7,
1367         'YiB': 1024 ** 8,
1368         'YB': 1000 ** 8,
1369         'yB': 1024 ** 8,
1370         'Yb': 1000 ** 8,
1371     }
1372
1373     return lookup_unit_table(_UNIT_TABLE, s)
1374
1375
1376 def parse_count(s):
1377     if s is None:
1378         return None
1379
1380     s = s.strip()
1381
1382     if re.match(r'^[\d,.]+$', s):
1383         return str_to_int(s)
1384
1385     _UNIT_TABLE = {
1386         'k': 1000,
1387         'K': 1000,
1388         'm': 1000 ** 2,
1389         'M': 1000 ** 2,
1390         'kk': 1000 ** 2,
1391         'KK': 1000 ** 2,
1392     }
1393
1394     return lookup_unit_table(_UNIT_TABLE, s)
1395
1396
1397 def month_by_name(name):
1398     """ Return the number of a month by (locale-independently) English name """
1399
1400     try:
1401         return ENGLISH_MONTH_NAMES.index(name) + 1
1402     except ValueError:
1403         return None
1404
1405
1406 def month_by_abbreviation(abbrev):
1407     """ Return the number of a month by (locale-independently) English
1408         abbreviations """
1409
1410     try:
1411         return [s[:3] for s in ENGLISH_MONTH_NAMES].index(abbrev) + 1
1412     except ValueError:
1413         return None
1414
1415
1416 def fix_xml_ampersands(xml_str):
1417     """Replace all the '&' by '&amp;' in XML"""
1418     return re.sub(
1419         r'&(?!amp;|lt;|gt;|apos;|quot;|#x[0-9a-fA-F]{,4};|#[0-9]{,4};)',
1420         '&amp;',
1421         xml_str)
1422
1423
1424 def setproctitle(title):
1425     assert isinstance(title, compat_str)
1426
1427     # ctypes in Jython is not complete
1428     # http://bugs.jython.org/issue2148
1429     if sys.platform.startswith('java'):
1430         return
1431
1432     try:
1433         libc = ctypes.cdll.LoadLibrary('libc.so.6')
1434     except OSError:
1435         return
1436     title_bytes = title.encode('utf-8')
1437     buf = ctypes.create_string_buffer(len(title_bytes))
1438     buf.value = title_bytes
1439     try:
1440         libc.prctl(15, buf, 0, 0, 0)
1441     except AttributeError:
1442         return  # Strange libc, just skip this
1443
1444
1445 def remove_start(s, start):
1446     if s.startswith(start):
1447         return s[len(start):]
1448     return s
1449
1450
1451 def remove_end(s, end):
1452     if s.endswith(end):
1453         return s[:-len(end)]
1454     return s
1455
1456
1457 def remove_quotes(s):
1458     if s is None or len(s) < 2:
1459         return s
1460     for quote in ('"', "'", ):
1461         if s[0] == quote and s[-1] == quote:
1462             return s[1:-1]
1463     return s
1464
1465
1466 def url_basename(url):
1467     path = compat_urlparse.urlparse(url).path
1468     return path.strip('/').split('/')[-1]
1469
1470
1471 class HEADRequest(compat_urllib_request.Request):
1472     def get_method(self):
1473         return 'HEAD'
1474
1475
1476 def int_or_none(v, scale=1, default=None, get_attr=None, invscale=1):
1477     if get_attr:
1478         if v is not None:
1479             v = getattr(v, get_attr, None)
1480     if v == '':
1481         v = None
1482     if v is None:
1483         return default
1484     try:
1485         return int(v) * invscale // scale
1486     except ValueError:
1487         return default
1488
1489
1490 def str_or_none(v, default=None):
1491     return default if v is None else compat_str(v)
1492
1493
1494 def str_to_int(int_str):
1495     """ A more relaxed version of int_or_none """
1496     if int_str is None:
1497         return None
1498     int_str = re.sub(r'[,\.\+]', '', int_str)
1499     return int(int_str)
1500
1501
1502 def float_or_none(v, scale=1, invscale=1, default=None):
1503     if v is None:
1504         return default
1505     try:
1506         return float(v) * invscale / scale
1507     except ValueError:
1508         return default
1509
1510
1511 def parse_duration(s):
1512     if not isinstance(s, compat_basestring):
1513         return None
1514
1515     s = s.strip()
1516
1517     m = re.match(
1518         r'''(?ix)(?:P?T)?
1519         (?:
1520             (?P<only_mins>[0-9.]+)\s*(?:mins?\.?|minutes?)\s*|
1521             (?P<only_hours>[0-9.]+)\s*(?:hours?)|
1522
1523             \s*(?P<hours_reversed>[0-9]+)\s*(?:[:h]|hours?)\s*(?P<mins_reversed>[0-9]+)\s*(?:[:m]|mins?\.?|minutes?)\s*|
1524             (?:
1525                 (?:
1526                     (?:(?P<days>[0-9]+)\s*(?:[:d]|days?)\s*)?
1527                     (?P<hours>[0-9]+)\s*(?:[:h]|hours?)\s*
1528                 )?
1529                 (?P<mins>[0-9]+)\s*(?:[:m]|mins?|minutes?)\s*
1530             )?
1531             (?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?\s*(?:s|secs?|seconds?)?
1532         )$''', s)
1533     if not m:
1534         return None
1535     res = 0
1536     if m.group('only_mins'):
1537         return float_or_none(m.group('only_mins'), invscale=60)
1538     if m.group('only_hours'):
1539         return float_or_none(m.group('only_hours'), invscale=60 * 60)
1540     if m.group('secs'):
1541         res += int(m.group('secs'))
1542     if m.group('mins_reversed'):
1543         res += int(m.group('mins_reversed')) * 60
1544     if m.group('mins'):
1545         res += int(m.group('mins')) * 60
1546     if m.group('hours'):
1547         res += int(m.group('hours')) * 60 * 60
1548     if m.group('hours_reversed'):
1549         res += int(m.group('hours_reversed')) * 60 * 60
1550     if m.group('days'):
1551         res += int(m.group('days')) * 24 * 60 * 60
1552     if m.group('ms'):
1553         res += float(m.group('ms'))
1554     return res
1555
1556
1557 def prepend_extension(filename, ext, expected_real_ext=None):
1558     name, real_ext = os.path.splitext(filename)
1559     return (
1560         '{0}.{1}{2}'.format(name, ext, real_ext)
1561         if not expected_real_ext or real_ext[1:] == expected_real_ext
1562         else '{0}.{1}'.format(filename, ext))
1563
1564
1565 def replace_extension(filename, ext, expected_real_ext=None):
1566     name, real_ext = os.path.splitext(filename)
1567     return '{0}.{1}'.format(
1568         name if not expected_real_ext or real_ext[1:] == expected_real_ext else filename,
1569         ext)
1570
1571
1572 def check_executable(exe, args=[]):
1573     """ Checks if the given binary is installed somewhere in PATH, and returns its name.
1574     args can be a list of arguments for a short output (like -version) """
1575     try:
1576         subprocess.Popen([exe] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
1577     except OSError:
1578         return False
1579     return exe
1580
1581
1582 def get_exe_version(exe, args=['--version'],
1583                     version_re=None, unrecognized='present'):
1584     """ Returns the version of the specified executable,
1585     or False if the executable is not present """
1586     try:
1587         out, _ = subprocess.Popen(
1588             [encodeArgument(exe)] + args,
1589             stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()
1590     except OSError:
1591         return False
1592     if isinstance(out, bytes):  # Python 2.x
1593         out = out.decode('ascii', 'ignore')
1594     return detect_exe_version(out, version_re, unrecognized)
1595
1596
1597 def detect_exe_version(output, version_re=None, unrecognized='present'):
1598     assert isinstance(output, compat_str)
1599     if version_re is None:
1600         version_re = r'version\s+([-0-9._a-zA-Z]+)'
1601     m = re.search(version_re, output)
1602     if m:
1603         return m.group(1)
1604     else:
1605         return unrecognized
1606
1607
1608 class PagedList(object):
1609     def __len__(self):
1610         # This is only useful for tests
1611         return len(self.getslice())
1612
1613
1614 class OnDemandPagedList(PagedList):
1615     def __init__(self, pagefunc, pagesize, use_cache=False):
1616         self._pagefunc = pagefunc
1617         self._pagesize = pagesize
1618         self._use_cache = use_cache
1619         if use_cache:
1620             self._cache = {}
1621
1622     def getslice(self, start=0, end=None):
1623         res = []
1624         for pagenum in itertools.count(start // self._pagesize):
1625             firstid = pagenum * self._pagesize
1626             nextfirstid = pagenum * self._pagesize + self._pagesize
1627             if start >= nextfirstid:
1628                 continue
1629
1630             page_results = None
1631             if self._use_cache:
1632                 page_results = self._cache.get(pagenum)
1633             if page_results is None:
1634                 page_results = list(self._pagefunc(pagenum))
1635             if self._use_cache:
1636                 self._cache[pagenum] = page_results
1637
1638             startv = (
1639                 start % self._pagesize
1640                 if firstid <= start < nextfirstid
1641                 else 0)
1642
1643             endv = (
1644                 ((end - 1) % self._pagesize) + 1
1645                 if (end is not None and firstid <= end <= nextfirstid)
1646                 else None)
1647
1648             if startv != 0 or endv is not None:
1649                 page_results = page_results[startv:endv]
1650             res.extend(page_results)
1651
1652             # A little optimization - if current page is not "full", ie. does
1653             # not contain page_size videos then we can assume that this page
1654             # is the last one - there are no more ids on further pages -
1655             # i.e. no need to query again.
1656             if len(page_results) + startv < self._pagesize:
1657                 break
1658
1659             # If we got the whole page, but the next page is not interesting,
1660             # break out early as well
1661             if end == nextfirstid:
1662                 break
1663         return res
1664
1665
1666 class InAdvancePagedList(PagedList):
1667     def __init__(self, pagefunc, pagecount, pagesize):
1668         self._pagefunc = pagefunc
1669         self._pagecount = pagecount
1670         self._pagesize = pagesize
1671
1672     def getslice(self, start=0, end=None):
1673         res = []
1674         start_page = start // self._pagesize
1675         end_page = (
1676             self._pagecount if end is None else (end // self._pagesize + 1))
1677         skip_elems = start - start_page * self._pagesize
1678         only_more = None if end is None else end - start
1679         for pagenum in range(start_page, end_page):
1680             page = list(self._pagefunc(pagenum))
1681             if skip_elems:
1682                 page = page[skip_elems:]
1683                 skip_elems = None
1684             if only_more is not None:
1685                 if len(page) < only_more:
1686                     only_more -= len(page)
1687                 else:
1688                     page = page[:only_more]
1689                     res.extend(page)
1690                     break
1691             res.extend(page)
1692         return res
1693
1694
1695 def uppercase_escape(s):
1696     unicode_escape = codecs.getdecoder('unicode_escape')
1697     return re.sub(
1698         r'\\U[0-9a-fA-F]{8}',
1699         lambda m: unicode_escape(m.group(0))[0],
1700         s)
1701
1702
1703 def lowercase_escape(s):
1704     unicode_escape = codecs.getdecoder('unicode_escape')
1705     return re.sub(
1706         r'\\u[0-9a-fA-F]{4}',
1707         lambda m: unicode_escape(m.group(0))[0],
1708         s)
1709
1710
1711 def escape_rfc3986(s):
1712     """Escape non-ASCII characters as suggested by RFC 3986"""
1713     if sys.version_info < (3, 0) and isinstance(s, compat_str):
1714         s = s.encode('utf-8')
1715     return compat_urllib_parse.quote(s, b"%/;:@&=+$,!~*'()?#[]")
1716
1717
1718 def escape_url(url):
1719     """Escape URL as suggested by RFC 3986"""
1720     url_parsed = compat_urllib_parse_urlparse(url)
1721     return url_parsed._replace(
1722         path=escape_rfc3986(url_parsed.path),
1723         params=escape_rfc3986(url_parsed.params),
1724         query=escape_rfc3986(url_parsed.query),
1725         fragment=escape_rfc3986(url_parsed.fragment)
1726     ).geturl()
1727
1728 try:
1729     struct.pack('!I', 0)
1730 except TypeError:
1731     # In Python 2.6 (and some 2.7 versions), struct requires a bytes argument
1732     def struct_pack(spec, *args):
1733         if isinstance(spec, compat_str):
1734             spec = spec.encode('ascii')
1735         return struct.pack(spec, *args)
1736
1737     def struct_unpack(spec, *args):
1738         if isinstance(spec, compat_str):
1739             spec = spec.encode('ascii')
1740         return struct.unpack(spec, *args)
1741 else:
1742     struct_pack = struct.pack
1743     struct_unpack = struct.unpack
1744
1745
1746 def read_batch_urls(batch_fd):
1747     def fixup(url):
1748         if not isinstance(url, compat_str):
1749             url = url.decode('utf-8', 'replace')
1750         BOM_UTF8 = '\xef\xbb\xbf'
1751         if url.startswith(BOM_UTF8):
1752             url = url[len(BOM_UTF8):]
1753         url = url.strip()
1754         if url.startswith(('#', ';', ']')):
1755             return False
1756         return url
1757
1758     with contextlib.closing(batch_fd) as fd:
1759         return [url for url in map(fixup, fd) if url]
1760
1761
1762 def urlencode_postdata(*args, **kargs):
1763     return compat_urllib_parse.urlencode(*args, **kargs).encode('ascii')
1764
1765
1766 def update_url_query(url, query):
1767     parsed_url = compat_urlparse.urlparse(url)
1768     qs = compat_parse_qs(parsed_url.query)
1769     qs.update(query)
1770     qs = encode_dict(qs)
1771     return compat_urlparse.urlunparse(parsed_url._replace(
1772         query=compat_urllib_parse.urlencode(qs, True)))
1773
1774
1775 def encode_dict(d, encoding='utf-8'):
1776     def encode(v):
1777         return v.encode(encoding) if isinstance(v, compat_basestring) else v
1778     return dict((encode(k), encode(v)) for k, v in d.items())
1779
1780
1781 def dict_get(d, key_or_keys, default=None, skip_false_values=True):
1782     if isinstance(key_or_keys, (list, tuple)):
1783         for key in key_or_keys:
1784             if key not in d or d[key] is None or skip_false_values and not d[key]:
1785                 continue
1786             return d[key]
1787         return default
1788     return d.get(key_or_keys, default)
1789
1790
1791 def encode_compat_str(string, encoding=preferredencoding(), errors='strict'):
1792     return string if isinstance(string, compat_str) else compat_str(string, encoding, errors)
1793
1794
1795 US_RATINGS = {
1796     'G': 0,
1797     'PG': 10,
1798     'PG-13': 13,
1799     'R': 16,
1800     'NC': 18,
1801 }
1802
1803
1804 def parse_age_limit(s):
1805     if s is None:
1806         return None
1807     m = re.match(r'^(?P<age>\d{1,2})\+?$', s)
1808     return int(m.group('age')) if m else US_RATINGS.get(s)
1809
1810
1811 def strip_jsonp(code):
1812     return re.sub(
1813         r'(?s)^[a-zA-Z0-9_.]+\s*\(\s*(.*)\);?\s*?(?://[^\n]*)*$', r'\1', code)
1814
1815
1816 def js_to_json(code):
1817     def fix_kv(m):
1818         v = m.group(0)
1819         if v in ('true', 'false', 'null'):
1820             return v
1821         if v.startswith('"'):
1822             v = re.sub(r"\\'", "'", v[1:-1])
1823         elif v.startswith("'"):
1824             v = v[1:-1]
1825             v = re.sub(r"\\\\|\\'|\"", lambda m: {
1826                 '\\\\': '\\\\',
1827                 "\\'": "'",
1828                 '"': '\\"',
1829             }[m.group(0)], v)
1830         return '"%s"' % v
1831
1832     res = re.sub(r'''(?x)
1833         "(?:[^"\\]*(?:\\\\|\\['"nu]))*[^"\\]*"|
1834         '(?:[^'\\]*(?:\\\\|\\['"nu]))*[^'\\]*'|
1835         [a-zA-Z_][.a-zA-Z_0-9]*
1836         ''', fix_kv, code)
1837     res = re.sub(r',(\s*[\]}])', lambda m: m.group(1), res)
1838     return res
1839
1840
1841 def qualities(quality_ids):
1842     """ Get a numeric quality value out of a list of possible values """
1843     def q(qid):
1844         try:
1845             return quality_ids.index(qid)
1846         except ValueError:
1847             return -1
1848     return q
1849
1850
1851 DEFAULT_OUTTMPL = '%(title)s-%(id)s.%(ext)s'
1852
1853
1854 def limit_length(s, length):
1855     """ Add ellipses to overly long strings """
1856     if s is None:
1857         return None
1858     ELLIPSES = '...'
1859     if len(s) > length:
1860         return s[:length - len(ELLIPSES)] + ELLIPSES
1861     return s
1862
1863
1864 def version_tuple(v):
1865     return tuple(int(e) for e in re.split(r'[-.]', v))
1866
1867
1868 def is_outdated_version(version, limit, assume_new=True):
1869     if not version:
1870         return not assume_new
1871     try:
1872         return version_tuple(version) < version_tuple(limit)
1873     except ValueError:
1874         return not assume_new
1875
1876
1877 def ytdl_is_updateable():
1878     """ Returns if youtube-dl can be updated with -U """
1879     from zipimport import zipimporter
1880
1881     return isinstance(globals().get('__loader__'), zipimporter) or hasattr(sys, 'frozen')
1882
1883
1884 def args_to_str(args):
1885     # Get a short string representation for a subprocess command
1886     return ' '.join(shlex_quote(a) for a in args)
1887
1888
1889 def error_to_compat_str(err):
1890     err_str = str(err)
1891     # On python 2 error byte string must be decoded with proper
1892     # encoding rather than ascii
1893     if sys.version_info[0] < 3:
1894         err_str = err_str.decode(preferredencoding())
1895     return err_str
1896
1897
1898 def mimetype2ext(mt):
1899     ext = {
1900         'audio/mp4': 'm4a',
1901     }.get(mt)
1902     if ext is not None:
1903         return ext
1904
1905     _, _, res = mt.rpartition('/')
1906
1907     return {
1908         '3gpp': '3gp',
1909         'smptett+xml': 'tt',
1910         'srt': 'srt',
1911         'ttaf+xml': 'dfxp',
1912         'ttml+xml': 'ttml',
1913         'vtt': 'vtt',
1914         'x-flv': 'flv',
1915         'x-mp4-fragmented': 'mp4',
1916         'x-ms-wmv': 'wmv',
1917     }.get(res, res)
1918
1919
1920 def urlhandle_detect_ext(url_handle):
1921     try:
1922         url_handle.headers
1923         getheader = lambda h: url_handle.headers[h]
1924     except AttributeError:  # Python < 3
1925         getheader = url_handle.info().getheader
1926
1927     cd = getheader('Content-Disposition')
1928     if cd:
1929         m = re.match(r'attachment;\s*filename="(?P<filename>[^"]+)"', cd)
1930         if m:
1931             e = determine_ext(m.group('filename'), default_ext=None)
1932             if e:
1933                 return e
1934
1935     return mimetype2ext(getheader('Content-Type'))
1936
1937
1938 def encode_data_uri(data, mime_type):
1939     return 'data:%s;base64,%s' % (mime_type, base64.b64encode(data).decode('ascii'))
1940
1941
1942 def age_restricted(content_limit, age_limit):
1943     """ Returns True iff the content should be blocked """
1944
1945     if age_limit is None:  # No limit set
1946         return False
1947     if content_limit is None:
1948         return False  # Content available for everyone
1949     return age_limit < content_limit
1950
1951
1952 def is_html(first_bytes):
1953     """ Detect whether a file contains HTML by examining its first bytes. """
1954
1955     BOMS = [
1956         (b'\xef\xbb\xbf', 'utf-8'),
1957         (b'\x00\x00\xfe\xff', 'utf-32-be'),
1958         (b'\xff\xfe\x00\x00', 'utf-32-le'),
1959         (b'\xff\xfe', 'utf-16-le'),
1960         (b'\xfe\xff', 'utf-16-be'),
1961     ]
1962     for bom, enc in BOMS:
1963         if first_bytes.startswith(bom):
1964             s = first_bytes[len(bom):].decode(enc, 'replace')
1965             break
1966     else:
1967         s = first_bytes.decode('utf-8', 'replace')
1968
1969     return re.match(r'^\s*<', s)
1970
1971
1972 def determine_protocol(info_dict):
1973     protocol = info_dict.get('protocol')
1974     if protocol is not None:
1975         return protocol
1976
1977     url = info_dict['url']
1978     if url.startswith('rtmp'):
1979         return 'rtmp'
1980     elif url.startswith('mms'):
1981         return 'mms'
1982     elif url.startswith('rtsp'):
1983         return 'rtsp'
1984
1985     ext = determine_ext(url)
1986     if ext == 'm3u8':
1987         return 'm3u8'
1988     elif ext == 'f4m':
1989         return 'f4m'
1990
1991     return compat_urllib_parse_urlparse(url).scheme
1992
1993
1994 def render_table(header_row, data):
1995     """ Render a list of rows, each as a list of values """
1996     table = [header_row] + data
1997     max_lens = [max(len(compat_str(v)) for v in col) for col in zip(*table)]
1998     format_str = ' '.join('%-' + compat_str(ml + 1) + 's' for ml in max_lens[:-1]) + '%s'
1999     return '\n'.join(format_str % tuple(row) for row in table)
2000
2001
2002 def _match_one(filter_part, dct):
2003     COMPARISON_OPERATORS = {
2004         '<': operator.lt,
2005         '<=': operator.le,
2006         '>': operator.gt,
2007         '>=': operator.ge,
2008         '=': operator.eq,
2009         '!=': operator.ne,
2010     }
2011     operator_rex = re.compile(r'''(?x)\s*
2012         (?P<key>[a-z_]+)
2013         \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
2014         (?:
2015             (?P<intval>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)|
2016             (?P<strval>(?![0-9.])[a-z0-9A-Z]*)
2017         )
2018         \s*$
2019         ''' % '|'.join(map(re.escape, COMPARISON_OPERATORS.keys())))
2020     m = operator_rex.search(filter_part)
2021     if m:
2022         op = COMPARISON_OPERATORS[m.group('op')]
2023         if m.group('strval') is not None:
2024             if m.group('op') not in ('=', '!='):
2025                 raise ValueError(
2026                     'Operator %s does not support string values!' % m.group('op'))
2027             comparison_value = m.group('strval')
2028         else:
2029             try:
2030                 comparison_value = int(m.group('intval'))
2031             except ValueError:
2032                 comparison_value = parse_filesize(m.group('intval'))
2033                 if comparison_value is None:
2034                     comparison_value = parse_filesize(m.group('intval') + 'B')
2035                 if comparison_value is None:
2036                     raise ValueError(
2037                         'Invalid integer value %r in filter part %r' % (
2038                             m.group('intval'), filter_part))
2039         actual_value = dct.get(m.group('key'))
2040         if actual_value is None:
2041             return m.group('none_inclusive')
2042         return op(actual_value, comparison_value)
2043
2044     UNARY_OPERATORS = {
2045         '': lambda v: v is not None,
2046         '!': lambda v: v is None,
2047     }
2048     operator_rex = re.compile(r'''(?x)\s*
2049         (?P<op>%s)\s*(?P<key>[a-z_]+)
2050         \s*$
2051         ''' % '|'.join(map(re.escape, UNARY_OPERATORS.keys())))
2052     m = operator_rex.search(filter_part)
2053     if m:
2054         op = UNARY_OPERATORS[m.group('op')]
2055         actual_value = dct.get(m.group('key'))
2056         return op(actual_value)
2057
2058     raise ValueError('Invalid filter part %r' % filter_part)
2059
2060
2061 def match_str(filter_str, dct):
2062     """ Filter a dictionary with a simple string syntax. Returns True (=passes filter) or false """
2063
2064     return all(
2065         _match_one(filter_part, dct) for filter_part in filter_str.split('&'))
2066
2067
2068 def match_filter_func(filter_str):
2069     def _match_func(info_dict):
2070         if match_str(filter_str, info_dict):
2071             return None
2072         else:
2073             video_title = info_dict.get('title', info_dict.get('id', 'video'))
2074             return '%s does not pass filter %s, skipping ..' % (video_title, filter_str)
2075     return _match_func
2076
2077
2078 def parse_dfxp_time_expr(time_expr):
2079     if not time_expr:
2080         return
2081
2082     mobj = re.match(r'^(?P<time_offset>\d+(?:\.\d+)?)s?$', time_expr)
2083     if mobj:
2084         return float(mobj.group('time_offset'))
2085
2086     mobj = re.match(r'^(\d+):(\d\d):(\d\d(?:(?:\.|:)\d+)?)$', time_expr)
2087     if mobj:
2088         return 3600 * int(mobj.group(1)) + 60 * int(mobj.group(2)) + float(mobj.group(3).replace(':', '.'))
2089
2090
2091 def srt_subtitles_timecode(seconds):
2092     return '%02d:%02d:%02d,%03d' % (seconds / 3600, (seconds % 3600) / 60, seconds % 60, (seconds % 1) * 1000)
2093
2094
2095 def dfxp2srt(dfxp_data):
2096     _x = functools.partial(xpath_with_ns, ns_map={
2097         'ttml': 'http://www.w3.org/ns/ttml',
2098         'ttaf1': 'http://www.w3.org/2006/10/ttaf1',
2099     })
2100
2101     class TTMLPElementParser(object):
2102         out = ''
2103
2104         def start(self, tag, attrib):
2105             if tag in (_x('ttml:br'), _x('ttaf1:br'), 'br'):
2106                 self.out += '\n'
2107
2108         def end(self, tag):
2109             pass
2110
2111         def data(self, data):
2112             self.out += data
2113
2114         def close(self):
2115             return self.out.strip()
2116
2117     def parse_node(node):
2118         target = TTMLPElementParser()
2119         parser = xml.etree.ElementTree.XMLParser(target=target)
2120         parser.feed(xml.etree.ElementTree.tostring(node))
2121         return parser.close()
2122
2123     dfxp = compat_etree_fromstring(dfxp_data.encode('utf-8'))
2124     out = []
2125     paras = dfxp.findall(_x('.//ttml:p')) or dfxp.findall(_x('.//ttaf1:p')) or dfxp.findall('.//p')
2126
2127     if not paras:
2128         raise ValueError('Invalid dfxp/TTML subtitle')
2129
2130     for para, index in zip(paras, itertools.count(1)):
2131         begin_time = parse_dfxp_time_expr(para.attrib.get('begin'))
2132         end_time = parse_dfxp_time_expr(para.attrib.get('end'))
2133         dur = parse_dfxp_time_expr(para.attrib.get('dur'))
2134         if begin_time is None:
2135             continue
2136         if not end_time:
2137             if not dur:
2138                 continue
2139             end_time = begin_time + dur
2140         out.append('%d\n%s --> %s\n%s\n\n' % (
2141             index,
2142             srt_subtitles_timecode(begin_time),
2143             srt_subtitles_timecode(end_time),
2144             parse_node(para)))
2145
2146     return ''.join(out)
2147
2148
2149 def cli_option(params, command_option, param):
2150     param = params.get(param)
2151     return [command_option, param] if param is not None else []
2152
2153
2154 def cli_bool_option(params, command_option, param, true_value='true', false_value='false', separator=None):
2155     param = params.get(param)
2156     assert isinstance(param, bool)
2157     if separator:
2158         return [command_option + separator + (true_value if param else false_value)]
2159     return [command_option, true_value if param else false_value]
2160
2161
2162 def cli_valueless_option(params, command_option, param, expected_value=True):
2163     param = params.get(param)
2164     return [command_option] if param == expected_value else []
2165
2166
2167 def cli_configuration_args(params, param, default=[]):
2168     ex_args = params.get(param)
2169     if ex_args is None:
2170         return default
2171     assert isinstance(ex_args, list)
2172     return ex_args
2173
2174
2175 class ISO639Utils(object):
2176     # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
2177     _lang_map = {
2178         'aa': 'aar',
2179         'ab': 'abk',
2180         'ae': 'ave',
2181         'af': 'afr',
2182         'ak': 'aka',
2183         'am': 'amh',
2184         'an': 'arg',
2185         'ar': 'ara',
2186         'as': 'asm',
2187         'av': 'ava',
2188         'ay': 'aym',
2189         'az': 'aze',
2190         'ba': 'bak',
2191         'be': 'bel',
2192         'bg': 'bul',
2193         'bh': 'bih',
2194         'bi': 'bis',
2195         'bm': 'bam',
2196         'bn': 'ben',
2197         'bo': 'bod',
2198         'br': 'bre',
2199         'bs': 'bos',
2200         'ca': 'cat',
2201         'ce': 'che',
2202         'ch': 'cha',
2203         'co': 'cos',
2204         'cr': 'cre',
2205         'cs': 'ces',
2206         'cu': 'chu',
2207         'cv': 'chv',
2208         'cy': 'cym',
2209         'da': 'dan',
2210         'de': 'deu',
2211         'dv': 'div',
2212         'dz': 'dzo',
2213         'ee': 'ewe',
2214         'el': 'ell',
2215         'en': 'eng',
2216         'eo': 'epo',
2217         'es': 'spa',
2218         'et': 'est',
2219         'eu': 'eus',
2220         'fa': 'fas',
2221         'ff': 'ful',
2222         'fi': 'fin',
2223         'fj': 'fij',
2224         'fo': 'fao',
2225         'fr': 'fra',
2226         'fy': 'fry',
2227         'ga': 'gle',
2228         'gd': 'gla',
2229         'gl': 'glg',
2230         'gn': 'grn',
2231         'gu': 'guj',
2232         'gv': 'glv',
2233         'ha': 'hau',
2234         'he': 'heb',
2235         'hi': 'hin',
2236         'ho': 'hmo',
2237         'hr': 'hrv',
2238         'ht': 'hat',
2239         'hu': 'hun',
2240         'hy': 'hye',
2241         'hz': 'her',
2242         'ia': 'ina',
2243         'id': 'ind',
2244         'ie': 'ile',
2245         'ig': 'ibo',
2246         'ii': 'iii',
2247         'ik': 'ipk',
2248         'io': 'ido',
2249         'is': 'isl',
2250         'it': 'ita',
2251         'iu': 'iku',
2252         'ja': 'jpn',
2253         'jv': 'jav',
2254         'ka': 'kat',
2255         'kg': 'kon',
2256         'ki': 'kik',
2257         'kj': 'kua',
2258         'kk': 'kaz',
2259         'kl': 'kal',
2260         'km': 'khm',
2261         'kn': 'kan',
2262         'ko': 'kor',
2263         'kr': 'kau',
2264         'ks': 'kas',
2265         'ku': 'kur',
2266         'kv': 'kom',
2267         'kw': 'cor',
2268         'ky': 'kir',
2269         'la': 'lat',
2270         'lb': 'ltz',
2271         'lg': 'lug',
2272         'li': 'lim',
2273         'ln': 'lin',
2274         'lo': 'lao',
2275         'lt': 'lit',
2276         'lu': 'lub',
2277         'lv': 'lav',
2278         'mg': 'mlg',
2279         'mh': 'mah',
2280         'mi': 'mri',
2281         'mk': 'mkd',
2282         'ml': 'mal',
2283         'mn': 'mon',
2284         'mr': 'mar',
2285         'ms': 'msa',
2286         'mt': 'mlt',
2287         'my': 'mya',
2288         'na': 'nau',
2289         'nb': 'nob',
2290         'nd': 'nde',
2291         'ne': 'nep',
2292         'ng': 'ndo',
2293         'nl': 'nld',
2294         'nn': 'nno',
2295         'no': 'nor',
2296         'nr': 'nbl',
2297         'nv': 'nav',
2298         'ny': 'nya',
2299         'oc': 'oci',
2300         'oj': 'oji',
2301         'om': 'orm',
2302         'or': 'ori',
2303         'os': 'oss',
2304         'pa': 'pan',
2305         'pi': 'pli',
2306         'pl': 'pol',
2307         'ps': 'pus',
2308         'pt': 'por',
2309         'qu': 'que',
2310         'rm': 'roh',
2311         'rn': 'run',
2312         'ro': 'ron',
2313         'ru': 'rus',
2314         'rw': 'kin',
2315         'sa': 'san',
2316         'sc': 'srd',
2317         'sd': 'snd',
2318         'se': 'sme',
2319         'sg': 'sag',
2320         'si': 'sin',
2321         'sk': 'slk',
2322         'sl': 'slv',
2323         'sm': 'smo',
2324         'sn': 'sna',
2325         'so': 'som',
2326         'sq': 'sqi',
2327         'sr': 'srp',
2328         'ss': 'ssw',
2329         'st': 'sot',
2330         'su': 'sun',
2331         'sv': 'swe',
2332         'sw': 'swa',
2333         'ta': 'tam',
2334         'te': 'tel',
2335         'tg': 'tgk',
2336         'th': 'tha',
2337         'ti': 'tir',
2338         'tk': 'tuk',
2339         'tl': 'tgl',
2340         'tn': 'tsn',
2341         'to': 'ton',
2342         'tr': 'tur',
2343         'ts': 'tso',
2344         'tt': 'tat',
2345         'tw': 'twi',
2346         'ty': 'tah',
2347         'ug': 'uig',
2348         'uk': 'ukr',
2349         'ur': 'urd',
2350         'uz': 'uzb',
2351         've': 'ven',
2352         'vi': 'vie',
2353         'vo': 'vol',
2354         'wa': 'wln',
2355         'wo': 'wol',
2356         'xh': 'xho',
2357         'yi': 'yid',
2358         'yo': 'yor',
2359         'za': 'zha',
2360         'zh': 'zho',
2361         'zu': 'zul',
2362     }
2363
2364     @classmethod
2365     def short2long(cls, code):
2366         """Convert language code from ISO 639-1 to ISO 639-2/T"""
2367         return cls._lang_map.get(code[:2])
2368
2369     @classmethod
2370     def long2short(cls, code):
2371         """Convert language code from ISO 639-2/T to ISO 639-1"""
2372         for short_name, long_name in cls._lang_map.items():
2373             if long_name == code:
2374                 return short_name
2375
2376
2377 class ISO3166Utils(object):
2378     # From http://data.okfn.org/data/core/country-list
2379     _country_map = {
2380         'AF': 'Afghanistan',
2381         'AX': 'Ã…land Islands',
2382         'AL': 'Albania',
2383         'DZ': 'Algeria',
2384         'AS': 'American Samoa',
2385         'AD': 'Andorra',
2386         'AO': 'Angola',
2387         'AI': 'Anguilla',
2388         'AQ': 'Antarctica',
2389         'AG': 'Antigua and Barbuda',
2390         'AR': 'Argentina',
2391         'AM': 'Armenia',
2392         'AW': 'Aruba',
2393         'AU': 'Australia',
2394         'AT': 'Austria',
2395         'AZ': 'Azerbaijan',
2396         'BS': 'Bahamas',
2397         'BH': 'Bahrain',
2398         'BD': 'Bangladesh',
2399         'BB': 'Barbados',
2400         'BY': 'Belarus',
2401         'BE': 'Belgium',
2402         'BZ': 'Belize',
2403         'BJ': 'Benin',
2404         'BM': 'Bermuda',
2405         'BT': 'Bhutan',
2406         'BO': 'Bolivia, Plurinational State of',
2407         'BQ': 'Bonaire, Sint Eustatius and Saba',
2408         'BA': 'Bosnia and Herzegovina',
2409         'BW': 'Botswana',
2410         'BV': 'Bouvet Island',
2411         'BR': 'Brazil',
2412         'IO': 'British Indian Ocean Territory',
2413         'BN': 'Brunei Darussalam',
2414         'BG': 'Bulgaria',
2415         'BF': 'Burkina Faso',
2416         'BI': 'Burundi',
2417         'KH': 'Cambodia',
2418         'CM': 'Cameroon',
2419         'CA': 'Canada',
2420         'CV': 'Cape Verde',
2421         'KY': 'Cayman Islands',
2422         'CF': 'Central African Republic',
2423         'TD': 'Chad',
2424         'CL': 'Chile',
2425         'CN': 'China',
2426         'CX': 'Christmas Island',
2427         'CC': 'Cocos (Keeling) Islands',
2428         'CO': 'Colombia',
2429         'KM': 'Comoros',
2430         'CG': 'Congo',
2431         'CD': 'Congo, the Democratic Republic of the',
2432         'CK': 'Cook Islands',
2433         'CR': 'Costa Rica',
2434         'CI': 'Côte d\'Ivoire',
2435         'HR': 'Croatia',
2436         'CU': 'Cuba',
2437         'CW': 'Curaçao',
2438         'CY': 'Cyprus',
2439         'CZ': 'Czech Republic',
2440         'DK': 'Denmark',
2441         'DJ': 'Djibouti',
2442         'DM': 'Dominica',
2443         'DO': 'Dominican Republic',
2444         'EC': 'Ecuador',
2445         'EG': 'Egypt',
2446         'SV': 'El Salvador',
2447         'GQ': 'Equatorial Guinea',
2448         'ER': 'Eritrea',
2449         'EE': 'Estonia',
2450         'ET': 'Ethiopia',
2451         'FK': 'Falkland Islands (Malvinas)',
2452         'FO': 'Faroe Islands',
2453         'FJ': 'Fiji',
2454         'FI': 'Finland',
2455         'FR': 'France',
2456         'GF': 'French Guiana',
2457         'PF': 'French Polynesia',
2458         'TF': 'French Southern Territories',
2459         'GA': 'Gabon',
2460         'GM': 'Gambia',
2461         'GE': 'Georgia',
2462         'DE': 'Germany',
2463         'GH': 'Ghana',
2464         'GI': 'Gibraltar',
2465         'GR': 'Greece',
2466         'GL': 'Greenland',
2467         'GD': 'Grenada',
2468         'GP': 'Guadeloupe',
2469         'GU': 'Guam',
2470         'GT': 'Guatemala',
2471         'GG': 'Guernsey',
2472         'GN': 'Guinea',
2473         'GW': 'Guinea-Bissau',
2474         'GY': 'Guyana',
2475         'HT': 'Haiti',
2476         'HM': 'Heard Island and McDonald Islands',
2477         'VA': 'Holy See (Vatican City State)',
2478         'HN': 'Honduras',
2479         'HK': 'Hong Kong',
2480         'HU': 'Hungary',
2481         'IS': 'Iceland',
2482         'IN': 'India',
2483         'ID': 'Indonesia',
2484         'IR': 'Iran, Islamic Republic of',
2485         'IQ': 'Iraq',
2486         'IE': 'Ireland',
2487         'IM': 'Isle of Man',
2488         'IL': 'Israel',
2489         'IT': 'Italy',
2490         'JM': 'Jamaica',
2491         'JP': 'Japan',
2492         'JE': 'Jersey',
2493         'JO': 'Jordan',
2494         'KZ': 'Kazakhstan',
2495         'KE': 'Kenya',
2496         'KI': 'Kiribati',
2497         'KP': 'Korea, Democratic People\'s Republic of',
2498         'KR': 'Korea, Republic of',
2499         'KW': 'Kuwait',
2500         'KG': 'Kyrgyzstan',
2501         'LA': 'Lao People\'s Democratic Republic',
2502         'LV': 'Latvia',
2503         'LB': 'Lebanon',
2504         'LS': 'Lesotho',
2505         'LR': 'Liberia',
2506         'LY': 'Libya',
2507         'LI': 'Liechtenstein',
2508         'LT': 'Lithuania',
2509         'LU': 'Luxembourg',
2510         'MO': 'Macao',
2511         'MK': 'Macedonia, the Former Yugoslav Republic of',
2512         'MG': 'Madagascar',
2513         'MW': 'Malawi',
2514         'MY': 'Malaysia',
2515         'MV': 'Maldives',
2516         'ML': 'Mali',
2517         'MT': 'Malta',
2518         'MH': 'Marshall Islands',
2519         'MQ': 'Martinique',
2520         'MR': 'Mauritania',
2521         'MU': 'Mauritius',
2522         'YT': 'Mayotte',
2523         'MX': 'Mexico',
2524         'FM': 'Micronesia, Federated States of',
2525         'MD': 'Moldova, Republic of',
2526         'MC': 'Monaco',
2527         'MN': 'Mongolia',
2528         'ME': 'Montenegro',
2529         'MS': 'Montserrat',
2530         'MA': 'Morocco',
2531         'MZ': 'Mozambique',
2532         'MM': 'Myanmar',
2533         'NA': 'Namibia',
2534         'NR': 'Nauru',
2535         'NP': 'Nepal',
2536         'NL': 'Netherlands',
2537         'NC': 'New Caledonia',
2538         'NZ': 'New Zealand',
2539         'NI': 'Nicaragua',
2540         'NE': 'Niger',
2541         'NG': 'Nigeria',
2542         'NU': 'Niue',
2543         'NF': 'Norfolk Island',
2544         'MP': 'Northern Mariana Islands',
2545         'NO': 'Norway',
2546         'OM': 'Oman',
2547         'PK': 'Pakistan',
2548         'PW': 'Palau',
2549         'PS': 'Palestine, State of',
2550         'PA': 'Panama',
2551         'PG': 'Papua New Guinea',
2552         'PY': 'Paraguay',
2553         'PE': 'Peru',
2554         'PH': 'Philippines',
2555         'PN': 'Pitcairn',
2556         'PL': 'Poland',
2557         'PT': 'Portugal',
2558         'PR': 'Puerto Rico',
2559         'QA': 'Qatar',
2560         'RE': 'Réunion',
2561         'RO': 'Romania',
2562         'RU': 'Russian Federation',
2563         'RW': 'Rwanda',
2564         'BL': 'Saint Barthélemy',
2565         'SH': 'Saint Helena, Ascension and Tristan da Cunha',
2566         'KN': 'Saint Kitts and Nevis',
2567         'LC': 'Saint Lucia',
2568         'MF': 'Saint Martin (French part)',
2569         'PM': 'Saint Pierre and Miquelon',
2570         'VC': 'Saint Vincent and the Grenadines',
2571         'WS': 'Samoa',
2572         'SM': 'San Marino',
2573         'ST': 'Sao Tome and Principe',
2574         'SA': 'Saudi Arabia',
2575         'SN': 'Senegal',
2576         'RS': 'Serbia',
2577         'SC': 'Seychelles',
2578         'SL': 'Sierra Leone',
2579         'SG': 'Singapore',
2580         'SX': 'Sint Maarten (Dutch part)',
2581         'SK': 'Slovakia',
2582         'SI': 'Slovenia',
2583         'SB': 'Solomon Islands',
2584         'SO': 'Somalia',
2585         'ZA': 'South Africa',
2586         'GS': 'South Georgia and the South Sandwich Islands',
2587         'SS': 'South Sudan',
2588         'ES': 'Spain',
2589         'LK': 'Sri Lanka',
2590         'SD': 'Sudan',
2591         'SR': 'Suriname',
2592         'SJ': 'Svalbard and Jan Mayen',
2593         'SZ': 'Swaziland',
2594         'SE': 'Sweden',
2595         'CH': 'Switzerland',
2596         'SY': 'Syrian Arab Republic',
2597         'TW': 'Taiwan, Province of China',
2598         'TJ': 'Tajikistan',
2599         'TZ': 'Tanzania, United Republic of',
2600         'TH': 'Thailand',
2601         'TL': 'Timor-Leste',
2602         'TG': 'Togo',
2603         'TK': 'Tokelau',
2604         'TO': 'Tonga',
2605         'TT': 'Trinidad and Tobago',
2606         'TN': 'Tunisia',
2607         'TR': 'Turkey',
2608         'TM': 'Turkmenistan',
2609         'TC': 'Turks and Caicos Islands',
2610         'TV': 'Tuvalu',
2611         'UG': 'Uganda',
2612         'UA': 'Ukraine',
2613         'AE': 'United Arab Emirates',
2614         'GB': 'United Kingdom',
2615         'US': 'United States',
2616         'UM': 'United States Minor Outlying Islands',
2617         'UY': 'Uruguay',
2618         'UZ': 'Uzbekistan',
2619         'VU': 'Vanuatu',
2620         'VE': 'Venezuela, Bolivarian Republic of',
2621         'VN': 'Viet Nam',
2622         'VG': 'Virgin Islands, British',
2623         'VI': 'Virgin Islands, U.S.',
2624         'WF': 'Wallis and Futuna',
2625         'EH': 'Western Sahara',
2626         'YE': 'Yemen',
2627         'ZM': 'Zambia',
2628         'ZW': 'Zimbabwe',
2629     }
2630
2631     @classmethod
2632     def short2full(cls, code):
2633         """Convert an ISO 3166-2 country code to the corresponding full name"""
2634         return cls._country_map.get(code.upper())
2635
2636
2637 class PerRequestProxyHandler(compat_urllib_request.ProxyHandler):
2638     def __init__(self, proxies=None):
2639         # Set default handlers
2640         for type in ('http', 'https'):
2641             setattr(self, '%s_open' % type,
2642                     lambda r, proxy='__noproxy__', type=type, meth=self.proxy_open:
2643                         meth(r, proxy, type))
2644         return compat_urllib_request.ProxyHandler.__init__(self, proxies)
2645
2646     def proxy_open(self, req, proxy, type):
2647         req_proxy = req.headers.get('Ytdl-request-proxy')
2648         if req_proxy is not None:
2649             proxy = req_proxy
2650             del req.headers['Ytdl-request-proxy']
2651
2652         if proxy == '__noproxy__':
2653             return None  # No Proxy
2654         return compat_urllib_request.ProxyHandler.proxy_open(
2655             self, req, proxy, type)
2656
2657
2658 def ohdave_rsa_encrypt(data, exponent, modulus):
2659     '''
2660     Implement OHDave's RSA algorithm. See http://www.ohdave.com/rsa/
2661
2662     Input:
2663         data: data to encrypt, bytes-like object
2664         exponent, modulus: parameter e and N of RSA algorithm, both integer
2665     Output: hex string of encrypted data
2666
2667     Limitation: supports one block encryption only
2668     '''
2669
2670     payload = int(binascii.hexlify(data[::-1]), 16)
2671     encrypted = pow(payload, exponent, modulus)
2672     return '%x' % encrypted
2673
2674
2675 def encode_base_n(num, n, table=None):
2676     FULL_TABLE = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
2677     if not table:
2678         table = FULL_TABLE[:n]
2679
2680     if n > len(table):
2681         raise ValueError('base %d exceeds table length %d' % (n, len(table)))
2682
2683     if num == 0:
2684         return table[0]
2685
2686     ret = ''
2687     while num:
2688         ret = table[num % n] + ret
2689         num = num // n
2690     return ret
2691
2692
2693 def decode_packed_codes(code):
2694     mobj = re.search(
2695         r"}\('(.+)',(\d+),(\d+),'([^']+)'\.split\('\|'\)",
2696         code)
2697     obfucasted_code, base, count, symbols = mobj.groups()
2698     base = int(base)
2699     count = int(count)
2700     symbols = symbols.split('|')
2701     symbol_table = {}
2702
2703     while count:
2704         count -= 1
2705         base_n_count = encode_base_n(count, base)
2706         symbol_table[base_n_count] = symbols[count] or base_n_count
2707
2708     return re.sub(
2709         r'\b(\w+)\b', lambda mobj: symbol_table[mobj.group(0)],
2710         obfucasted_code)