diff --git a/src/calibre/srv/http_request.py b/src/calibre/srv/http_request.py index 8b18489f36..27de341433 100644 --- a/src/calibre/srv/http_request.py +++ b/src/calibre/srv/http_request.py @@ -154,6 +154,7 @@ class HTTPRequest(Connection): request_handler = None static_cache = None + translator_cache = None def __init__(self, *args, **kwargs): Connection.__init__(self, *args, **kwargs) diff --git a/src/calibre/srv/http_response.py b/src/calibre/srv/http_response.py index 176aae3206..3e3d23a82e 100644 --- a/src/calibre/srv/http_response.py +++ b/src/calibre/srv/http_response.py @@ -19,7 +19,9 @@ from calibre.srv.loop import WRITE from calibre.srv.errors import HTTPSimpleResponse from calibre.srv.http_request import HTTPRequest, read_headers from calibre.srv.sendfile import file_metadata, sendfile_to_socket_async, CannotSendfile, SendfileInterrupted -from calibre.srv.utils import MultiDict, http_date, HTTP1, HTTP11, socket_errors_socket_closed +from calibre.srv.utils import ( + MultiDict, http_date, HTTP1, HTTP11, socket_errors_socket_closed, + sort_q_values, get_translator_for_lang) from calibre.utils.monotonic import monotonic Range = namedtuple('Range', 'start stop size') @@ -71,21 +73,19 @@ def parse_if_none_match(val): # {{{ # }}} def acceptable_encoding(val, allowed=frozenset({'gzip'})): # {{{ - def enc(x): - e, r = x.partition(';')[::2] - p, v = r.partition('=')[::2] - q = 1.0 - if p == 'q' and v: - try: - q = float(v) - except Exception: - pass - return e.lower(), q + for x in sort_q_values(val): + x = x.lower() + if x in allowed: + return x +# }}} - emap = dict(enc(x.strip()) for x in val.split(',')) - acceptable = sorted(set(emap) & allowed, key=emap.__getitem__, reverse=True) - if acceptable: - return acceptable[0] +def preferred_lang(val, get_translator_for_lang): # {{{ + for x in sort_q_values(val): + x = x.lower() + found, lang, translator = get_translator_for_lang(x) + if found: + return x + return 'en' # }}} def get_ranges(headervalue, content_length): # {{{ @@ -180,9 +180,13 @@ def get_range_parts(ranges, content_type, content_length): # {{{ class RequestData(object): # {{{ - def __init__(self, method, path, query, inheaders, request_body_file, outheaders, response_protocol, static_cache, opts, remote_addr, remote_port): - self.method, self.path, self.query, self.inheaders, self.request_body_file, self.outheaders, self.response_protocol, self.static_cache = ( - method, path, query, inheaders, request_body_file, outheaders, response_protocol, static_cache + def __init__(self, method, path, query, inheaders, request_body_file, outheaders, response_protocol, + static_cache, opts, remote_addr, remote_port, translator_cache): + + (self.method, self.path, self.query, self.inheaders, self.request_body_file, self.outheaders, + self.response_protocol, self.static_cache, self.translator_cache) = ( + method, path, query, inheaders, request_body_file, outheaders, + response_protocol, static_cache, translator_cache ) self.remote_addr, self.remote_port = remote_addr, remote_port self.opts = opts @@ -196,6 +200,12 @@ class RequestData(object): # {{{ def read(self, size=-1): return self.request_body_file.read(size) + + def get_translator(self, bcp_47_code): + return get_translator_for_lang(self.translator_cache, bcp_47_code) + + def get_preferred_language(self): + return preferred_lang(self.outheaders.get('Accept-Language'), self.get_translator) # }}} class ReadableOutput(object): @@ -322,7 +332,7 @@ class HTTPConnection(HTTPRequest): data = RequestData( self.method, self.path, self.query, inheaders, request_body_file, outheaders, self.response_protocol, self.static_cache, self.opts, - self.remote_addr, self.remote_port + self.remote_addr, self.remote_port, self.translator_cache ) self.queue_job(self.run_request_handler, data) @@ -545,10 +555,12 @@ class HTTPConnection(HTTPRequest): def create_http_handler(handler): static_cache = {} + translator_cache = {} @wraps(handler) def wrapper(*args, **kwargs): ans = HTTPConnection(*args, **kwargs) ans.request_handler = handler ans.static_cache = static_cache + ans.translator_cache = translator_cache return ans return wrapper diff --git a/src/calibre/srv/tests/http.py b/src/calibre/srv/tests/http.py index 125784dfeb..0021ea98fc 100644 --- a/src/calibre/srv/tests/http.py +++ b/src/calibre/srv/tests/http.py @@ -66,6 +66,18 @@ class TestHTTP(BaseTest): test('Priority', '1;q=0.5, 2;q=0.75, 3;q=1.0', '3', {'1', '2', '3'}) # }}} + def test_accept_language(self): # {{{ + 'Test parsing of Accept-Language' + from calibre.srv.http_response import preferred_lang + def test(name, val, ans): + self.ae(preferred_lang(val, lambda x:(True, x, None)), ans, name + ' failed') + test('Empty field', '', 'en') + test('Simple', 'de', 'de') + test('Case insensitive', 'Es', 'es') + test('Multiple', 'fr, es', 'fr') + test('Priority', 'en;q=0.1, de;q=0.7, fr;q=0.5', 'de') + # }}} + def test_range_parsing(self): # {{{ 'Test parsing of Range header' from calibre.srv.http_response import get_ranges diff --git a/src/calibre/srv/utils.py b/src/calibre/srv/utils.py index b11648559a..4ce8125480 100644 --- a/src/calibre/srv/utils.py +++ b/src/calibre/srv/utils.py @@ -12,10 +12,12 @@ from urlparse import parse_qs import repr as reprlib from email.utils import formatdate from operator import itemgetter +from future_builtins import map from calibre import prints from calibre.constants import iswindows from calibre.utils.filenames import atomic_rename +from calibre.utils.localization import get_translator from calibre.utils.socket_inheritance import set_socket_inherit from calibre.utils.logging import ThreadSafeLog @@ -169,6 +171,22 @@ def create_sock_pair(port=0): return client_sock, srv_sock +def sort_q_values(header_val): + 'Get sorted items from an HTTP header of type: a;q=0.5, b;q=0.7...' + if not header_val: + return [] + def item(x): + e, r = x.partition(';')[::2] + p, v = r.partition('=')[::2] + q = 1.0 + if p == 'q' and v: + try: + q = max(0.0, min(1.0, float(v.strip()))) + except Exception: + pass + return e.strip(), q + return tuple(map(itemgetter(0), sorted(map(item, header_val.split(',')), key=itemgetter(1), reverse=True))) + def eintr_retry_call(func, *args, **kwargs): while True: try: @@ -178,6 +196,14 @@ def eintr_retry_call(func, *args, **kwargs): continue raise +def get_translator_for_lang(cache, bcp_47_code): + try: + return cache[bcp_47_code] + except KeyError: + pass + cache[bcp_47_code] = ans = get_translator(bcp_47_code) + return ans + # Logging {{{ class ServerLog(ThreadSafeLog): diff --git a/src/calibre/utils/localization.py b/src/calibre/utils/localization.py index f316607f1d..33660cd324 100644 --- a/src/calibre/utils/localization.py +++ b/src/calibre/utils/localization.py @@ -107,6 +107,30 @@ def get_all_translators(): buf = cStringIO.StringIO(zf.read(mpath + '/messages.mo')) yield lang, GNUTranslations(buf) +def get_single_translator(mpath): + from zipfile import ZipFile + with ZipFile(P('localization/locales.zip', allow_user_override=False), 'r') as zf: + buf = cStringIO.StringIO(zf.read(mpath + '/messages.mo')) + return GNUTranslations(buf) + +def get_translator(bcp_47_code): + parts = bcp_47_code.replace('-', '_').split('_')[:2] + parts[0] = lang_as_iso639_1(parts[0].lower()) + if len(parts) > 1: + parts[1] = parts[1].upper() + lang = '_'.join(parts) + lang = {'pt':'pt_BR', 'zh':'zh_CN'}.get(lang, lang) + available = available_translations() + found = True + if lang not in available: + lang = {'pt':'pt_BR', 'zh':'zh_CN'}.get(parts[0], parts[0]) + if lang not in available: + lang = get_lang() + found = False + if lang == 'en': + return found, lang, NullTranslations() + return found, lang, get_single_translator(lang) + lcdata = { u'abday': (u'Sun', u'Mon', u'Tue', u'Wed', u'Thu', u'Fri', u'Sat'), u'abmon': (u'Jan', u'Feb', u'Mar', u'Apr', u'May', u'Jun', u'Jul', u'Aug', u'Sep', u'Oct', u'Nov', u'Dec'),