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