3846dfdcad0d45af18f07eaa8ef50242d2a5abfd
[youtube-dl] / youtube_dl / utils.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import calendar
5 import codecs
6 import contextlib
7 import ctypes
8 import datetime
9 import email.utils
10 import errno
11 import getpass
12 import gzip
13 import itertools
14 import io
15 import json
16 import locale
17 import math
18 import os
19 import pipes
20 import platform
21 import re
22 import ssl
23 import socket
24 import struct
25 import subprocess
26 import sys
27 import tempfile
28 import traceback
29 import xml.etree.ElementTree
30 import zlib
31
32 try:
33     import urllib.request as compat_urllib_request
34 except ImportError: # Python 2
35     import urllib2 as compat_urllib_request
36
37 try:
38     import urllib.error as compat_urllib_error
39 except ImportError: # Python 2
40     import urllib2 as compat_urllib_error
41
42 try:
43     import urllib.parse as compat_urllib_parse
44 except ImportError: # Python 2
45     import urllib as compat_urllib_parse
46
47 try:
48     from urllib.parse import urlparse as compat_urllib_parse_urlparse
49 except ImportError: # Python 2
50     from urlparse import urlparse as compat_urllib_parse_urlparse
51
52 try:
53     import urllib.parse as compat_urlparse
54 except ImportError: # Python 2
55     import urlparse as compat_urlparse
56
57 try:
58     import http.cookiejar as compat_cookiejar
59 except ImportError: # Python 2
60     import cookielib as compat_cookiejar
61
62 try:
63     import html.entities as compat_html_entities
64 except ImportError: # Python 2
65     import htmlentitydefs as compat_html_entities
66
67 try:
68     import html.parser as compat_html_parser
69 except ImportError: # Python 2
70     import HTMLParser as compat_html_parser
71
72 try:
73     import http.client as compat_http_client
74 except ImportError: # Python 2
75     import httplib as compat_http_client
76
77 try:
78     from urllib.error import HTTPError as compat_HTTPError
79 except ImportError:  # Python 2
80     from urllib2 import HTTPError as compat_HTTPError
81
82 try:
83     from urllib.request import urlretrieve as compat_urlretrieve
84 except ImportError:  # Python 2
85     from urllib import urlretrieve as compat_urlretrieve
86
87
88 try:
89     from subprocess import DEVNULL
90     compat_subprocess_get_DEVNULL = lambda: DEVNULL
91 except ImportError:
92     compat_subprocess_get_DEVNULL = lambda: open(os.path.devnull, 'w')
93
94 try:
95     from urllib.parse import unquote as compat_urllib_parse_unquote
96 except ImportError:
97     def compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
98         if string == '':
99             return string
100         res = string.split('%')
101         if len(res) == 1:
102             return string
103         if encoding is None:
104             encoding = 'utf-8'
105         if errors is None:
106             errors = 'replace'
107         # pct_sequence: contiguous sequence of percent-encoded bytes, decoded
108         pct_sequence = b''
109         string = res[0]
110         for item in res[1:]:
111             try:
112                 if not item:
113                     raise ValueError
114                 pct_sequence += item[:2].decode('hex')
115                 rest = item[2:]
116                 if not rest:
117                     # This segment was just a single percent-encoded character.
118                     # May be part of a sequence of code units, so delay decoding.
119                     # (Stored in pct_sequence).
120                     continue
121             except ValueError:
122                 rest = '%' + item
123             # Encountered non-percent-encoded characters. Flush the current
124             # pct_sequence.
125             string += pct_sequence.decode(encoding, errors) + rest
126             pct_sequence = b''
127         if pct_sequence:
128             # Flush the final pct_sequence
129             string += pct_sequence.decode(encoding, errors)
130         return string
131
132
133 try:
134     from urllib.parse import parse_qs as compat_parse_qs
135 except ImportError: # Python 2
136     # HACK: The following is the correct parse_qs implementation from cpython 3's stdlib.
137     # Python 2's version is apparently totally broken
138
139     def _parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
140                 encoding='utf-8', errors='replace'):
141         qs, _coerce_result = qs, unicode
142         pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
143         r = []
144         for name_value in pairs:
145             if not name_value and not strict_parsing:
146                 continue
147             nv = name_value.split('=', 1)
148             if len(nv) != 2:
149                 if strict_parsing:
150                     raise ValueError("bad query field: %r" % (name_value,))
151                 # Handle case of a control-name with no equal sign
152                 if keep_blank_values:
153                     nv.append('')
154                 else:
155                     continue
156             if len(nv[1]) or keep_blank_values:
157                 name = nv[0].replace('+', ' ')
158                 name = compat_urllib_parse_unquote(
159                     name, encoding=encoding, errors=errors)
160                 name = _coerce_result(name)
161                 value = nv[1].replace('+', ' ')
162                 value = compat_urllib_parse_unquote(
163                     value, encoding=encoding, errors=errors)
164                 value = _coerce_result(value)
165                 r.append((name, value))
166         return r
167
168     def compat_parse_qs(qs, keep_blank_values=False, strict_parsing=False,
169                 encoding='utf-8', errors='replace'):
170         parsed_result = {}
171         pairs = _parse_qsl(qs, keep_blank_values, strict_parsing,
172                         encoding=encoding, errors=errors)
173         for name, value in pairs:
174             if name in parsed_result:
175                 parsed_result[name].append(value)
176             else:
177                 parsed_result[name] = [value]
178         return parsed_result
179
180 try:
181     compat_str = unicode # Python 2
182 except NameError:
183     compat_str = str
184
185 try:
186     compat_chr = unichr # Python 2
187 except NameError:
188     compat_chr = chr
189
190 try:
191     from xml.etree.ElementTree import ParseError as compat_xml_parse_error
192 except ImportError:  # Python 2.6
193     from xml.parsers.expat import ExpatError as compat_xml_parse_error
194
195 try:
196     from shlex import quote as shlex_quote
197 except ImportError:  # Python < 3.3
198     def shlex_quote(s):
199         return "'" + s.replace("'", "'\"'\"'") + "'"
200
201
202 def compat_ord(c):
203     if type(c) is int: return c
204     else: return ord(c)
205
206 # This is not clearly defined otherwise
207 compiled_regex_type = type(re.compile(''))
208
209 std_headers = {
210     'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 (Chrome)',
211     'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
212     'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
213     'Accept-Encoding': 'gzip, deflate',
214     'Accept-Language': 'en-us,en;q=0.5',
215 }
216
217 def preferredencoding():
218     """Get preferred encoding.
219
220     Returns the best encoding scheme for the system, based on
221     locale.getpreferredencoding() and some further tweaks.
222     """
223     try:
224         pref = locale.getpreferredencoding()
225         u'TEST'.encode(pref)
226     except:
227         pref = 'UTF-8'
228
229     return pref
230
231 if sys.version_info < (3,0):
232     def compat_print(s):
233         print(s.encode(preferredencoding(), 'xmlcharrefreplace'))
234 else:
235     def compat_print(s):
236         assert type(s) == type(u'')
237         print(s)
238
239
240 def write_json_file(obj, fn):
241     """ Encode obj as JSON and write it to fn, atomically """
242
243     args = {
244         'suffix': '.tmp',
245         'prefix': os.path.basename(fn) + '.',
246         'dir': os.path.dirname(fn),
247         'delete': False,
248     }
249
250     # In Python 2.x, json.dump expects a bytestream.
251     # In Python 3.x, it writes to a character stream
252     if sys.version_info < (3, 0):
253         args['mode'] = 'wb'
254     else:
255         args.update({
256             'mode': 'w',
257             'encoding': 'utf-8',
258         })
259
260     tf = tempfile.NamedTemporaryFile(**args)
261
262     try:
263         with tf:
264             json.dump(obj, tf)
265         os.rename(tf.name, fn)
266     except:
267         try:
268             os.remove(tf.name)
269         except OSError:
270             pass
271         raise
272
273
274 if sys.version_info >= (2, 7):
275     def find_xpath_attr(node, xpath, key, val):
276         """ Find the xpath xpath[@key=val] """
277         assert re.match(r'^[a-zA-Z-]+$', key)
278         assert re.match(r'^[a-zA-Z0-9@\s:._-]*$', val)
279         expr = xpath + u"[@%s='%s']" % (key, val)
280         return node.find(expr)
281 else:
282     def find_xpath_attr(node, xpath, key, val):
283         for f in node.findall(xpath):
284             if f.attrib.get(key) == val:
285                 return f
286         return None
287
288 # On python2.6 the xml.etree.ElementTree.Element methods don't support
289 # the namespace parameter
290 def xpath_with_ns(path, ns_map):
291     components = [c.split(':') for c in path.split('/')]
292     replaced = []
293     for c in components:
294         if len(c) == 1:
295             replaced.append(c[0])
296         else:
297             ns, tag = c
298             replaced.append('{%s}%s' % (ns_map[ns], tag))
299     return '/'.join(replaced)
300
301
302 compat_html_parser.locatestarttagend = re.compile(r"""<[a-zA-Z][-.a-zA-Z0-9:_]*(?:\s+(?:(?<=['"\s])[^\s/>][^\s/=>]*(?:\s*=+\s*(?:'[^']*'|"[^"]*"|(?!['"])[^>\s]*))?\s*)*)?\s*""", re.VERBOSE) # backport bugfix
303 class BaseHTMLParser(compat_html_parser.HTMLParser):
304     def __init(self):
305         compat_html_parser.HTMLParser.__init__(self)
306         self.html = None
307
308     def loads(self, html):
309         self.html = html
310         self.feed(html)
311         self.close()
312
313 class AttrParser(BaseHTMLParser):
314     """Modified HTMLParser that isolates a tag with the specified attribute"""
315     def __init__(self, attribute, value):
316         self.attribute = attribute
317         self.value = value
318         self.result = None
319         self.started = False
320         self.depth = {}
321         self.watch_startpos = False
322         self.error_count = 0
323         BaseHTMLParser.__init__(self)
324
325     def error(self, message):
326         if self.error_count > 10 or self.started:
327             raise compat_html_parser.HTMLParseError(message, self.getpos())
328         self.rawdata = '\n'.join(self.html.split('\n')[self.getpos()[0]:]) # skip one line
329         self.error_count += 1
330         self.goahead(1)
331
332     def handle_starttag(self, tag, attrs):
333         attrs = dict(attrs)
334         if self.started:
335             self.find_startpos(None)
336         if self.attribute in attrs and attrs[self.attribute] == self.value:
337             self.result = [tag]
338             self.started = True
339             self.watch_startpos = True
340         if self.started:
341             if not tag in self.depth: self.depth[tag] = 0
342             self.depth[tag] += 1
343
344     def handle_endtag(self, tag):
345         if self.started:
346             if tag in self.depth: self.depth[tag] -= 1
347             if self.depth[self.result[0]] == 0:
348                 self.started = False
349                 self.result.append(self.getpos())
350
351     def find_startpos(self, x):
352         """Needed to put the start position of the result (self.result[1])
353         after the opening tag with the requested id"""
354         if self.watch_startpos:
355             self.watch_startpos = False
356             self.result.append(self.getpos())
357     handle_entityref = handle_charref = handle_data = handle_comment = \
358     handle_decl = handle_pi = unknown_decl = find_startpos
359
360     def get_result(self):
361         if self.result is None:
362             return None
363         if len(self.result) != 3:
364             return None
365         lines = self.html.split('\n')
366         lines = lines[self.result[1][0]-1:self.result[2][0]]
367         lines[0] = lines[0][self.result[1][1]:]
368         if len(lines) == 1:
369             lines[-1] = lines[-1][:self.result[2][1]-self.result[1][1]]
370         lines[-1] = lines[-1][:self.result[2][1]]
371         return '\n'.join(lines).strip()
372 # Hack for https://github.com/rg3/youtube-dl/issues/662
373 if sys.version_info < (2, 7, 3):
374     AttrParser.parse_endtag = (lambda self, i:
375         i + len("</scr'+'ipt>")
376         if self.rawdata[i:].startswith("</scr'+'ipt>")
377         else compat_html_parser.HTMLParser.parse_endtag(self, i))
378
379 def get_element_by_id(id, html):
380     """Return the content of the tag with the specified ID in the passed HTML document"""
381     return get_element_by_attribute("id", id, html)
382
383 def get_element_by_attribute(attribute, value, html):
384     """Return the content of the tag with the specified attribute in the passed HTML document"""
385     parser = AttrParser(attribute, value)
386     try:
387         parser.loads(html)
388     except compat_html_parser.HTMLParseError:
389         pass
390     return parser.get_result()
391
392 class MetaParser(BaseHTMLParser):
393     """
394     Modified HTMLParser that isolates a meta tag with the specified name 
395     attribute.
396     """
397     def __init__(self, name):
398         BaseHTMLParser.__init__(self)
399         self.name = name
400         self.content = None
401         self.result = None
402
403     def handle_starttag(self, tag, attrs):
404         if tag != 'meta':
405             return
406         attrs = dict(attrs)
407         if attrs.get('name') == self.name:
408             self.result = attrs.get('content')
409
410     def get_result(self):
411         return self.result
412
413 def get_meta_content(name, html):
414     """
415     Return the content attribute from the meta tag with the given name attribute.
416     """
417     parser = MetaParser(name)
418     try:
419         parser.loads(html)
420     except compat_html_parser.HTMLParseError:
421         pass
422     return parser.get_result()
423
424
425 def clean_html(html):
426     """Clean an HTML snippet into a readable string"""
427     # Newline vs <br />
428     html = html.replace('\n', ' ')
429     html = re.sub(r'\s*<\s*br\s*/?\s*>\s*', '\n', html)
430     html = re.sub(r'<\s*/\s*p\s*>\s*<\s*p[^>]*>', '\n', html)
431     # Strip html tags
432     html = re.sub('<.*?>', '', html)
433     # Replace html entities
434     html = unescapeHTML(html)
435     return html.strip()
436
437
438 def sanitize_open(filename, open_mode):
439     """Try to open the given filename, and slightly tweak it if this fails.
440
441     Attempts to open the given filename. If this fails, it tries to change
442     the filename slightly, step by step, until it's either able to open it
443     or it fails and raises a final exception, like the standard open()
444     function.
445
446     It returns the tuple (stream, definitive_file_name).
447     """
448     try:
449         if filename == u'-':
450             if sys.platform == 'win32':
451                 import msvcrt
452                 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
453             return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename)
454         stream = open(encodeFilename(filename), open_mode)
455         return (stream, filename)
456     except (IOError, OSError) as err:
457         if err.errno in (errno.EACCES,):
458             raise
459
460         # In case of error, try to remove win32 forbidden chars
461         alt_filename = os.path.join(
462                         re.sub(u'[/<>:"\\|\\\\?\\*]', u'#', path_part)
463                         for path_part in os.path.split(filename)
464                        )
465         if alt_filename == filename:
466             raise
467         else:
468             # An exception here should be caught in the caller
469             stream = open(encodeFilename(filename), open_mode)
470             return (stream, alt_filename)
471
472
473 def timeconvert(timestr):
474     """Convert RFC 2822 defined time string into system timestamp"""
475     timestamp = None
476     timetuple = email.utils.parsedate_tz(timestr)
477     if timetuple is not None:
478         timestamp = email.utils.mktime_tz(timetuple)
479     return timestamp
480
481 def sanitize_filename(s, restricted=False, is_id=False):
482     """Sanitizes a string so it could be used as part of a filename.
483     If restricted is set, use a stricter subset of allowed characters.
484     Set is_id if this is not an arbitrary string, but an ID that should be kept if possible
485     """
486     def replace_insane(char):
487         if char == '?' or ord(char) < 32 or ord(char) == 127:
488             return ''
489         elif char == '"':
490             return '' if restricted else '\''
491         elif char == ':':
492             return '_-' if restricted else ' -'
493         elif char in '\\/|*<>':
494             return '_'
495         if restricted and (char in '!&\'()[]{}$;`^,#' or char.isspace()):
496             return '_'
497         if restricted and ord(char) > 127:
498             return '_'
499         return char
500
501     result = u''.join(map(replace_insane, s))
502     if not is_id:
503         while '__' in result:
504             result = result.replace('__', '_')
505         result = result.strip('_')
506         # Common case of "Foreign band name - English song title"
507         if restricted and result.startswith('-_'):
508             result = result[2:]
509         if not result:
510             result = '_'
511     return result
512
513 def orderedSet(iterable):
514     """ Remove all duplicates from the input iterable """
515     res = []
516     for el in iterable:
517         if el not in res:
518             res.append(el)
519     return res
520
521
522 def _htmlentity_transform(entity):
523     """Transforms an HTML entity to a character."""
524     # Known non-numeric HTML entity
525     if entity in compat_html_entities.name2codepoint:
526         return compat_chr(compat_html_entities.name2codepoint[entity])
527
528     mobj = re.match(r'#(x?[0-9]+)', entity)
529     if mobj is not None:
530         numstr = mobj.group(1)
531         if numstr.startswith(u'x'):
532             base = 16
533             numstr = u'0%s' % numstr
534         else:
535             base = 10
536         return compat_chr(int(numstr, base))
537
538     # Unknown entity in name, return its literal representation
539     return (u'&%s;' % entity)
540
541
542 def unescapeHTML(s):
543     if s is None:
544         return None
545     assert type(s) == compat_str
546
547     return re.sub(
548         r'&([^;]+);', lambda m: _htmlentity_transform(m.group(1)), s)
549
550
551 def encodeFilename(s, for_subprocess=False):
552     """
553     @param s The name of the file
554     """
555
556     assert type(s) == compat_str
557
558     # Python 3 has a Unicode API
559     if sys.version_info >= (3, 0):
560         return s
561
562     if sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
563         # Pass u'' directly to use Unicode APIs on Windows 2000 and up
564         # (Detecting Windows NT 4 is tricky because 'major >= 4' would
565         # match Windows 9x series as well. Besides, NT 4 is obsolete.)
566         if not for_subprocess:
567             return s
568         else:
569             # For subprocess calls, encode with locale encoding
570             # Refer to http://stackoverflow.com/a/9951851/35070
571             encoding = preferredencoding()
572     else:
573         encoding = sys.getfilesystemencoding()
574     if encoding is None:
575         encoding = 'utf-8'
576     return s.encode(encoding, 'ignore')
577
578
579 def encodeArgument(s):
580     if not isinstance(s, compat_str):
581         # Legacy code that uses byte strings
582         # Uncomment the following line after fixing all post processors
583         #assert False, 'Internal error: %r should be of type %r, is %r' % (s, compat_str, type(s))
584         s = s.decode('ascii')
585     return encodeFilename(s, True)
586
587
588 def decodeOption(optval):
589     if optval is None:
590         return optval
591     if isinstance(optval, bytes):
592         optval = optval.decode(preferredencoding())
593
594     assert isinstance(optval, compat_str)
595     return optval
596
597 def formatSeconds(secs):
598     if secs > 3600:
599         return '%d:%02d:%02d' % (secs // 3600, (secs % 3600) // 60, secs % 60)
600     elif secs > 60:
601         return '%d:%02d' % (secs // 60, secs % 60)
602     else:
603         return '%d' % secs
604
605
606 def make_HTTPS_handler(opts_no_check_certificate, **kwargs):
607     if sys.version_info < (3, 2):
608         import httplib
609
610         class HTTPSConnectionV3(httplib.HTTPSConnection):
611             def __init__(self, *args, **kwargs):
612                 httplib.HTTPSConnection.__init__(self, *args, **kwargs)
613
614             def connect(self):
615                 sock = socket.create_connection((self.host, self.port), self.timeout)
616                 if getattr(self, '_tunnel_host', False):
617                     self.sock = sock
618                     self._tunnel()
619                 try:
620                     self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version=ssl.PROTOCOL_SSLv3)
621                 except ssl.SSLError:
622                     self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version=ssl.PROTOCOL_SSLv23)
623
624         class HTTPSHandlerV3(compat_urllib_request.HTTPSHandler):
625             def https_open(self, req):
626                 return self.do_open(HTTPSConnectionV3, req)
627         return HTTPSHandlerV3(**kwargs)
628     else:
629         context = ssl.SSLContext(ssl.PROTOCOL_SSLv3)
630         context.verify_mode = (ssl.CERT_NONE
631                                if opts_no_check_certificate
632                                else ssl.CERT_REQUIRED)
633         context.set_default_verify_paths()
634         try:
635             context.load_default_certs()
636         except AttributeError:
637             pass  # Python < 3.4
638         return compat_urllib_request.HTTPSHandler(context=context, **kwargs)
639
640 class ExtractorError(Exception):
641     """Error during info extraction."""
642     def __init__(self, msg, tb=None, expected=False, cause=None, video_id=None):
643         """ tb, if given, is the original traceback (so that it can be printed out).
644         If expected is set, this is a normal error message and most likely not a bug in youtube-dl.
645         """
646
647         if sys.exc_info()[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError):
648             expected = True
649         if video_id is not None:
650             msg = video_id + ': ' + msg
651         if not expected:
652             msg = msg + u'; please report this issue on https://yt-dl.org/bug . Be sure to call youtube-dl with the --verbose flag and include its complete output. Make sure you are using the latest version; type  youtube-dl -U  to update.'
653         super(ExtractorError, self).__init__(msg)
654
655         self.traceback = tb
656         self.exc_info = sys.exc_info()  # preserve original exception
657         self.cause = cause
658         self.video_id = video_id
659
660     def format_traceback(self):
661         if self.traceback is None:
662             return None
663         return u''.join(traceback.format_tb(self.traceback))
664
665
666 class RegexNotFoundError(ExtractorError):
667     """Error when a regex didn't match"""
668     pass
669
670
671 class DownloadError(Exception):
672     """Download Error exception.
673
674     This exception may be thrown by FileDownloader objects if they are not
675     configured to continue on errors. They will contain the appropriate
676     error message.
677     """
678     def __init__(self, msg, exc_info=None):
679         """ exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """
680         super(DownloadError, self).__init__(msg)
681         self.exc_info = exc_info
682
683
684 class SameFileError(Exception):
685     """Same File exception.
686
687     This exception will be thrown by FileDownloader objects if they detect
688     multiple files would have to be downloaded to the same file on disk.
689     """
690     pass
691
692
693 class PostProcessingError(Exception):
694     """Post Processing exception.
695
696     This exception may be raised by PostProcessor's .run() method to
697     indicate an error in the postprocessing task.
698     """
699     def __init__(self, msg):
700         self.msg = msg
701
702 class MaxDownloadsReached(Exception):
703     """ --max-downloads limit has been reached. """
704     pass
705
706
707 class UnavailableVideoError(Exception):
708     """Unavailable Format exception.
709
710     This exception will be thrown when a video is requested
711     in a format that is not available for that video.
712     """
713     pass
714
715
716 class ContentTooShortError(Exception):
717     """Content Too Short exception.
718
719     This exception may be raised by FileDownloader objects when a file they
720     download is too small for what the server announced first, indicating
721     the connection was probably interrupted.
722     """
723     # Both in bytes
724     downloaded = None
725     expected = None
726
727     def __init__(self, downloaded, expected):
728         self.downloaded = downloaded
729         self.expected = expected
730
731 class YoutubeDLHandler(compat_urllib_request.HTTPHandler):
732     """Handler for HTTP requests and responses.
733
734     This class, when installed with an OpenerDirector, automatically adds
735     the standard headers to every HTTP request and handles gzipped and
736     deflated responses from web servers. If compression is to be avoided in
737     a particular request, the original request in the program code only has
738     to include the HTTP header "Youtubedl-No-Compression", which will be
739     removed before making the real request.
740
741     Part of this code was copied from:
742
743     http://techknack.net/python-urllib2-handlers/
744
745     Andrew Rowls, the author of that code, agreed to release it to the
746     public domain.
747     """
748
749     @staticmethod
750     def deflate(data):
751         try:
752             return zlib.decompress(data, -zlib.MAX_WBITS)
753         except zlib.error:
754             return zlib.decompress(data)
755
756     @staticmethod
757     def addinfourl_wrapper(stream, headers, url, code):
758         if hasattr(compat_urllib_request.addinfourl, 'getcode'):
759             return compat_urllib_request.addinfourl(stream, headers, url, code)
760         ret = compat_urllib_request.addinfourl(stream, headers, url)
761         ret.code = code
762         return ret
763
764     def http_request(self, req):
765         for h, v in std_headers.items():
766             if h not in req.headers:
767                 req.add_header(h, v)
768         if 'Youtubedl-no-compression' in req.headers:
769             if 'Accept-encoding' in req.headers:
770                 del req.headers['Accept-encoding']
771             del req.headers['Youtubedl-no-compression']
772         if 'Youtubedl-user-agent' in req.headers:
773             if 'User-agent' in req.headers:
774                 del req.headers['User-agent']
775             req.headers['User-agent'] = req.headers['Youtubedl-user-agent']
776             del req.headers['Youtubedl-user-agent']
777         return req
778
779     def http_response(self, req, resp):
780         old_resp = resp
781         # gzip
782         if resp.headers.get('Content-encoding', '') == 'gzip':
783             content = resp.read()
784             gz = gzip.GzipFile(fileobj=io.BytesIO(content), mode='rb')
785             try:
786                 uncompressed = io.BytesIO(gz.read())
787             except IOError as original_ioerror:
788                 # There may be junk add the end of the file
789                 # See http://stackoverflow.com/q/4928560/35070 for details
790                 for i in range(1, 1024):
791                     try:
792                         gz = gzip.GzipFile(fileobj=io.BytesIO(content[:-i]), mode='rb')
793                         uncompressed = io.BytesIO(gz.read())
794                     except IOError:
795                         continue
796                     break
797                 else:
798                     raise original_ioerror
799             resp = self.addinfourl_wrapper(uncompressed, old_resp.headers, old_resp.url, old_resp.code)
800             resp.msg = old_resp.msg
801         # deflate
802         if resp.headers.get('Content-encoding', '') == 'deflate':
803             gz = io.BytesIO(self.deflate(resp.read()))
804             resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code)
805             resp.msg = old_resp.msg
806         return resp
807
808     https_request = http_request
809     https_response = http_response
810
811
812 def parse_iso8601(date_str, delimiter='T'):
813     """ Return a UNIX timestamp from the given date """
814
815     if date_str is None:
816         return None
817
818     m = re.search(
819         r'Z$| ?(?P<sign>\+|-)(?P<hours>[0-9]{2}):?(?P<minutes>[0-9]{2})$',
820         date_str)
821     if not m:
822         timezone = datetime.timedelta()
823     else:
824         date_str = date_str[:-len(m.group(0))]
825         if not m.group('sign'):
826             timezone = datetime.timedelta()
827         else:
828             sign = 1 if m.group('sign') == '+' else -1
829             timezone = datetime.timedelta(
830                 hours=sign * int(m.group('hours')),
831                 minutes=sign * int(m.group('minutes')))
832     date_format =  '%Y-%m-%d{0}%H:%M:%S'.format(delimiter)
833     dt = datetime.datetime.strptime(date_str, date_format) - timezone
834     return calendar.timegm(dt.timetuple())
835
836
837 def unified_strdate(date_str):
838     """Return a string with the date in the format YYYYMMDD"""
839
840     if date_str is None:
841         return None
842
843     upload_date = None
844     #Replace commas
845     date_str = date_str.replace(',', ' ')
846     # %z (UTC offset) is only supported in python>=3.2
847     date_str = re.sub(r' ?(\+|-)[0-9]{2}:?[0-9]{2}$', '', date_str)
848     format_expressions = [
849         '%d %B %Y',
850         '%d %b %Y',
851         '%B %d %Y',
852         '%b %d %Y',
853         '%b %dst %Y %I:%M%p',
854         '%b %dnd %Y %I:%M%p',
855         '%b %dth %Y %I:%M%p',
856         '%Y-%m-%d',
857         '%Y/%m/%d',
858         '%d.%m.%Y',
859         '%d/%m/%Y',
860         '%d/%m/%y',
861         '%Y/%m/%d %H:%M:%S',
862         '%Y-%m-%d %H:%M:%S',
863         '%d.%m.%Y %H:%M',
864         '%d.%m.%Y %H.%M',
865         '%Y-%m-%dT%H:%M:%SZ',
866         '%Y-%m-%dT%H:%M:%S.%fZ',
867         '%Y-%m-%dT%H:%M:%S.%f0Z',
868         '%Y-%m-%dT%H:%M:%S',
869         '%Y-%m-%dT%H:%M:%S.%f',
870         '%Y-%m-%dT%H:%M',
871     ]
872     for expression in format_expressions:
873         try:
874             upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
875         except ValueError:
876             pass
877     if upload_date is None:
878         timetuple = email.utils.parsedate_tz(date_str)
879         if timetuple:
880             upload_date = datetime.datetime(*timetuple[:6]).strftime('%Y%m%d')
881     return upload_date
882
883 def determine_ext(url, default_ext=u'unknown_video'):
884     if url is None:
885         return default_ext
886     guess = url.partition(u'?')[0].rpartition(u'.')[2]
887     if re.match(r'^[A-Za-z0-9]+$', guess):
888         return guess
889     else:
890         return default_ext
891
892 def subtitles_filename(filename, sub_lang, sub_format):
893     return filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format
894
895 def date_from_str(date_str):
896     """
897     Return a datetime object from a string in the format YYYYMMDD or
898     (now|today)[+-][0-9](day|week|month|year)(s)?"""
899     today = datetime.date.today()
900     if date_str == 'now'or date_str == 'today':
901         return today
902     match = re.match('(now|today)(?P<sign>[+-])(?P<time>\d+)(?P<unit>day|week|month|year)(s)?', date_str)
903     if match is not None:
904         sign = match.group('sign')
905         time = int(match.group('time'))
906         if sign == '-':
907             time = -time
908         unit = match.group('unit')
909         #A bad aproximation?
910         if unit == 'month':
911             unit = 'day'
912             time *= 30
913         elif unit == 'year':
914             unit = 'day'
915             time *= 365
916         unit += 's'
917         delta = datetime.timedelta(**{unit: time})
918         return today + delta
919     return datetime.datetime.strptime(date_str, "%Y%m%d").date()
920     
921 def hyphenate_date(date_str):
922     """
923     Convert a date in 'YYYYMMDD' format to 'YYYY-MM-DD' format"""
924     match = re.match(r'^(\d\d\d\d)(\d\d)(\d\d)$', date_str)
925     if match is not None:
926         return '-'.join(match.groups())
927     else:
928         return date_str
929
930 class DateRange(object):
931     """Represents a time interval between two dates"""
932     def __init__(self, start=None, end=None):
933         """start and end must be strings in the format accepted by date"""
934         if start is not None:
935             self.start = date_from_str(start)
936         else:
937             self.start = datetime.datetime.min.date()
938         if end is not None:
939             self.end = date_from_str(end)
940         else:
941             self.end = datetime.datetime.max.date()
942         if self.start > self.end:
943             raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
944     @classmethod
945     def day(cls, day):
946         """Returns a range that only contains the given day"""
947         return cls(day,day)
948     def __contains__(self, date):
949         """Check if the date is in the range"""
950         if not isinstance(date, datetime.date):
951             date = date_from_str(date)
952         return self.start <= date <= self.end
953     def __str__(self):
954         return '%s - %s' % ( self.start.isoformat(), self.end.isoformat())
955
956
957 def platform_name():
958     """ Returns the platform name as a compat_str """
959     res = platform.platform()
960     if isinstance(res, bytes):
961         res = res.decode(preferredencoding())
962
963     assert isinstance(res, compat_str)
964     return res
965
966
967 def _windows_write_string(s, out):
968     """ Returns True if the string was written using special methods,
969     False if it has yet to be written out."""
970     # Adapted from http://stackoverflow.com/a/3259271/35070
971
972     import ctypes
973     import ctypes.wintypes
974
975     WIN_OUTPUT_IDS = {
976         1: -11,
977         2: -12,
978     }
979
980     try:
981         fileno = out.fileno()
982     except AttributeError:
983         # If the output stream doesn't have a fileno, it's virtual
984         return False
985     if fileno not in WIN_OUTPUT_IDS:
986         return False
987
988     GetStdHandle = ctypes.WINFUNCTYPE(
989         ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD)(
990         ("GetStdHandle", ctypes.windll.kernel32))
991     h = GetStdHandle(WIN_OUTPUT_IDS[fileno])
992
993     WriteConsoleW = ctypes.WINFUNCTYPE(
994         ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE, ctypes.wintypes.LPWSTR,
995         ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD),
996         ctypes.wintypes.LPVOID)(("WriteConsoleW", ctypes.windll.kernel32))
997     written = ctypes.wintypes.DWORD(0)
998
999     GetFileType = ctypes.WINFUNCTYPE(ctypes.wintypes.DWORD, ctypes.wintypes.DWORD)(("GetFileType", ctypes.windll.kernel32))
1000     FILE_TYPE_CHAR = 0x0002
1001     FILE_TYPE_REMOTE = 0x8000
1002     GetConsoleMode = ctypes.WINFUNCTYPE(
1003         ctypes.wintypes.BOOL, ctypes.wintypes.HANDLE,
1004         ctypes.POINTER(ctypes.wintypes.DWORD))(
1005         ("GetConsoleMode", ctypes.windll.kernel32))
1006     INVALID_HANDLE_VALUE = ctypes.wintypes.DWORD(-1).value
1007
1008     def not_a_console(handle):
1009         if handle == INVALID_HANDLE_VALUE or handle is None:
1010             return True
1011         return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR
1012                 or GetConsoleMode(handle, ctypes.byref(ctypes.wintypes.DWORD())) == 0)
1013
1014     if not_a_console(h):
1015         return False
1016
1017     def next_nonbmp_pos(s):
1018         try:
1019             return next(i for i, c in enumerate(s) if ord(c) > 0xffff)
1020         except StopIteration:
1021             return len(s)
1022
1023     while s:
1024         count = min(next_nonbmp_pos(s), 1024)
1025
1026         ret = WriteConsoleW(
1027             h, s, count if count else 2, ctypes.byref(written), None)
1028         if ret == 0:
1029             raise OSError('Failed to write string')
1030         if not count:  # We just wrote a non-BMP character
1031             assert written.value == 2
1032             s = s[1:]
1033         else:
1034             assert written.value > 0
1035             s = s[written.value:]
1036     return True
1037
1038
1039 def write_string(s, out=None, encoding=None):
1040     if out is None:
1041         out = sys.stderr
1042     assert type(s) == compat_str
1043
1044     if sys.platform == 'win32' and encoding is None and hasattr(out, 'fileno'):
1045         if _windows_write_string(s, out):
1046             return
1047
1048     if ('b' in getattr(out, 'mode', '') or
1049             sys.version_info[0] < 3):  # Python 2 lies about mode of sys.stderr
1050         byt = s.encode(encoding or preferredencoding(), 'ignore')
1051         out.write(byt)
1052     elif hasattr(out, 'buffer'):
1053         enc = encoding or getattr(out, 'encoding', None) or preferredencoding()
1054         byt = s.encode(enc, 'ignore')
1055         out.buffer.write(byt)
1056     else:
1057         out.write(s)
1058     out.flush()
1059
1060
1061 def bytes_to_intlist(bs):
1062     if not bs:
1063         return []
1064     if isinstance(bs[0], int):  # Python 3
1065         return list(bs)
1066     else:
1067         return [ord(c) for c in bs]
1068
1069
1070 def intlist_to_bytes(xs):
1071     if not xs:
1072         return b''
1073     if isinstance(chr(0), bytes):  # Python 2
1074         return ''.join([chr(x) for x in xs])
1075     else:
1076         return bytes(xs)
1077
1078
1079 def get_cachedir(params={}):
1080     cache_root = os.environ.get('XDG_CACHE_HOME',
1081                                 os.path.expanduser('~/.cache'))
1082     return params.get('cachedir', os.path.join(cache_root, 'youtube-dl'))
1083
1084
1085 # Cross-platform file locking
1086 if sys.platform == 'win32':
1087     import ctypes.wintypes
1088     import msvcrt
1089
1090     class OVERLAPPED(ctypes.Structure):
1091         _fields_ = [
1092             ('Internal', ctypes.wintypes.LPVOID),
1093             ('InternalHigh', ctypes.wintypes.LPVOID),
1094             ('Offset', ctypes.wintypes.DWORD),
1095             ('OffsetHigh', ctypes.wintypes.DWORD),
1096             ('hEvent', ctypes.wintypes.HANDLE),
1097         ]
1098
1099     kernel32 = ctypes.windll.kernel32
1100     LockFileEx = kernel32.LockFileEx
1101     LockFileEx.argtypes = [
1102         ctypes.wintypes.HANDLE,     # hFile
1103         ctypes.wintypes.DWORD,      # dwFlags
1104         ctypes.wintypes.DWORD,      # dwReserved
1105         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
1106         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
1107         ctypes.POINTER(OVERLAPPED)  # Overlapped
1108     ]
1109     LockFileEx.restype = ctypes.wintypes.BOOL
1110     UnlockFileEx = kernel32.UnlockFileEx
1111     UnlockFileEx.argtypes = [
1112         ctypes.wintypes.HANDLE,     # hFile
1113         ctypes.wintypes.DWORD,      # dwReserved
1114         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
1115         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
1116         ctypes.POINTER(OVERLAPPED)  # Overlapped
1117     ]
1118     UnlockFileEx.restype = ctypes.wintypes.BOOL
1119     whole_low = 0xffffffff
1120     whole_high = 0x7fffffff
1121
1122     def _lock_file(f, exclusive):
1123         overlapped = OVERLAPPED()
1124         overlapped.Offset = 0
1125         overlapped.OffsetHigh = 0
1126         overlapped.hEvent = 0
1127         f._lock_file_overlapped_p = ctypes.pointer(overlapped)
1128         handle = msvcrt.get_osfhandle(f.fileno())
1129         if not LockFileEx(handle, 0x2 if exclusive else 0x0, 0,
1130                           whole_low, whole_high, f._lock_file_overlapped_p):
1131             raise OSError('Locking file failed: %r' % ctypes.FormatError())
1132
1133     def _unlock_file(f):
1134         assert f._lock_file_overlapped_p
1135         handle = msvcrt.get_osfhandle(f.fileno())
1136         if not UnlockFileEx(handle, 0,
1137                             whole_low, whole_high, f._lock_file_overlapped_p):
1138             raise OSError('Unlocking file failed: %r' % ctypes.FormatError())
1139
1140 else:
1141     import fcntl
1142
1143     def _lock_file(f, exclusive):
1144         fcntl.flock(f, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
1145
1146     def _unlock_file(f):
1147         fcntl.flock(f, fcntl.LOCK_UN)
1148
1149
1150 class locked_file(object):
1151     def __init__(self, filename, mode, encoding=None):
1152         assert mode in ['r', 'a', 'w']
1153         self.f = io.open(filename, mode, encoding=encoding)
1154         self.mode = mode
1155
1156     def __enter__(self):
1157         exclusive = self.mode != 'r'
1158         try:
1159             _lock_file(self.f, exclusive)
1160         except IOError:
1161             self.f.close()
1162             raise
1163         return self
1164
1165     def __exit__(self, etype, value, traceback):
1166         try:
1167             _unlock_file(self.f)
1168         finally:
1169             self.f.close()
1170
1171     def __iter__(self):
1172         return iter(self.f)
1173
1174     def write(self, *args):
1175         return self.f.write(*args)
1176
1177     def read(self, *args):
1178         return self.f.read(*args)
1179
1180
1181 def shell_quote(args):
1182     quoted_args = []
1183     encoding = sys.getfilesystemencoding()
1184     if encoding is None:
1185         encoding = 'utf-8'
1186     for a in args:
1187         if isinstance(a, bytes):
1188             # We may get a filename encoded with 'encodeFilename'
1189             a = a.decode(encoding)
1190         quoted_args.append(pipes.quote(a))
1191     return u' '.join(quoted_args)
1192
1193
1194 def takewhile_inclusive(pred, seq):
1195     """ Like itertools.takewhile, but include the latest evaluated element
1196         (the first element so that Not pred(e)) """
1197     for e in seq:
1198         yield e
1199         if not pred(e):
1200             return
1201
1202
1203 def smuggle_url(url, data):
1204     """ Pass additional data in a URL for internal use. """
1205
1206     sdata = compat_urllib_parse.urlencode(
1207         {u'__youtubedl_smuggle': json.dumps(data)})
1208     return url + u'#' + sdata
1209
1210
1211 def unsmuggle_url(smug_url, default=None):
1212     if not '#__youtubedl_smuggle' in smug_url:
1213         return smug_url, default
1214     url, _, sdata = smug_url.rpartition(u'#')
1215     jsond = compat_parse_qs(sdata)[u'__youtubedl_smuggle'][0]
1216     data = json.loads(jsond)
1217     return url, data
1218
1219
1220 def format_bytes(bytes):
1221     if bytes is None:
1222         return u'N/A'
1223     if type(bytes) is str:
1224         bytes = float(bytes)
1225     if bytes == 0.0:
1226         exponent = 0
1227     else:
1228         exponent = int(math.log(bytes, 1024.0))
1229     suffix = [u'B', u'KiB', u'MiB', u'GiB', u'TiB', u'PiB', u'EiB', u'ZiB', u'YiB'][exponent]
1230     converted = float(bytes) / float(1024 ** exponent)
1231     return u'%.2f%s' % (converted, suffix)
1232
1233
1234 def get_term_width():
1235     columns = os.environ.get('COLUMNS', None)
1236     if columns:
1237         return int(columns)
1238
1239     try:
1240         sp = subprocess.Popen(
1241             ['stty', 'size'],
1242             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1243         out, err = sp.communicate()
1244         return int(out.split()[1])
1245     except:
1246         pass
1247     return None
1248
1249
1250 def month_by_name(name):
1251     """ Return the number of a month by (locale-independently) English name """
1252
1253     ENGLISH_NAMES = [
1254         u'January', u'February', u'March', u'April', u'May', u'June',
1255         u'July', u'August', u'September', u'October', u'November', u'December']
1256     try:
1257         return ENGLISH_NAMES.index(name) + 1
1258     except ValueError:
1259         return None
1260
1261
1262 def fix_xml_ampersands(xml_str):
1263     """Replace all the '&' by '&amp;' in XML"""
1264     return re.sub(
1265         r'&(?!amp;|lt;|gt;|apos;|quot;|#x[0-9a-fA-F]{,4};|#[0-9]{,4};)',
1266         u'&amp;',
1267         xml_str)
1268
1269
1270 def setproctitle(title):
1271     assert isinstance(title, compat_str)
1272     try:
1273         libc = ctypes.cdll.LoadLibrary("libc.so.6")
1274     except OSError:
1275         return
1276     title_bytes = title.encode('utf-8')
1277     buf = ctypes.create_string_buffer(len(title_bytes))
1278     buf.value = title_bytes
1279     try:
1280         libc.prctl(15, buf, 0, 0, 0)
1281     except AttributeError:
1282         return  # Strange libc, just skip this
1283
1284
1285 def remove_start(s, start):
1286     if s.startswith(start):
1287         return s[len(start):]
1288     return s
1289
1290
1291 def remove_end(s, end):
1292     if s.endswith(end):
1293         return s[:-len(end)]
1294     return s
1295
1296
1297 def url_basename(url):
1298     path = compat_urlparse.urlparse(url).path
1299     return path.strip(u'/').split(u'/')[-1]
1300
1301
1302 class HEADRequest(compat_urllib_request.Request):
1303     def get_method(self):
1304         return "HEAD"
1305
1306
1307 def int_or_none(v, scale=1, default=None, get_attr=None, invscale=1):
1308     if get_attr:
1309         if v is not None:
1310             v = getattr(v, get_attr, None)
1311     if v == '':
1312         v = None
1313     return default if v is None else (int(v) * invscale // scale)
1314
1315
1316 def str_or_none(v, default=None):
1317     return default if v is None else compat_str(v)
1318
1319
1320 def str_to_int(int_str):
1321     """ A more relaxed version of int_or_none """
1322     if int_str is None:
1323         return None
1324     int_str = re.sub(r'[,\.\+]', u'', int_str)
1325     return int(int_str)
1326
1327
1328 def float_or_none(v, scale=1, invscale=1, default=None):
1329     return default if v is None else (float(v) * invscale / scale)
1330
1331
1332 def parse_duration(s):
1333     if s is None:
1334         return None
1335
1336     s = s.strip()
1337
1338     m = re.match(
1339         r'(?i)(?:(?:(?P<hours>[0-9]+)\s*(?:[:h]|hours?)\s*)?(?P<mins>[0-9]+)\s*(?:[:m]|mins?|minutes?)\s*)?(?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?\s*(?:s|secs?|seconds?)?$', s)
1340     if not m:
1341         return None
1342     res = int(m.group('secs'))
1343     if m.group('mins'):
1344         res += int(m.group('mins')) * 60
1345         if m.group('hours'):
1346             res += int(m.group('hours')) * 60 * 60
1347     if m.group('ms'):
1348         res += float(m.group('ms'))
1349     return res
1350
1351
1352 def prepend_extension(filename, ext):
1353     name, real_ext = os.path.splitext(filename) 
1354     return u'{0}.{1}{2}'.format(name, ext, real_ext)
1355
1356
1357 def check_executable(exe, args=[]):
1358     """ Checks if the given binary is installed somewhere in PATH, and returns its name.
1359     args can be a list of arguments for a short output (like -version) """
1360     try:
1361         subprocess.Popen([exe] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
1362     except OSError:
1363         return False
1364     return exe
1365
1366
1367 class PagedList(object):
1368     def __init__(self, pagefunc, pagesize):
1369         self._pagefunc = pagefunc
1370         self._pagesize = pagesize
1371
1372     def __len__(self):
1373         # This is only useful for tests
1374         return len(self.getslice())
1375
1376     def getslice(self, start=0, end=None):
1377         res = []
1378         for pagenum in itertools.count(start // self._pagesize):
1379             firstid = pagenum * self._pagesize
1380             nextfirstid = pagenum * self._pagesize + self._pagesize
1381             if start >= nextfirstid:
1382                 continue
1383
1384             page_results = list(self._pagefunc(pagenum))
1385
1386             startv = (
1387                 start % self._pagesize
1388                 if firstid <= start < nextfirstid
1389                 else 0)
1390
1391             endv = (
1392                 ((end - 1) % self._pagesize) + 1
1393                 if (end is not None and firstid <= end <= nextfirstid)
1394                 else None)
1395
1396             if startv != 0 or endv is not None:
1397                 page_results = page_results[startv:endv]
1398             res.extend(page_results)
1399
1400             # A little optimization - if current page is not "full", ie. does
1401             # not contain page_size videos then we can assume that this page
1402             # is the last one - there are no more ids on further pages -
1403             # i.e. no need to query again.
1404             if len(page_results) + startv < self._pagesize:
1405                 break
1406
1407             # If we got the whole page, but the next page is not interesting,
1408             # break out early as well
1409             if end == nextfirstid:
1410                 break
1411         return res
1412
1413
1414 def uppercase_escape(s):
1415     unicode_escape = codecs.getdecoder('unicode_escape')
1416     return re.sub(
1417         r'\\U[0-9a-fA-F]{8}',
1418         lambda m: unicode_escape(m.group(0))[0],
1419         s)
1420
1421 try:
1422     struct.pack(u'!I', 0)
1423 except TypeError:
1424     # In Python 2.6 (and some 2.7 versions), struct requires a bytes argument
1425     def struct_pack(spec, *args):
1426         if isinstance(spec, compat_str):
1427             spec = spec.encode('ascii')
1428         return struct.pack(spec, *args)
1429
1430     def struct_unpack(spec, *args):
1431         if isinstance(spec, compat_str):
1432             spec = spec.encode('ascii')
1433         return struct.unpack(spec, *args)
1434 else:
1435     struct_pack = struct.pack
1436     struct_unpack = struct.unpack
1437
1438
1439 def read_batch_urls(batch_fd):
1440     def fixup(url):
1441         if not isinstance(url, compat_str):
1442             url = url.decode('utf-8', 'replace')
1443         BOM_UTF8 = u'\xef\xbb\xbf'
1444         if url.startswith(BOM_UTF8):
1445             url = url[len(BOM_UTF8):]
1446         url = url.strip()
1447         if url.startswith(('#', ';', ']')):
1448             return False
1449         return url
1450
1451     with contextlib.closing(batch_fd) as fd:
1452         return [url for url in map(fixup, fd) if url]
1453
1454
1455 def urlencode_postdata(*args, **kargs):
1456     return compat_urllib_parse.urlencode(*args, **kargs).encode('ascii')
1457
1458
1459 try:
1460     etree_iter = xml.etree.ElementTree.Element.iter
1461 except AttributeError:  # Python <=2.6
1462     etree_iter = lambda n: n.findall('.//*')
1463
1464
1465 def parse_xml(s):
1466     class TreeBuilder(xml.etree.ElementTree.TreeBuilder):
1467         def doctype(self, name, pubid, system):
1468             pass  # Ignore doctypes
1469
1470     parser = xml.etree.ElementTree.XMLParser(target=TreeBuilder())
1471     kwargs = {'parser': parser} if sys.version_info >= (2, 7) else {}
1472     tree = xml.etree.ElementTree.XML(s.encode('utf-8'), **kwargs)
1473     # Fix up XML parser in Python 2.x
1474     if sys.version_info < (3, 0):
1475         for n in etree_iter(tree):
1476             if n.text is not None:
1477                 if not isinstance(n.text, compat_str):
1478                     n.text = n.text.decode('utf-8')
1479     return tree
1480
1481
1482 if sys.version_info < (3, 0) and sys.platform == 'win32':
1483     def compat_getpass(prompt, *args, **kwargs):
1484         if isinstance(prompt, compat_str):
1485             prompt = prompt.encode(preferredencoding())
1486         return getpass.getpass(prompt, *args, **kwargs)
1487 else:
1488     compat_getpass = getpass.getpass
1489
1490
1491 US_RATINGS = {
1492     'G': 0,
1493     'PG': 10,
1494     'PG-13': 13,
1495     'R': 16,
1496     'NC': 18,
1497 }
1498
1499
1500 def strip_jsonp(code):
1501     return re.sub(r'(?s)^[a-zA-Z0-9_]+\s*\(\s*(.*)\);?\s*?\s*$', r'\1', code)
1502
1503
1504 def js_to_json(code):
1505     def fix_kv(m):
1506         key = m.group(2)
1507         if key.startswith("'"):
1508             assert key.endswith("'")
1509             assert '"' not in key
1510             key = '"%s"' % key[1:-1]
1511         elif not key.startswith('"'):
1512             key = '"%s"' % key
1513
1514         value = m.group(4)
1515         if value.startswith("'"):
1516             assert value.endswith("'")
1517             assert '"' not in value
1518             value = '"%s"' % value[1:-1]
1519
1520         return m.group(1) + key + m.group(3) + value
1521
1522     res = re.sub(r'''(?x)
1523             ([{,]\s*)
1524             ("[^"]*"|\'[^\']*\'|[a-z0-9A-Z]+)
1525             (:\s*)
1526             ([0-9.]+|true|false|"[^"]*"|\'[^\']*\'|\[|\{)
1527         ''', fix_kv, code)
1528     res = re.sub(r',(\s*\])', lambda m: m.group(1), res)
1529     return res
1530
1531
1532 def qualities(quality_ids):
1533     """ Get a numeric quality value out of a list of possible values """
1534     def q(qid):
1535         try:
1536             return quality_ids.index(qid)
1537         except ValueError:
1538             return -1
1539     return q
1540
1541
1542 DEFAULT_OUTTMPL = '%(title)s-%(id)s.%(ext)s'
1543
1544 try:
1545     subprocess_check_output = subprocess.check_output
1546 except AttributeError:
1547     def subprocess_check_output(*args, **kwargs):
1548         assert 'input' not in kwargs
1549         p = subprocess.Popen(*args, stdout=subprocess.PIPE, **kwargs)
1550         output, _ = p.communicate()
1551         ret = p.poll()
1552         if ret:
1553             raise subprocess.CalledProcessError(ret, p.args, output=output)
1554         return output