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