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