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