]> gitweb @ CieloNegro.org - youtube-dl.git/blob - youtube_dl/utils.py
[zdf/common] Use API in ZDF extractor.
[youtube-dl.git] / youtube_dl / utils.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import datetime
5 import email.utils
6 import errno
7 import gzip
8 import io
9 import json
10 import locale
11 import math
12 import os
13 import pipes
14 import platform
15 import re
16 import ssl
17 import socket
18 import sys
19 import traceback
20 import xml.etree.ElementTree
21 import zlib
22
23 try:
24     import urllib.request as compat_urllib_request
25 except ImportError: # Python 2
26     import urllib2 as compat_urllib_request
27
28 try:
29     import urllib.error as compat_urllib_error
30 except ImportError: # Python 2
31     import urllib2 as compat_urllib_error
32
33 try:
34     import urllib.parse as compat_urllib_parse
35 except ImportError: # Python 2
36     import urllib as compat_urllib_parse
37
38 try:
39     from urllib.parse import urlparse as compat_urllib_parse_urlparse
40 except ImportError: # Python 2
41     from urlparse import urlparse as compat_urllib_parse_urlparse
42
43 try:
44     import urllib.parse as compat_urlparse
45 except ImportError: # Python 2
46     import urlparse as compat_urlparse
47
48 try:
49     import http.cookiejar as compat_cookiejar
50 except ImportError: # Python 2
51     import cookielib as compat_cookiejar
52
53 try:
54     import html.entities as compat_html_entities
55 except ImportError: # Python 2
56     import htmlentitydefs as compat_html_entities
57
58 try:
59     import html.parser as compat_html_parser
60 except ImportError: # Python 2
61     import HTMLParser as compat_html_parser
62
63 try:
64     import http.client as compat_http_client
65 except ImportError: # Python 2
66     import httplib as compat_http_client
67
68 try:
69     from urllib.error import HTTPError as compat_HTTPError
70 except ImportError:  # Python 2
71     from urllib2 import HTTPError as compat_HTTPError
72
73 try:
74     from urllib.request import urlretrieve as compat_urlretrieve
75 except ImportError:  # Python 2
76     from urllib import urlretrieve as compat_urlretrieve
77
78
79 try:
80     from subprocess import DEVNULL
81     compat_subprocess_get_DEVNULL = lambda: DEVNULL
82 except ImportError:
83     compat_subprocess_get_DEVNULL = lambda: open(os.path.devnull, 'w')
84
85 try:
86     from urllib.parse import parse_qs as compat_parse_qs
87 except ImportError: # Python 2
88     # HACK: The following is the correct parse_qs implementation from cpython 3's stdlib.
89     # Python 2's version is apparently totally broken
90     def _unquote(string, encoding='utf-8', errors='replace'):
91         if string == '':
92             return string
93         res = string.split('%')
94         if len(res) == 1:
95             return string
96         if encoding is None:
97             encoding = 'utf-8'
98         if errors is None:
99             errors = 'replace'
100         # pct_sequence: contiguous sequence of percent-encoded bytes, decoded
101         pct_sequence = b''
102         string = res[0]
103         for item in res[1:]:
104             try:
105                 if not item:
106                     raise ValueError
107                 pct_sequence += item[:2].decode('hex')
108                 rest = item[2:]
109                 if not rest:
110                     # This segment was just a single percent-encoded character.
111                     # May be part of a sequence of code units, so delay decoding.
112                     # (Stored in pct_sequence).
113                     continue
114             except ValueError:
115                 rest = '%' + item
116             # Encountered non-percent-encoded characters. Flush the current
117             # pct_sequence.
118             string += pct_sequence.decode(encoding, errors) + rest
119             pct_sequence = b''
120         if pct_sequence:
121             # Flush the final pct_sequence
122             string += pct_sequence.decode(encoding, errors)
123         return string
124
125     def _parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
126                 encoding='utf-8', errors='replace'):
127         qs, _coerce_result = qs, unicode
128         pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
129         r = []
130         for name_value in pairs:
131             if not name_value and not strict_parsing:
132                 continue
133             nv = name_value.split('=', 1)
134             if len(nv) != 2:
135                 if strict_parsing:
136                     raise ValueError("bad query field: %r" % (name_value,))
137                 # Handle case of a control-name with no equal sign
138                 if keep_blank_values:
139                     nv.append('')
140                 else:
141                     continue
142             if len(nv[1]) or keep_blank_values:
143                 name = nv[0].replace('+', ' ')
144                 name = _unquote(name, encoding=encoding, errors=errors)
145                 name = _coerce_result(name)
146                 value = nv[1].replace('+', ' ')
147                 value = _unquote(value, encoding=encoding, errors=errors)
148                 value = _coerce_result(value)
149                 r.append((name, value))
150         return r
151
152     def compat_parse_qs(qs, keep_blank_values=False, strict_parsing=False,
153                 encoding='utf-8', errors='replace'):
154         parsed_result = {}
155         pairs = _parse_qsl(qs, keep_blank_values, strict_parsing,
156                         encoding=encoding, errors=errors)
157         for name, value in pairs:
158             if name in parsed_result:
159                 parsed_result[name].append(value)
160             else:
161                 parsed_result[name] = [value]
162         return parsed_result
163
164 try:
165     compat_str = unicode # Python 2
166 except NameError:
167     compat_str = str
168
169 try:
170     compat_chr = unichr # Python 2
171 except NameError:
172     compat_chr = chr
173
174 def compat_ord(c):
175     if type(c) is int: return c
176     else: return ord(c)
177
178 # This is not clearly defined otherwise
179 compiled_regex_type = type(re.compile(''))
180
181 std_headers = {
182     'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 (Chrome)',
183     'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
184     'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
185     'Accept-Encoding': 'gzip, deflate',
186     'Accept-Language': 'en-us,en;q=0.5',
187 }
188
189 def preferredencoding():
190     """Get preferred encoding.
191
192     Returns the best encoding scheme for the system, based on
193     locale.getpreferredencoding() and some further tweaks.
194     """
195     try:
196         pref = locale.getpreferredencoding()
197         u'TEST'.encode(pref)
198     except:
199         pref = 'UTF-8'
200
201     return pref
202
203 if sys.version_info < (3,0):
204     def compat_print(s):
205         print(s.encode(preferredencoding(), 'xmlcharrefreplace'))
206 else:
207     def compat_print(s):
208         assert type(s) == type(u'')
209         print(s)
210
211 # In Python 2.x, json.dump expects a bytestream.
212 # In Python 3.x, it writes to a character stream
213 if sys.version_info < (3,0):
214     def write_json_file(obj, fn):
215         with open(fn, 'wb') as f:
216             json.dump(obj, f)
217 else:
218     def write_json_file(obj, fn):
219         with open(fn, 'w', encoding='utf-8') as f:
220             json.dump(obj, f)
221
222 if sys.version_info >= (2,7):
223     def find_xpath_attr(node, xpath, key, val):
224         """ Find the xpath xpath[@key=val] """
225         assert re.match(r'^[a-zA-Z]+$', key)
226         assert re.match(r'^[a-zA-Z0-9@\s]*$', val)
227         expr = xpath + u"[@%s='%s']" % (key, val)
228         return node.find(expr)
229 else:
230     def find_xpath_attr(node, xpath, key, val):
231         for f in node.findall(xpath):
232             if f.attrib.get(key) == val:
233                 return f
234         return None
235
236 # On python2.6 the xml.etree.ElementTree.Element methods don't support
237 # the namespace parameter
238 def xpath_with_ns(path, ns_map):
239     components = [c.split(':') for c in path.split('/')]
240     replaced = []
241     for c in components:
242         if len(c) == 1:
243             replaced.append(c[0])
244         else:
245             ns, tag = c
246             replaced.append('{%s}%s' % (ns_map[ns], tag))
247     return '/'.join(replaced)
248
249 def htmlentity_transform(matchobj):
250     """Transforms an HTML entity to a character.
251
252     This function receives a match object and is intended to be used with
253     the re.sub() function.
254     """
255     entity = matchobj.group(1)
256
257     # Known non-numeric HTML entity
258     if entity in compat_html_entities.name2codepoint:
259         return compat_chr(compat_html_entities.name2codepoint[entity])
260
261     mobj = re.match(u'(?u)#(x?\\d+)', entity)
262     if mobj is not None:
263         numstr = mobj.group(1)
264         if numstr.startswith(u'x'):
265             base = 16
266             numstr = u'0%s' % numstr
267         else:
268             base = 10
269         return compat_chr(int(numstr, base))
270
271     # Unknown entity in name, return its literal representation
272     return (u'&%s;' % entity)
273
274 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
275 class BaseHTMLParser(compat_html_parser.HTMLParser):
276     def __init(self):
277         compat_html_parser.HTMLParser.__init__(self)
278         self.html = None
279
280     def loads(self, html):
281         self.html = html
282         self.feed(html)
283         self.close()
284
285 class AttrParser(BaseHTMLParser):
286     """Modified HTMLParser that isolates a tag with the specified attribute"""
287     def __init__(self, attribute, value):
288         self.attribute = attribute
289         self.value = value
290         self.result = None
291         self.started = False
292         self.depth = {}
293         self.watch_startpos = False
294         self.error_count = 0
295         BaseHTMLParser.__init__(self)
296
297     def error(self, message):
298         if self.error_count > 10 or self.started:
299             raise compat_html_parser.HTMLParseError(message, self.getpos())
300         self.rawdata = '\n'.join(self.html.split('\n')[self.getpos()[0]:]) # skip one line
301         self.error_count += 1
302         self.goahead(1)
303
304     def handle_starttag(self, tag, attrs):
305         attrs = dict(attrs)
306         if self.started:
307             self.find_startpos(None)
308         if self.attribute in attrs and attrs[self.attribute] == self.value:
309             self.result = [tag]
310             self.started = True
311             self.watch_startpos = True
312         if self.started:
313             if not tag in self.depth: self.depth[tag] = 0
314             self.depth[tag] += 1
315
316     def handle_endtag(self, tag):
317         if self.started:
318             if tag in self.depth: self.depth[tag] -= 1
319             if self.depth[self.result[0]] == 0:
320                 self.started = False
321                 self.result.append(self.getpos())
322
323     def find_startpos(self, x):
324         """Needed to put the start position of the result (self.result[1])
325         after the opening tag with the requested id"""
326         if self.watch_startpos:
327             self.watch_startpos = False
328             self.result.append(self.getpos())
329     handle_entityref = handle_charref = handle_data = handle_comment = \
330     handle_decl = handle_pi = unknown_decl = find_startpos
331
332     def get_result(self):
333         if self.result is None:
334             return None
335         if len(self.result) != 3:
336             return None
337         lines = self.html.split('\n')
338         lines = lines[self.result[1][0]-1:self.result[2][0]]
339         lines[0] = lines[0][self.result[1][1]:]
340         if len(lines) == 1:
341             lines[-1] = lines[-1][:self.result[2][1]-self.result[1][1]]
342         lines[-1] = lines[-1][:self.result[2][1]]
343         return '\n'.join(lines).strip()
344 # Hack for https://github.com/rg3/youtube-dl/issues/662
345 if sys.version_info < (2, 7, 3):
346     AttrParser.parse_endtag = (lambda self, i:
347         i + len("</scr'+'ipt>")
348         if self.rawdata[i:].startswith("</scr'+'ipt>")
349         else compat_html_parser.HTMLParser.parse_endtag(self, i))
350
351 def get_element_by_id(id, html):
352     """Return the content of the tag with the specified ID in the passed HTML document"""
353     return get_element_by_attribute("id", id, html)
354
355 def get_element_by_attribute(attribute, value, html):
356     """Return the content of the tag with the specified attribute in the passed HTML document"""
357     parser = AttrParser(attribute, value)
358     try:
359         parser.loads(html)
360     except compat_html_parser.HTMLParseError:
361         pass
362     return parser.get_result()
363
364 class MetaParser(BaseHTMLParser):
365     """
366     Modified HTMLParser that isolates a meta tag with the specified name 
367     attribute.
368     """
369     def __init__(self, name):
370         BaseHTMLParser.__init__(self)
371         self.name = name
372         self.content = None
373         self.result = None
374
375     def handle_starttag(self, tag, attrs):
376         if tag != 'meta':
377             return
378         attrs = dict(attrs)
379         if attrs.get('name') == self.name:
380             self.result = attrs.get('content')
381
382     def get_result(self):
383         return self.result
384
385 def get_meta_content(name, html):
386     """
387     Return the content attribute from the meta tag with the given name attribute.
388     """
389     parser = MetaParser(name)
390     try:
391         parser.loads(html)
392     except compat_html_parser.HTMLParseError:
393         pass
394     return parser.get_result()
395
396
397 def clean_html(html):
398     """Clean an HTML snippet into a readable string"""
399     # Newline vs <br />
400     html = html.replace('\n', ' ')
401     html = re.sub(r'\s*<\s*br\s*/?\s*>\s*', '\n', html)
402     html = re.sub(r'<\s*/\s*p\s*>\s*<\s*p[^>]*>', '\n', html)
403     # Strip html tags
404     html = re.sub('<.*?>', '', html)
405     # Replace html entities
406     html = unescapeHTML(html)
407     return html.strip()
408
409
410 def sanitize_open(filename, open_mode):
411     """Try to open the given filename, and slightly tweak it if this fails.
412
413     Attempts to open the given filename. If this fails, it tries to change
414     the filename slightly, step by step, until it's either able to open it
415     or it fails and raises a final exception, like the standard open()
416     function.
417
418     It returns the tuple (stream, definitive_file_name).
419     """
420     try:
421         if filename == u'-':
422             if sys.platform == 'win32':
423                 import msvcrt
424                 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
425             return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename)
426         stream = open(encodeFilename(filename), open_mode)
427         return (stream, filename)
428     except (IOError, OSError) as err:
429         if err.errno in (errno.EACCES,):
430             raise
431
432         # In case of error, try to remove win32 forbidden chars
433         alt_filename = os.path.join(
434                         re.sub(u'[/<>:"\\|\\\\?\\*]', u'#', path_part)
435                         for path_part in os.path.split(filename)
436                        )
437         if alt_filename == filename:
438             raise
439         else:
440             # An exception here should be caught in the caller
441             stream = open(encodeFilename(filename), open_mode)
442             return (stream, alt_filename)
443
444
445 def timeconvert(timestr):
446     """Convert RFC 2822 defined time string into system timestamp"""
447     timestamp = None
448     timetuple = email.utils.parsedate_tz(timestr)
449     if timetuple is not None:
450         timestamp = email.utils.mktime_tz(timetuple)
451     return timestamp
452
453 def sanitize_filename(s, restricted=False, is_id=False):
454     """Sanitizes a string so it could be used as part of a filename.
455     If restricted is set, use a stricter subset of allowed characters.
456     Set is_id if this is not an arbitrary string, but an ID that should be kept if possible
457     """
458     def replace_insane(char):
459         if char == '?' or ord(char) < 32 or ord(char) == 127:
460             return ''
461         elif char == '"':
462             return '' if restricted else '\''
463         elif char == ':':
464             return '_-' if restricted else ' -'
465         elif char in '\\/|*<>':
466             return '_'
467         if restricted and (char in '!&\'()[]{}$;`^,#' or char.isspace()):
468             return '_'
469         if restricted and ord(char) > 127:
470             return '_'
471         return char
472
473     result = u''.join(map(replace_insane, s))
474     if not is_id:
475         while '__' in result:
476             result = result.replace('__', '_')
477         result = result.strip('_')
478         # Common case of "Foreign band name - English song title"
479         if restricted and result.startswith('-_'):
480             result = result[2:]
481         if not result:
482             result = '_'
483     return result
484
485 def orderedSet(iterable):
486     """ Remove all duplicates from the input iterable """
487     res = []
488     for el in iterable:
489         if el not in res:
490             res.append(el)
491     return res
492
493 def unescapeHTML(s):
494     """
495     @param s a string
496     """
497     assert type(s) == type(u'')
498
499     result = re.sub(u'(?u)&(.+?);', htmlentity_transform, s)
500     return result
501
502 def encodeFilename(s):
503     """
504     @param s The name of the file
505     """
506
507     assert type(s) == type(u'')
508
509     # Python 3 has a Unicode API
510     if sys.version_info >= (3, 0):
511         return s
512
513     if sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
514         # Pass u'' directly to use Unicode APIs on Windows 2000 and up
515         # (Detecting Windows NT 4 is tricky because 'major >= 4' would
516         # match Windows 9x series as well. Besides, NT 4 is obsolete.)
517         return s
518     else:
519         encoding = sys.getfilesystemencoding()
520         if encoding is None:
521             encoding = 'utf-8'
522         return s.encode(encoding, 'ignore')
523
524 def decodeOption(optval):
525     if optval is None:
526         return optval
527     if isinstance(optval, bytes):
528         optval = optval.decode(preferredencoding())
529
530     assert isinstance(optval, compat_str)
531     return optval
532
533 def formatSeconds(secs):
534     if secs > 3600:
535         return '%d:%02d:%02d' % (secs // 3600, (secs % 3600) // 60, secs % 60)
536     elif secs > 60:
537         return '%d:%02d' % (secs // 60, secs % 60)
538     else:
539         return '%d' % secs
540
541
542 def make_HTTPS_handler(opts):
543     if sys.version_info < (3, 2):
544         import httplib
545
546         class HTTPSConnectionV3(httplib.HTTPSConnection):
547             def __init__(self, *args, **kwargs):
548                 httplib.HTTPSConnection.__init__(self, *args, **kwargs)
549
550             def connect(self):
551                 sock = socket.create_connection((self.host, self.port), self.timeout)
552                 if self._tunnel_host:
553                     self.sock = sock
554                     self._tunnel()
555                 try:
556                     self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version=ssl.PROTOCOL_SSLv3)
557                 except ssl.SSLError as e:
558                     self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version=ssl.PROTOCOL_SSLv23)
559
560         class HTTPSHandlerV3(compat_urllib_request.HTTPSHandler):
561             def https_open(self, req):
562                 return self.do_open(HTTPSConnectionV3, req)
563         return HTTPSHandlerV3()
564     else:
565         context = ssl.SSLContext(ssl.PROTOCOL_SSLv3)
566         context.set_default_verify_paths()
567         
568         context.verify_mode = (ssl.CERT_NONE
569                                if opts.no_check_certificate
570                                else ssl.CERT_REQUIRED)
571         return compat_urllib_request.HTTPSHandler(context=context)
572
573 class ExtractorError(Exception):
574     """Error during info extraction."""
575     def __init__(self, msg, tb=None, expected=False, cause=None):
576         """ tb, if given, is the original traceback (so that it can be printed out).
577         If expected is set, this is a normal error message and most likely not a bug in youtube-dl.
578         """
579
580         if sys.exc_info()[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError):
581             expected = True
582         if not expected:
583             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.'
584         super(ExtractorError, self).__init__(msg)
585
586         self.traceback = tb
587         self.exc_info = sys.exc_info()  # preserve original exception
588         self.cause = cause
589
590     def format_traceback(self):
591         if self.traceback is None:
592             return None
593         return u''.join(traceback.format_tb(self.traceback))
594
595
596 class RegexNotFoundError(ExtractorError):
597     """Error when a regex didn't match"""
598     pass
599
600
601 class DownloadError(Exception):
602     """Download Error exception.
603
604     This exception may be thrown by FileDownloader objects if they are not
605     configured to continue on errors. They will contain the appropriate
606     error message.
607     """
608     def __init__(self, msg, exc_info=None):
609         """ exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """
610         super(DownloadError, self).__init__(msg)
611         self.exc_info = exc_info
612
613
614 class SameFileError(Exception):
615     """Same File exception.
616
617     This exception will be thrown by FileDownloader objects if they detect
618     multiple files would have to be downloaded to the same file on disk.
619     """
620     pass
621
622
623 class PostProcessingError(Exception):
624     """Post Processing exception.
625
626     This exception may be raised by PostProcessor's .run() method to
627     indicate an error in the postprocessing task.
628     """
629     def __init__(self, msg):
630         self.msg = msg
631
632 class MaxDownloadsReached(Exception):
633     """ --max-downloads limit has been reached. """
634     pass
635
636
637 class UnavailableVideoError(Exception):
638     """Unavailable Format exception.
639
640     This exception will be thrown when a video is requested
641     in a format that is not available for that video.
642     """
643     pass
644
645
646 class ContentTooShortError(Exception):
647     """Content Too Short exception.
648
649     This exception may be raised by FileDownloader objects when a file they
650     download is too small for what the server announced first, indicating
651     the connection was probably interrupted.
652     """
653     # Both in bytes
654     downloaded = None
655     expected = None
656
657     def __init__(self, downloaded, expected):
658         self.downloaded = downloaded
659         self.expected = expected
660
661 class YoutubeDLHandler(compat_urllib_request.HTTPHandler):
662     """Handler for HTTP requests and responses.
663
664     This class, when installed with an OpenerDirector, automatically adds
665     the standard headers to every HTTP request and handles gzipped and
666     deflated responses from web servers. If compression is to be avoided in
667     a particular request, the original request in the program code only has
668     to include the HTTP header "Youtubedl-No-Compression", which will be
669     removed before making the real request.
670
671     Part of this code was copied from:
672
673     http://techknack.net/python-urllib2-handlers/
674
675     Andrew Rowls, the author of that code, agreed to release it to the
676     public domain.
677     """
678
679     @staticmethod
680     def deflate(data):
681         try:
682             return zlib.decompress(data, -zlib.MAX_WBITS)
683         except zlib.error:
684             return zlib.decompress(data)
685
686     @staticmethod
687     def addinfourl_wrapper(stream, headers, url, code):
688         if hasattr(compat_urllib_request.addinfourl, 'getcode'):
689             return compat_urllib_request.addinfourl(stream, headers, url, code)
690         ret = compat_urllib_request.addinfourl(stream, headers, url)
691         ret.code = code
692         return ret
693
694     def http_request(self, req):
695         for h,v in std_headers.items():
696             if h in req.headers:
697                 del req.headers[h]
698             req.add_header(h, v)
699         if 'Youtubedl-no-compression' in req.headers:
700             if 'Accept-encoding' in req.headers:
701                 del req.headers['Accept-encoding']
702             del req.headers['Youtubedl-no-compression']
703         if 'Youtubedl-user-agent' in req.headers:
704             if 'User-agent' in req.headers:
705                 del req.headers['User-agent']
706             req.headers['User-agent'] = req.headers['Youtubedl-user-agent']
707             del req.headers['Youtubedl-user-agent']
708         return req
709
710     def http_response(self, req, resp):
711         old_resp = resp
712         # gzip
713         if resp.headers.get('Content-encoding', '') == 'gzip':
714             content = resp.read()
715             gz = gzip.GzipFile(fileobj=io.BytesIO(content), mode='rb')
716             try:
717                 uncompressed = io.BytesIO(gz.read())
718             except IOError as original_ioerror:
719                 # There may be junk add the end of the file
720                 # See http://stackoverflow.com/q/4928560/35070 for details
721                 for i in range(1, 1024):
722                     try:
723                         gz = gzip.GzipFile(fileobj=io.BytesIO(content[:-i]), mode='rb')
724                         uncompressed = io.BytesIO(gz.read())
725                     except IOError:
726                         continue
727                     break
728                 else:
729                     raise original_ioerror
730             resp = self.addinfourl_wrapper(uncompressed, old_resp.headers, old_resp.url, old_resp.code)
731             resp.msg = old_resp.msg
732         # deflate
733         if resp.headers.get('Content-encoding', '') == 'deflate':
734             gz = io.BytesIO(self.deflate(resp.read()))
735             resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code)
736             resp.msg = old_resp.msg
737         return resp
738
739     https_request = http_request
740     https_response = http_response
741
742 def unified_strdate(date_str):
743     """Return a string with the date in the format YYYYMMDD"""
744     upload_date = None
745     #Replace commas
746     date_str = date_str.replace(',',' ')
747     # %z (UTC offset) is only supported in python>=3.2
748     date_str = re.sub(r' (\+|-)[\d]*$', '', date_str)
749     format_expressions = [
750         '%d %B %Y',
751         '%B %d %Y',
752         '%b %d %Y',
753         '%Y-%m-%d',
754         '%d/%m/%Y',
755         '%Y/%m/%d %H:%M:%S',
756         '%d.%m.%Y %H:%M',
757         '%Y-%m-%dT%H:%M:%SZ',
758         '%Y-%m-%dT%H:%M:%S.%fZ',
759         '%Y-%m-%dT%H:%M:%S.%f0Z',
760         '%Y-%m-%dT%H:%M:%S',
761     ]
762     for expression in format_expressions:
763         try:
764             upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
765         except:
766             pass
767     return upload_date
768
769 def determine_ext(url, default_ext=u'unknown_video'):
770     guess = url.partition(u'?')[0].rpartition(u'.')[2]
771     if re.match(r'^[A-Za-z0-9]+$', guess):
772         return guess
773     else:
774         return default_ext
775
776 def subtitles_filename(filename, sub_lang, sub_format):
777     return filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format
778
779 def date_from_str(date_str):
780     """
781     Return a datetime object from a string in the format YYYYMMDD or
782     (now|today)[+-][0-9](day|week|month|year)(s)?"""
783     today = datetime.date.today()
784     if date_str == 'now'or date_str == 'today':
785         return today
786     match = re.match('(now|today)(?P<sign>[+-])(?P<time>\d+)(?P<unit>day|week|month|year)(s)?', date_str)
787     if match is not None:
788         sign = match.group('sign')
789         time = int(match.group('time'))
790         if sign == '-':
791             time = -time
792         unit = match.group('unit')
793         #A bad aproximation?
794         if unit == 'month':
795             unit = 'day'
796             time *= 30
797         elif unit == 'year':
798             unit = 'day'
799             time *= 365
800         unit += 's'
801         delta = datetime.timedelta(**{unit: time})
802         return today + delta
803     return datetime.datetime.strptime(date_str, "%Y%m%d").date()
804     
805 class DateRange(object):
806     """Represents a time interval between two dates"""
807     def __init__(self, start=None, end=None):
808         """start and end must be strings in the format accepted by date"""
809         if start is not None:
810             self.start = date_from_str(start)
811         else:
812             self.start = datetime.datetime.min.date()
813         if end is not None:
814             self.end = date_from_str(end)
815         else:
816             self.end = datetime.datetime.max.date()
817         if self.start > self.end:
818             raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
819     @classmethod
820     def day(cls, day):
821         """Returns a range that only contains the given day"""
822         return cls(day,day)
823     def __contains__(self, date):
824         """Check if the date is in the range"""
825         if not isinstance(date, datetime.date):
826             date = date_from_str(date)
827         return self.start <= date <= self.end
828     def __str__(self):
829         return '%s - %s' % ( self.start.isoformat(), self.end.isoformat())
830
831
832 def platform_name():
833     """ Returns the platform name as a compat_str """
834     res = platform.platform()
835     if isinstance(res, bytes):
836         res = res.decode(preferredencoding())
837
838     assert isinstance(res, compat_str)
839     return res
840
841
842 def write_string(s, out=None):
843     if out is None:
844         out = sys.stderr
845     assert type(s) == type(u'')
846
847     if ('b' in getattr(out, 'mode', '') or
848             sys.version_info[0] < 3):  # Python 2 lies about mode of sys.stderr
849         s = s.encode(preferredencoding(), 'ignore')
850     out.write(s)
851     out.flush()
852
853
854 def bytes_to_intlist(bs):
855     if not bs:
856         return []
857     if isinstance(bs[0], int):  # Python 3
858         return list(bs)
859     else:
860         return [ord(c) for c in bs]
861
862
863 def intlist_to_bytes(xs):
864     if not xs:
865         return b''
866     if isinstance(chr(0), bytes):  # Python 2
867         return ''.join([chr(x) for x in xs])
868     else:
869         return bytes(xs)
870
871
872 def get_cachedir(params={}):
873     cache_root = os.environ.get('XDG_CACHE_HOME',
874                                 os.path.expanduser('~/.cache'))
875     return params.get('cachedir', os.path.join(cache_root, 'youtube-dl'))
876
877
878 # Cross-platform file locking
879 if sys.platform == 'win32':
880     import ctypes.wintypes
881     import msvcrt
882
883     class OVERLAPPED(ctypes.Structure):
884         _fields_ = [
885             ('Internal', ctypes.wintypes.LPVOID),
886             ('InternalHigh', ctypes.wintypes.LPVOID),
887             ('Offset', ctypes.wintypes.DWORD),
888             ('OffsetHigh', ctypes.wintypes.DWORD),
889             ('hEvent', ctypes.wintypes.HANDLE),
890         ]
891
892     kernel32 = ctypes.windll.kernel32
893     LockFileEx = kernel32.LockFileEx
894     LockFileEx.argtypes = [
895         ctypes.wintypes.HANDLE,     # hFile
896         ctypes.wintypes.DWORD,      # dwFlags
897         ctypes.wintypes.DWORD,      # dwReserved
898         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
899         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
900         ctypes.POINTER(OVERLAPPED)  # Overlapped
901     ]
902     LockFileEx.restype = ctypes.wintypes.BOOL
903     UnlockFileEx = kernel32.UnlockFileEx
904     UnlockFileEx.argtypes = [
905         ctypes.wintypes.HANDLE,     # hFile
906         ctypes.wintypes.DWORD,      # dwReserved
907         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockLow
908         ctypes.wintypes.DWORD,      # nNumberOfBytesToLockHigh
909         ctypes.POINTER(OVERLAPPED)  # Overlapped
910     ]
911     UnlockFileEx.restype = ctypes.wintypes.BOOL
912     whole_low = 0xffffffff
913     whole_high = 0x7fffffff
914
915     def _lock_file(f, exclusive):
916         overlapped = OVERLAPPED()
917         overlapped.Offset = 0
918         overlapped.OffsetHigh = 0
919         overlapped.hEvent = 0
920         f._lock_file_overlapped_p = ctypes.pointer(overlapped)
921         handle = msvcrt.get_osfhandle(f.fileno())
922         if not LockFileEx(handle, 0x2 if exclusive else 0x0, 0,
923                           whole_low, whole_high, f._lock_file_overlapped_p):
924             raise OSError('Locking file failed: %r' % ctypes.FormatError())
925
926     def _unlock_file(f):
927         assert f._lock_file_overlapped_p
928         handle = msvcrt.get_osfhandle(f.fileno())
929         if not UnlockFileEx(handle, 0,
930                             whole_low, whole_high, f._lock_file_overlapped_p):
931             raise OSError('Unlocking file failed: %r' % ctypes.FormatError())
932
933 else:
934     import fcntl
935
936     def _lock_file(f, exclusive):
937         fcntl.lockf(f, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
938
939     def _unlock_file(f):
940         fcntl.lockf(f, fcntl.LOCK_UN)
941
942
943 class locked_file(object):
944     def __init__(self, filename, mode, encoding=None):
945         assert mode in ['r', 'a', 'w']
946         self.f = io.open(filename, mode, encoding=encoding)
947         self.mode = mode
948
949     def __enter__(self):
950         exclusive = self.mode != 'r'
951         try:
952             _lock_file(self.f, exclusive)
953         except IOError:
954             self.f.close()
955             raise
956         return self
957
958     def __exit__(self, etype, value, traceback):
959         try:
960             _unlock_file(self.f)
961         finally:
962             self.f.close()
963
964     def __iter__(self):
965         return iter(self.f)
966
967     def write(self, *args):
968         return self.f.write(*args)
969
970     def read(self, *args):
971         return self.f.read(*args)
972
973
974 def shell_quote(args):
975     quoted_args = []
976     encoding = sys.getfilesystemencoding()
977     if encoding is None:
978         encoding = 'utf-8'
979     for a in args:
980         if isinstance(a, bytes):
981             # We may get a filename encoded with 'encodeFilename'
982             a = a.decode(encoding)
983         quoted_args.append(pipes.quote(a))
984     return u' '.join(quoted_args)
985
986
987 def takewhile_inclusive(pred, seq):
988     """ Like itertools.takewhile, but include the latest evaluated element
989         (the first element so that Not pred(e)) """
990     for e in seq:
991         yield e
992         if not pred(e):
993             return
994
995
996 def smuggle_url(url, data):
997     """ Pass additional data in a URL for internal use. """
998
999     sdata = compat_urllib_parse.urlencode(
1000         {u'__youtubedl_smuggle': json.dumps(data)})
1001     return url + u'#' + sdata
1002
1003
1004 def unsmuggle_url(smug_url):
1005     if not '#__youtubedl_smuggle' in smug_url:
1006         return smug_url, None
1007     url, _, sdata = smug_url.rpartition(u'#')
1008     jsond = compat_parse_qs(sdata)[u'__youtubedl_smuggle'][0]
1009     data = json.loads(jsond)
1010     return url, data
1011
1012
1013 def parse_xml_doc(s):
1014     assert isinstance(s, type(u''))
1015     return xml.etree.ElementTree.fromstring(s.encode('utf-8'))
1016
1017
1018 def format_bytes(bytes):
1019     if bytes is None:
1020         return u'N/A'
1021     if type(bytes) is str:
1022         bytes = float(bytes)
1023     if bytes == 0.0:
1024         exponent = 0
1025     else:
1026         exponent = int(math.log(bytes, 1024.0))
1027     suffix = [u'B', u'KiB', u'MiB', u'GiB', u'TiB', u'PiB', u'EiB', u'ZiB', u'YiB'][exponent]
1028     converted = float(bytes) / float(1024 ** exponent)
1029     return u'%.2f%s' % (converted, suffix)