diff --git a/imgsrc/languages.svg b/imgsrc/languages.svg new file mode 100644 index 0000000000..b45019a56d --- /dev/null +++ b/imgsrc/languages.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/recipes/el_mostrador.recipe b/recipes/el_mostrador.recipe new file mode 100644 index 0000000000..ab487f3c17 --- /dev/null +++ b/recipes/el_mostrador.recipe @@ -0,0 +1,40 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1313609361(BasicNewsRecipe): + news = True + title = u'El Mostrador' + __author__ = 'Alex Mitrani' + description = u'Chilean online newspaper' + publisher = u'La Plaza S.A.' + category = 'news, rss' + oldest_article = 7 + max_articles_per_feed = 100 + summary_length = 1000 + language = 'es_CL' + remove_javascript = True + no_stylesheets = True + use_embedded_content = False + remove_empty_feeds = True + masthead_url = 'http://www.elmostrador.cl/assets/img/logo-elmostrador-m.jpg' + remove_tags_before = dict(name='div', attrs={'class':'news-heading cf'}) + remove_tags_after = dict(name='div', attrs={'class':'footer-actions cf'}) + remove_tags = [dict(name='div', attrs={'class':'footer-actions cb cf'}) + ,dict(name='div', attrs={'class':'news-aside fl'}) + ,dict(name='div', attrs={'class':'footer-actions cf'}) + ,dict(name='div', attrs={'class':'user-bar','id':'top'}) + ,dict(name='div', attrs={'class':'indicators'}) + ,dict(name='div', attrs={'id':'header'}) + ] + + + feeds = [(u'Temas Destacados' + , u'http://www.elmostrador.cl/destacado/feed/') + , (u'El D\xeda', u'http://www.elmostrador.cl/dia/feed/') + , (u'Pa\xeds', u'http://www.elmostrador.cl/noticias/pais/feed/') + , (u'Mundo', u'http://www.elmostrador.cl/noticias/mundo/feed/') + , (u'Negocios', u'http://www.elmostrador.cl/noticias/negocios/feed/') + , (u'Cultura', u'http://www.elmostrador.cl/noticias/cultura/feed/') + , (u'Vida en L\xednea', u'http://www.elmostrador.cl/vida-en-linea/feed/') + , (u'Opini\xf3n & Blogs', u'http://www.elmostrador.cl/opinion/feed/') + ] + diff --git a/recipes/metro_news_nl.recipe b/recipes/metro_news_nl.recipe index 180dab079f..4c1a153d6d 100644 --- a/recipes/metro_news_nl.recipe +++ b/recipes/metro_news_nl.recipe @@ -2,6 +2,9 @@ from calibre.web.feeds.news import BasicNewsRecipe class AdvancedUserRecipe1306097511(BasicNewsRecipe): title = u'Metro Nieuws NL' + description = u'Metro Nieuws - NL' +# Version 1.2, updated cover image to match the changed website. +# added info date on title oldest_article = 2 max_articles_per_feed = 100 __author__ = u'DrMerry' @@ -10,11 +13,11 @@ class AdvancedUserRecipe1306097511(BasicNewsRecipe): simultaneous_downloads = 5 delay = 1 # timefmt = ' [%A, %d %B, %Y]' - timefmt = '' + timefmt = ' [%A, %d %b %Y]' no_stylesheets = True remove_javascript = True remove_empty_feeds = True - cover_url = 'http://www.readmetro.com/img/en/metroholland/last/1/small.jpg' + cover_url = 'http://www.oldreadmetro.com/img/en/metroholland/last/1/small.jpg' remove_empty_feeds = True publication_type = 'newspaper' remove_tags_before = dict(name='div', attrs={'id':'date'}) diff --git a/recipes/the_clinic_online.recipe b/recipes/the_clinic_online.recipe new file mode 100644 index 0000000000..e25a9f3124 --- /dev/null +++ b/recipes/the_clinic_online.recipe @@ -0,0 +1,27 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1313555075(BasicNewsRecipe): + news = True + title = u'The Clinic' + __author__ = 'Alex Mitrani' + description = u'Online version of Chilean satirical weekly' + publisher = u'The Clinic' + category = 'news, politics, Chile, rss' + oldest_article = 7 + max_articles_per_feed = 100 + summary_length = 1000 + language = 'es_CL' + + remove_javascript = True + no_stylesheets = True + use_embedded_content = False + remove_empty_feeds = True + masthead_url = 'http://www.theclinic.cl/wp-content/themes/tc12m/css/ui/mainLogoTC-top.png' + remove_tags_before = dict(name='article', attrs={'class':'scope bordered'}) + remove_tags_after = dict(name='div', attrs={'id':'commentsSection'}) + remove_tags = [dict(name='span', attrs={'class':'relTags'}) + ,dict(name='div', attrs={'class':'articleActivity hdcol'}) + ,dict(name='div', attrs={'id':'commentsSection'}) + ] + + feeds = [(u'The Clinic Online', u'http://www.theclinic.cl/feed/')] diff --git a/resources/images/languages.png b/resources/images/languages.png new file mode 100644 index 0000000000..ce2b2c0e15 Binary files /dev/null and b/resources/images/languages.png differ diff --git a/setup/translations.py b/setup/translations.py index 611b3b2d68..2e8e6d52f3 100644 --- a/setup/translations.py +++ b/setup/translations.py @@ -291,6 +291,8 @@ class ISO639(Command): by_3t = {} m2to3 = {} m3to2 = {} + m3bto3t = {} + nm = {} codes2, codes3t, codes3b = set([]), set([]), set([]) for x in root.xpath('//iso_639_entry'): name = x.get('name') @@ -304,12 +306,19 @@ class ISO639(Command): m3to2[threeb] = m3to2[threet] = two by_3b[threeb] = name by_3t[threet] = name + if threeb != threet: + m3bto3t[threeb] = threet codes3b.add(x.get('iso_639_2B_code')) codes3t.add(x.get('iso_639_2T_code')) + base_name = name.lower() + nm[base_name] = threet + simple_name = base_name.partition(';')[0].strip() + if simple_name not in nm: + nm[simple_name] = threet from cPickle import dump x = {'by_2':by_2, 'by_3b':by_3b, 'by_3t':by_3t, 'codes2':codes2, 'codes3b':codes3b, 'codes3t':codes3t, '2to3':m2to3, - '3to2':m3to2} + '3to2':m3to2, '3bto3t':m3bto3t, 'name_map':nm} dump(x, open(dest, 'wb'), -1) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index f22b67dcd1..8e4678ebb5 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -64,6 +64,7 @@ class ANDROID(USBMS): 0x6860 : [0x0400], 0x6877 : [0x0400], 0x689e : [0x0400], + 0xdeed : [0x0222], }, # Viewsonic @@ -132,7 +133,7 @@ class ANDROID(USBMS): '7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2', 'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE', 'A7EB', 'STREAK', 'MB525', 'ANDROID2.3', 'SGH-I997', 'GT-I5800_CARD', 'MB612', - 'GT-S5830_CARD', 'GT-S5570_CARD', 'MB870'] + 'GT-S5830_CARD', 'GT-S5570_CARD', 'MB870', 'MID7015A'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD', diff --git a/src/calibre/devices/kindle/driver.py b/src/calibre/devices/kindle/driver.py index b027542bf0..3c875ba9d9 100644 --- a/src/calibre/devices/kindle/driver.py +++ b/src/calibre/devices/kindle/driver.py @@ -64,7 +64,7 @@ class KINDLE(USBMS): EBOOK_DIR_MAIN = 'documents' EBOOK_DIR_CARD_A = 'documents' - DELETE_EXTS = ['.mbp','.tan','.pdr'] + DELETE_EXTS = ['.mbp', '.tan', '.pdr', '.ea', '.apnx', '.phl'] SUPPORTS_SUB_DIRS = True SUPPORTS_ANNOTATIONS = True diff --git a/src/calibre/devices/misc.py b/src/calibre/devices/misc.py index 92fce68f11..90d03f073a 100644 --- a/src/calibre/devices/misc.py +++ b/src/calibre/devices/misc.py @@ -252,8 +252,8 @@ class EEEREADER(USBMS): EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'Book' - VENDOR_NAME = 'LINUX' - WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET' + VENDOR_NAME = ['LINUX', 'ASUS'] + WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['FILE-STOR_GADGET', 'EEE_NOTE'] class ADAM(USBMS): diff --git a/src/calibre/devices/usbms/cli.py b/src/calibre/devices/usbms/cli.py index 1554d6fce0..4ff9efef8b 100644 --- a/src/calibre/devices/usbms/cli.py +++ b/src/calibre/devices/usbms/cli.py @@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en' import os, shutil, time from calibre.devices.errors import PathError +from calibre.utils.filenames import case_preserving_open_file class File(object): @@ -46,10 +47,8 @@ class CLI(object): path = os.path.join(path, infile.name) if not replace_file and os.path.exists(path): raise PathError('File already exists: ' + path) - d = os.path.dirname(path) - if not os.path.exists(d): - os.makedirs(d) - with open(path, 'w+b') as dest: + dest, actual_path = case_preserving_open_file(path) + with dest: try: shutil.copyfileobj(infile, dest) except IOError: @@ -62,6 +61,7 @@ class CLI(object): #if not check_transfer(infile, dest): raise Exception('Transfer failed') if close: infile.close() + return actual_path def munge_path(self, path): if path.startswith('/') and not (path.startswith(self._main_prefix) or \ diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 89531ec057..e09876081b 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -258,10 +258,10 @@ class USBMS(CLI, Device): for i, infile in enumerate(files): mdata, fname = metadata.next(), names.next() filepath = self.normalize_path(self.create_upload_path(path, mdata, fname)) - paths.append(filepath) if not hasattr(infile, 'read'): infile = self.normalize_path(infile) - self.put_file(infile, filepath, replace_file=True) + filepath = self.put_file(infile, filepath, replace_file=True) + paths.append(filepath) try: self.upload_cover(os.path.dirname(filepath), os.path.splitext(os.path.basename(filepath))[0], diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py index 50ad2b0b50..c2e338ea10 100644 --- a/src/calibre/ebooks/__init__.py +++ b/src/calibre/ebooks/__init__.py @@ -28,8 +28,9 @@ class ParserError(ValueError): BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'txtz', 'text', 'htm', 'xhtm', 'html', 'htmlz', 'xhtml', 'pdf', 'pdb', 'pdr', 'prc', 'mobi', 'azw', 'doc', - 'epub', 'fb2', 'djvu', 'lrx', 'cbr', 'cbz', 'cbc', 'oebzip', - 'rb', 'imp', 'odt', 'chm', 'tpz', 'azw1', 'pml', 'pmlz', 'mbp', 'tan', 'snb'] + 'epub', 'fb2', 'djv', 'djvu', 'lrx', 'cbr', 'cbz', 'cbc', 'oebzip', + 'rb', 'imp', 'odt', 'chm', 'tpz', 'azw1', 'pml', 'pmlz', 'mbp', 'tan', 'snb', + 'xps', 'oxps'] class HTMLRenderer(object): diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py index 50e7b916ee..38a824374c 100644 --- a/src/calibre/ebooks/metadata/book/__init__.py +++ b/src/calibre/ebooks/metadata/book/__init__.py @@ -47,8 +47,7 @@ PUBLICATION_METADATA_FIELDS = frozenset([ # If None, means book 'publication_type', 'uuid', # A UUID usually of type 4 - 'language', # the primary language of this book - 'languages', # ordered list + 'languages', # ordered list of languages in this publication 'publisher', # Simple string, no special semantics # Absolute path to image file encoded in filesystem_encoding 'cover', @@ -109,7 +108,7 @@ STANDARD_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union( # Metadata fields that smart update must do special processing to copy. SC_FIELDS_NOT_COPIED = frozenset(['title', 'title_sort', 'authors', 'author_sort', 'author_sort_map', - 'cover_data', 'tags', 'language', + 'cover_data', 'tags', 'languages', 'identifiers']) # Metadata fields that smart update should copy only if the source is not None diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 7c56dcabb4..1d2838c135 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -102,6 +102,7 @@ class Metadata(object): @param other: None or a metadata object ''' _data = copy.deepcopy(NULL_VALUES) + _data.pop('language') object.__setattr__(self, '_data', _data) if other is not None: self.smart_update(other) @@ -136,6 +137,11 @@ class Metadata(object): _data = object.__getattribute__(self, '_data') if field in TOP_LEVEL_IDENTIFIERS: return _data.get('identifiers').get(field, None) + if field == 'language': + try: + return _data.get('languages', [])[0] + except: + return NULL_VALUES['language'] if field in STANDARD_METADATA_FIELDS: return _data.get(field, None) try: @@ -175,6 +181,11 @@ class Metadata(object): if not val: val = copy.copy(NULL_VALUES.get('identifiers', None)) self.set_identifiers(val) + elif field == 'language': + langs = [] + if val and val.lower() != 'und': + langs = [val] + _data['languages'] = langs elif field in STANDARD_METADATA_FIELDS: if val is None: val = copy.copy(NULL_VALUES.get(field, None)) @@ -553,9 +564,9 @@ class Metadata(object): for attr in TOP_LEVEL_IDENTIFIERS: copy_not_none(self, other, attr) - other_lang = getattr(other, 'language', None) - if other_lang and other_lang.lower() != 'und': - self.language = other_lang + other_lang = getattr(other, 'languages', []) + if other_lang and other_lang != ['und']: + self.languages = list(other_lang) if not getattr(self, 'series', None): self.series_index = None @@ -706,8 +717,8 @@ class Metadata(object): fmt('Tags', u', '.join([unicode(t) for t in self.tags])) if self.series: fmt('Series', self.series + ' #%s'%self.format_series_index()) - if not self.is_null('language'): - fmt('Language', self.language) + if not self.is_null('languages'): + fmt('Languages', ', '.join(self.languages)) if self.rating is not None: fmt('Rating', self.rating) if self.timestamp is not None: @@ -743,7 +754,7 @@ class Metadata(object): ans += [(_('Tags'), u', '.join([unicode(t) for t in self.tags]))] if self.series: ans += [(_('Series'), unicode(self.series) + ' #%s'%self.format_series_index())] - ans += [(_('Language'), unicode(self.language))] + ans += [(_('Languages'), u', '.join(self.languages))] if self.timestamp is not None: ans += [(_('Timestamp'), unicode(self.timestamp.isoformat(' ')))] if self.pubdate is not None: diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index 35fd724ddd..9958ad75c9 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -19,7 +19,7 @@ from calibre.ebooks.metadata.toc import TOC from calibre.ebooks.metadata import string_to_authors, MetaInformation, check_isbn from calibre.ebooks.metadata.book.base import Metadata from calibre.utils.date import parse_date, isoformat -from calibre.utils.localization import get_lang +from calibre.utils.localization import get_lang, canonicalize_lang from calibre import prints, guess_type from calibre.utils.cleantext import clean_ascii_chars from calibre.utils.config import tweaks @@ -515,6 +515,7 @@ class OPF(object): # {{{ '(re:match(@opf:scheme, "calibre|libprs500", "i") or re:match(@scheme, "calibre|libprs500", "i"))]') uuid_id_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+ '(re:match(@opf:scheme, "uuid", "i") or re:match(@scheme, "uuid", "i"))]') + languages_path = XPath('descendant::*[local-name()="language"]') manifest_path = XPath('descendant::*[re:match(name(), "manifest", "i")]/*[re:match(name(), "item", "i")]') manifest_ppath = XPath('descendant::*[re:match(name(), "manifest", "i")]') @@ -523,7 +524,6 @@ class OPF(object): # {{{ title = MetadataField('title', formatter=lambda x: re.sub(r'\s+', ' ', x)) publisher = MetadataField('publisher') - language = MetadataField('language') comments = MetadataField('description') category = MetadataField('type') rights = MetadataField('rights') @@ -930,6 +930,44 @@ class OPF(object): # {{{ return property(fget=fget, fset=fset) + @dynamic_property + def language(self): + + def fget(self): + ans = self.languages + if ans: + return ans[0] + + def fset(self, val): + self.languages = [val] + + return property(fget=fget, fset=fset) + + + @dynamic_property + def languages(self): + + def fget(self): + ans = [] + for match in self.languages_path(self.metadata): + t = self.get_text(match) + if t and t.strip(): + l = canonicalize_lang(t.strip()) + if l: + ans.append(l) + return ans + + def fset(self, val): + matches = self.languages_path(self.metadata) + for x in matches: + x.getparent().remove(x) + + for lang in val: + l = self.create_metadata_element('language') + self.set_text(l, unicode(lang)) + + return property(fget=fget, fset=fset) + @dynamic_property def book_producer(self): @@ -1052,9 +1090,9 @@ class OPF(object): # {{{ val = getattr(mi, attr, None) if val is not None and val != [] and val != (None, None): setattr(self, attr, val) - lang = getattr(mi, 'language', None) - if lang and lang != 'und': - self.language = lang + langs = getattr(mi, 'languages', []) + if langs and langs != ['und']: + self.languages = langs temp = self.to_book_metadata() temp.smart_update(mi, replace_metadata=replace_metadata) self._user_metadata_ = temp.get_all_user_metadata(True) @@ -1202,10 +1240,11 @@ class OPFCreator(Metadata): dc_attrs={'id':__appname__+'_id'})) if getattr(self, 'pubdate', None) is not None: a(DC_ELEM('date', self.pubdate.isoformat())) - lang = self.language - if not lang or lang.lower() == 'und': - lang = get_lang().replace('_', '-') - a(DC_ELEM('language', lang)) + langs = self.languages + if not langs or langs == ['und']: + langs = [get_lang().replace('_', '-').partition('-')[0]] + for lang in langs: + a(DC_ELEM('language', lang)) if self.comments: a(DC_ELEM('description', self.comments)) if self.publisher: @@ -1288,8 +1327,9 @@ def metadata_to_opf(mi, as_string=True): mi.book_producer = __appname__ + ' (%s) '%__version__ + \ '[http://calibre-ebook.com]' - if not mi.language: - mi.language = 'UND' + if not mi.languages: + lang = get_lang().replace('_', '-').partition('-')[0] + mi.languages = [lang] root = etree.fromstring(textwrap.dedent( ''' @@ -1339,8 +1379,10 @@ def metadata_to_opf(mi, as_string=True): factory(DC('identifier'), val, scheme=icu_upper(key)) if mi.rights: factory(DC('rights'), mi.rights) - factory(DC('language'), mi.language if mi.language and mi.language.lower() - != 'und' else get_lang().replace('_', '-')) + for lang in mi.languages: + if not lang or lang.lower() == 'und': + continue + factory(DC('language'), lang) if mi.tags: for tag in mi.tags: factory(DC('subject'), tag) diff --git a/src/calibre/ebooks/metadata/sources/amazon.py b/src/calibre/ebooks/metadata/sources/amazon.py index 6220f29020..aaa13d5769 100644 --- a/src/calibre/ebooks/metadata/sources/amazon.py +++ b/src/calibre/ebooks/metadata/sources/amazon.py @@ -22,6 +22,7 @@ from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.metadata.book.base import Metadata from calibre.library.comments import sanitize_comments_html from calibre.utils.date import parse_date +from calibre.utils.localization import canonicalize_lang class Worker(Thread): # Get details {{{ @@ -106,10 +107,11 @@ class Worker(Thread): # Get details {{{ r'([0-9.]+) (out of|von|su|étoiles sur) (\d+)( (stars|Sternen|stelle)){0,1}') lm = { - 'en': ('English', 'Englisch'), - 'fr': ('French', 'Français'), - 'it': ('Italian', 'Italiano'), - 'de': ('German', 'Deutsch'), + 'eng': ('English', 'Englisch'), + 'fra': ('French', 'Français'), + 'ita': ('Italian', 'Italiano'), + 'deu': ('German', 'Deutsch'), + 'spa': ('Spanish', 'Espa\xf1ol', 'Espaniol'), } self.lang_map = {} for code, names in lm.iteritems(): @@ -374,8 +376,11 @@ class Worker(Thread): # Get details {{{ def parse_language(self, pd): for x in reversed(pd.xpath(self.language_xpath)): if x.tail: - ans = x.tail.strip() - ans = self.lang_map.get(ans, None) + raw = x.tail.strip() + ans = self.lang_map.get(raw, None) + if ans: + return ans + ans = canonicalize_lang(ans) if ans: return ans # }}} @@ -388,7 +393,7 @@ class Amazon(Source): capabilities = frozenset(['identify', 'cover']) touched_fields = frozenset(['title', 'authors', 'identifier:amazon', 'identifier:isbn', 'rating', 'comments', 'publisher', 'pubdate', - 'language']) + 'languages']) has_html_comments = True supports_gzip_transfer_encoding = True diff --git a/src/calibre/ebooks/metadata/sources/google.py b/src/calibre/ebooks/metadata/sources/google.py index bd1043b774..f9c43d86cc 100644 --- a/src/calibre/ebooks/metadata/sources/google.py +++ b/src/calibre/ebooks/metadata/sources/google.py @@ -20,6 +20,7 @@ from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.chardet import xml_to_unicode from calibre.utils.date import parse_date, utcnow from calibre.utils.cleantext import clean_ascii_chars +from calibre.utils.localization import canonicalize_lang from calibre import as_unicode NAMESPACES = { @@ -95,7 +96,9 @@ def to_metadata(browser, log, entry_, timeout): # {{{ return mi mi.comments = get_text(extra, description) - #mi.language = get_text(extra, language) + lang = canonicalize_lang(get_text(extra, language)) + if lang: + mi.language = lang mi.publisher = get_text(extra, publisher) # ISBN @@ -162,7 +165,7 @@ class GoogleBooks(Source): capabilities = frozenset(['identify', 'cover']) touched_fields = frozenset(['title', 'authors', 'tags', 'pubdate', 'comments', 'publisher', 'identifier:isbn', 'rating', - 'identifier:google']) # language currently disabled + 'identifier:google', 'languages']) supports_gzip_transfer_encoding = True cached_cover_url_is_reliable = False diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index a7bcbc5a89..97fbae4727 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -484,6 +484,7 @@ def identify(log, abort, # {{{ 'publication dates') start_time = time.time() results = merge_identify_results(results, log) + log('We have %d merged results, merging took: %.2f seconds' % (len(results), time.time() - start_time)) diff --git a/src/calibre/ebooks/metadata/sources/overdrive.py b/src/calibre/ebooks/metadata/sources/overdrive.py index f52b1f423b..2e63a2e267 100755 --- a/src/calibre/ebooks/metadata/sources/overdrive.py +++ b/src/calibre/ebooks/metadata/sources/overdrive.py @@ -35,7 +35,7 @@ class OverDrive(Source): capabilities = frozenset(['identify', 'cover']) touched_fields = frozenset(['title', 'authors', 'tags', 'pubdate', 'comments', 'publisher', 'identifier:isbn', 'series', 'series_index', - 'language', 'identifier:overdrive']) + 'languages', 'identifier:overdrive']) has_html_comments = True supports_gzip_transfer_encoding = False cached_cover_url_is_reliable = True @@ -421,8 +421,10 @@ class OverDrive(Source): pass if lang: lang = lang[0].strip().lower() - mi.language = {'english':'en', 'french':'fr', 'german':'de', - 'spanish':'es'}.get(lang, None) + lang = {'english':'eng', 'french':'fra', 'german':'deu', + 'spanish':'spa'}.get(lang, None) + if lang: + mi.language = lang if ebook_isbn: #print "ebook isbn is "+str(ebook_isbn[0]) diff --git a/src/calibre/ebooks/mobi/langcodes.py b/src/calibre/ebooks/mobi/langcodes.py index 5d085906df..1b839dc54d 100644 --- a/src/calibre/ebooks/mobi/langcodes.py +++ b/src/calibre/ebooks/mobi/langcodes.py @@ -4,6 +4,7 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' from struct import pack +from calibre.utils.localization import lang_as_iso639_1 lang_codes = { } @@ -314,7 +315,8 @@ def iana2mobi(icode): subtags = list(icode.split('-')) while len(subtags) > 0: lang = subtags.pop(0).lower() - if lang in IANA_MOBI: + lang = lang_as_iso639_1(lang) + if lang and lang in IANA_MOBI: langdict = IANA_MOBI[lang] break diff --git a/src/calibre/ebooks/oeb/transforms/metadata.py b/src/calibre/ebooks/oeb/transforms/metadata.py index 0db24dd2ad..41d5421dde 100644 --- a/src/calibre/ebooks/oeb/transforms/metadata.py +++ b/src/calibre/ebooks/oeb/transforms/metadata.py @@ -61,9 +61,11 @@ def meta_info_to_oeb_metadata(mi, m, log, override_input_metadata=False): m.add('identifier', val, scheme=typ.upper()) if override_input_metadata and not set_isbn: m.filter('identifier', lambda x: x.scheme.lower() == 'isbn') - if not mi.is_null('language'): + if not mi.is_null('languages'): m.clear('language') - m.add('language', mi.language) + for lang in mi.languages: + if lang and lang.lower() not in ('und', ''): + m.add('language', lang) if not mi.is_null('series_index'): m.clear('series_index') m.add('series_index', mi.format_series_index()) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index dedec91a1c..fc02ad7fae 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -94,7 +94,7 @@ gprefs.defaults['book_display_fields'] = [ ('path', True), ('publisher', False), ('rating', False), ('author_sort', False), ('sort', False), ('timestamp', False), ('uuid', False), ('comments', True), ('id', False), ('pubdate', False), - ('last_modified', False), ('size', False), + ('last_modified', False), ('size', False), ('languages', False), ] gprefs.defaults['default_author_link'] = 'http://en.wikipedia.org/w/index.php?search={author}' gprefs.defaults['preserve_date_on_ctl'] = True diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index d7fb869400..a070b24986 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -24,6 +24,7 @@ from calibre.gui2 import (config, open_local_file, open_url, pixmap_to_data, from calibre.utils.icu import sort_key from calibre.utils.formatter import EvalFormatter from calibre.utils.date import is_date_undefined +from calibre.utils.localization import calibre_langcode_to_name def render_html(mi, css, vertical, widget, all_fields=False): # {{{ table = render_data(mi, all_fields=all_fields, @@ -152,6 +153,12 @@ def render_data(mi, use_roman_numbers=True, all_fields=False): authors.append(aut) ans.append((field, u'%s%s'%(name, u' & '.join(authors)))) + elif field == 'languages': + if not mi.languages: + continue + names = filter(None, map(calibre_langcode_to_name, mi.languages)) + ans.append((field, u'%s%s'%(name, + u', '.join(names)))) else: val = mi.format_field(field)[-1] if val is None: diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 1472107386..6e9dcf5116 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -134,7 +134,7 @@ class MyBlockingBusy(QDialog): # {{{ do_autonumber, do_remove_format, remove_format, do_swap_ta, \ do_remove_conv, do_auto_author, series, do_series_restart, \ series_start_value, do_title_case, cover_action, clear_series, \ - pubdate, adddate, do_title_sort = self.args + pubdate, adddate, do_title_sort, languages, clear_languages = self.args # first loop: do author and title. These will commit at the end of each @@ -238,6 +238,12 @@ class MyBlockingBusy(QDialog): # {{{ if do_remove_conv: self.db.delete_conversion_options(id, 'PIPE', commit=False) + + if clear_languages: + self.db.set_languages(id, [], notify=False, commit=False) + elif languages: + self.db.set_languages(id, languages, notify=False, commit=False) + elif self.current_phase == 3: # both of these are fast enough to just do them all for w in self.cc_widgets: @@ -329,6 +335,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): geom = gprefs.get('bulk_metadata_window_geometry', None) if geom is not None: self.restoreGeometry(bytes(geom)) + self.languages.setEditText('') self.exec_() def save_state(self, *args): @@ -352,6 +359,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): self.do_again = True self.accept() + # S&R {{{ def prepare_search_and_replace(self): self.search_for.initialize('bulk_edit_search_for') self.replace_with.initialize('bulk_edit_replace_with') @@ -796,6 +804,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): # permanent. Make sure it really is. self.db.commit() self.model.refresh_ids(list(books_to_refresh)) + # }}} def create_custom_column_editors(self): w = self.central_widget.widget(1) @@ -919,6 +928,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): do_auto_author = self.auto_author_sort.isChecked() do_title_case = self.change_title_to_title_case.isChecked() do_title_sort = self.update_title_sort.isChecked() + clear_languages = self.clear_languages.isChecked() + languages = self.languages.lang_codes pubdate = adddate = None if self.apply_pubdate.isChecked(): pubdate = qt_to_dt(self.pubdate.date()) @@ -937,7 +948,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): do_autonumber, do_remove_format, remove_format, do_swap_ta, do_remove_conv, do_auto_author, series, do_series_restart, series_start_value, do_title_case, cover_action, clear_series, - pubdate, adddate, do_title_sort) + pubdate, adddate, do_title_sort, languages, clear_languages) bb = MyBlockingBusy(_('Applying changes to %d books.\nPhase {0} {1}%%.') %len(self.ids), args, self.db, self.ids, diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index 59a68d6514..c2e6635f98 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -443,7 +443,7 @@ from the value in the box - + Remove &format: @@ -453,7 +453,7 @@ from the value in the box - + @@ -463,7 +463,7 @@ from the value in the box - + Qt::Vertical @@ -479,7 +479,7 @@ from the value in the box - + @@ -529,7 +529,7 @@ Future conversion of these books will use the default settings. - + Change &cover @@ -559,7 +559,7 @@ Future conversion of these books will use the default settings. - + Qt::Vertical @@ -572,6 +572,29 @@ Future conversion of these books will use the default settings. + + + + &Languages: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + languages + + + + + + + + + + Remove &all + + + @@ -1145,6 +1168,11 @@ not multiple and the destination field is multiple QLineEdit
widgets.h
+ + LanguagesEdit + QComboBox +
calibre/gui2/languages.h
+
authors diff --git a/src/calibre/gui2/keyboard.py b/src/calibre/gui2/keyboard.py index f63eb2ef7e..0876e7c6fa 100644 --- a/src/calibre/gui2/keyboard.py +++ b/src/calibre/gui2/keyboard.py @@ -20,7 +20,7 @@ from calibre.constants import DEBUG from calibre import prints from calibre.utils.icu import sort_key, lower from calibre.gui2 import NONE, error_dialog, info_dialog -from calibre.utils.search_query_parser import SearchQueryParser +from calibre.utils.search_query_parser import SearchQueryParser, ParseException from calibre.gui2.search_box import SearchBox2 ROOT = QModelIndex() @@ -53,6 +53,7 @@ def finalize(shortcuts, custom_keys_map={}): # {{{ if DEBUG: prints('Key %r for shortcut %s is already used by' ' %s, ignoring'%(x, shortcut['name'], seen[x]['name'])) + keys_map[unique_name] = () continue seen[x] = shortcut keys.append(ks) @@ -113,6 +114,8 @@ class Manager(QObject): # {{{ custom_keys_map = {un:tuple(keys) for un, keys in self.config.get( 'map', {}).iteritems()} self.keys_map = finalize(self.shortcuts, custom_keys_map=custom_keys_map) + #import pprint + #pprint.pprint(self.keys_map) # }}} @@ -149,7 +152,7 @@ class ConfigModel(QAbstractItemModel, SearchQueryParser): shortcut_map = {k:v.copy() for k, v in self.keyboard.shortcuts.iteritems()} for un, s in shortcut_map.iteritems(): - s['keys'] = tuple(self.keyboard.keys_map[un]) + s['keys'] = tuple(self.keyboard.keys_map.get(un, ())) s['unique_name'] = un s['group'] = [g for g, names in self.keyboard.groups.iteritems() if un in names][0] @@ -590,11 +593,19 @@ class ShortcutConfig(QWidget): # {{{ return self.view.state() == self.view.EditingState def find(self, query): - idx = self._model.find(query) + if not query: + return + try: + idx = self._model.find(query) + except ParseException: + self.search.search_done(False) + return + self.search.search_done(True) if not idx.isValid(): - return info_dialog(self, _('No matches'), - _('Could not find any matching shortcuts'), show=True, - show_copy_button=False) + info_dialog(self, _('No matches'), + _('Could not find any shortcuts matching %s')%query, + show=True, show_copy_button=False) + return self.highlight_index(idx) def highlight_index(self, idx): @@ -602,6 +613,7 @@ class ShortcutConfig(QWidget): # {{{ self.view.selectionModel().select(idx, self.view.selectionModel().ClearAndSelect) self.view.setCurrentIndex(idx) + self.view.setFocus(Qt.OtherFocusReason) def find_next(self, *args): idx = self.view.currentIndex() diff --git a/src/calibre/gui2/languages.py b/src/calibre/gui2/languages.py new file mode 100644 index 0000000000..95b2a0bd5b --- /dev/null +++ b/src/calibre/gui2/languages.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from calibre.gui2.complete import MultiCompleteComboBox +from calibre.utils.localization import lang_map +from calibre.utils.icu import sort_key + +class LanguagesEdit(MultiCompleteComboBox): + + def __init__(self, parent=None): + MultiCompleteComboBox.__init__(self, parent) + + self._lang_map = lang_map() + self._rmap = {v:k for k,v in self._lang_map.iteritems()} + + all_items = sorted(self._lang_map.itervalues(), + key=sort_key) + self.update_items_cache(all_items) + for item in all_items: + self.addItem(item) + + @dynamic_property + def lang_codes(self): + + def fget(self): + vals = [x.strip() for x in + unicode(self.lineEdit().text()).split(',')] + ans = [] + for name in vals: + if name: + code = self._rmap.get(name, None) + if code is not None: + ans.append(code) + return ans + + def fset(self, lang_codes): + ans = [] + for lc in lang_codes: + name = self._lang_map.get(lc, None) + if name is not None: + ans.append(name) + self.setEditText(', '.join(ans)) + + return property(fget=fget, fset=fset) + + def validate(self): + vals = [x.strip() for x in + unicode(self.lineEdit().text()).split(',')] + bad = [] + for name in vals: + if name: + code = self._rmap.get(name, None) + if code is None: + bad.append(name) + return bad + diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index c4ea05d38c..64c94980be 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -23,6 +23,7 @@ from calibre.utils.formatter import validation_formatter from calibre.utils.icu import sort_key from calibre.gui2.dialogs.comments_dialog import CommentsDialog from calibre.gui2.dialogs.template_dialog import TemplateDialog +from calibre.gui2.languages import LanguagesEdit class RatingDelegate(QStyledItemDelegate): # {{{ @@ -155,7 +156,7 @@ class TextDelegate(QStyledItemDelegate): # {{{ def __init__(self, parent): ''' Delegate for text data. If auto_complete_function needs to return a list - of text items to auto-complete with. The funciton is None no + of text items to auto-complete with. If the function is None no auto-complete will be used. ''' QStyledItemDelegate.__init__(self, parent) @@ -229,6 +230,20 @@ class CompleteDelegate(QStyledItemDelegate): # {{{ QStyledItemDelegate.setModelData(self, editor, model, index) # }}} +class LanguagesDelegate(QStyledItemDelegate): # {{{ + + def createEditor(self, parent, option, index): + editor = LanguagesEdit(parent) + ct = index.data(Qt.DisplayRole).toString() + editor.setEditText(ct) + editor.lineEdit().selectAll() + return editor + + def setModelData(self, editor, model, index): + val = ','.join(editor.lang_codes) + model.setData(index, QVariant(val), Qt.EditRole) +# }}} + class CcDateDelegate(QStyledItemDelegate): # {{{ ''' Delegate for custom columns dates. Because this delegate stores the diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index a0c103a33b..a0870b1e8d 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -25,6 +25,7 @@ from calibre.library.caches import (_match, CONTAINS_MATCH, EQUALS_MATCH, from calibre import strftime, isbytestring from calibre.constants import filesystem_encoding, DEBUG from calibre.gui2.library import DEFAULT_SORT +from calibre.utils.localization import calibre_langcode_to_name def human_readable(size, precision=1): """ Convert a size in bytes into megabytes """ @@ -64,6 +65,7 @@ class BooksModel(QAbstractTableModel): # {{{ 'tags' : _("Tags"), 'series' : ngettext("Series", 'Series', 1), 'last_modified' : _('Modified'), + 'languages' : _('Languages'), } def __init__(self, parent=None, buffer=40): @@ -71,7 +73,8 @@ class BooksModel(QAbstractTableModel): # {{{ self.db = None self.book_on_device = None self.editable_cols = ['title', 'authors', 'rating', 'publisher', - 'tags', 'series', 'timestamp', 'pubdate'] + 'tags', 'series', 'timestamp', 'pubdate', + 'languages'] self.default_image = default_image() self.sorted_on = DEFAULT_SORT self.sort_history = [self.sorted_on] @@ -540,6 +543,13 @@ class BooksModel(QAbstractTableModel): # {{{ else: return None + def languages(r, idx=-1): + lc = self.db.data[r][idx] + if lc: + langs = [calibre_langcode_to_name(l.strip()) for l in lc.split(',')] + return QVariant(', '.join(langs)) + return None + def tags(r, idx=-1): tags = self.db.data[r][idx] if tags: @@ -641,6 +651,8 @@ class BooksModel(QAbstractTableModel): # {{{ siix=self.db.field_metadata['series_index']['rec_index']), 'ondevice' : functools.partial(text_type, idx=self.db.field_metadata['ondevice']['rec_index'], mult=None), + 'languages': functools.partial(languages, + idx=self.db.field_metadata['languages']['rec_index']), } self.dc_decorator = { @@ -884,6 +896,9 @@ class BooksModel(QAbstractTableModel): # {{{ if val.isNull() or not val.isValid(): return False self.db.set_pubdate(id, qt_to_dt(val, as_utc=False)) + elif column == 'languages': + val = val.split(',') + self.db.set_languages(id, val) else: books_to_refresh |= self.db.set(row, column, val, allow_case_change=True) diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index f0f30bdb08..5a62b76c6b 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -8,14 +8,14 @@ __docformat__ = 'restructuredtext en' import os from functools import partial -from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \ - QModelIndex, QIcon, QItemSelection, QMimeData, QDrag, QApplication, \ - QPoint, QPixmap, QUrl, QImage, QPainter, QColor, QRect +from PyQt4.Qt import (QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, + QModelIndex, QIcon, QItemSelection, QMimeData, QDrag, QApplication, + QPoint, QPixmap, QUrl, QImage, QPainter, QColor, QRect) -from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \ - TextDelegate, DateDelegate, CompleteDelegate, CcTextDelegate, \ - CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate, \ - CcEnumDelegate, CcNumberDelegate +from calibre.gui2.library.delegates import (RatingDelegate, PubDateDelegate, + TextDelegate, DateDelegate, CompleteDelegate, CcTextDelegate, + CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate, + CcEnumDelegate, CcNumberDelegate, LanguagesDelegate) from calibre.gui2.library.models import BooksModel, DeviceBooksModel from calibre.utils.config import tweaks, prefs from calibre.gui2 import error_dialog, gprefs @@ -85,6 +85,7 @@ class BooksView(QTableView): # {{{ self.pubdate_delegate = PubDateDelegate(self) self.last_modified_delegate = DateDelegate(self, tweak_name='gui_last_modified_display_format') + self.languages_delegate = LanguagesDelegate(self) self.tags_delegate = CompleteDelegate(self, ',', 'all_tags') self.authors_delegate = CompleteDelegate(self, '&', 'all_author_names', True) self.cc_names_delegate = CompleteDelegate(self, '&', 'all_custom', True) @@ -306,6 +307,7 @@ class BooksView(QTableView): # {{{ state['hidden_columns'] = [cm[i] for i in range(h.count()) if h.isSectionHidden(i) and cm[i] != 'ondevice'] state['last_modified_injected'] = True + state['languages_injected'] = True state['sort_history'] = \ self.cleanup_sort_history(self.model().sort_history) state['column_positions'] = {} @@ -390,7 +392,7 @@ class BooksView(QTableView): # {{{ def get_default_state(self): old_state = { - 'hidden_columns': ['last_modified'], + 'hidden_columns': ['last_modified', 'languages'], 'sort_history':[DEFAULT_SORT], 'column_positions': {}, 'column_sizes': {}, @@ -399,6 +401,7 @@ class BooksView(QTableView): # {{{ 'timestamp':'center', 'pubdate':'center'}, 'last_modified_injected': True, + 'languages_injected': True, } h = self.column_header cm = self.column_map @@ -430,11 +433,20 @@ class BooksView(QTableView): # {{{ if ans is not None: db.prefs[name] = ans else: + injected = False if not ans.get('last_modified_injected', False): + injected = True ans['last_modified_injected'] = True hc = ans.get('hidden_columns', []) if 'last_modified' not in hc: hc.append('last_modified') + if not ans.get('languages_injected', False): + injected = True + ans['languages_injected'] = True + hc = ans.get('hidden_columns', []) + if 'languages' not in hc: + hc.append('languages') + if injected: db.prefs[name] = ans return ans @@ -501,7 +513,7 @@ class BooksView(QTableView): # {{{ for i in range(self.model().columnCount(None)): if self.itemDelegateForColumn(i) in (self.rating_delegate, self.timestamp_delegate, self.pubdate_delegate, - self.last_modified_delegate): + self.last_modified_delegate, self.languages_delegate): self.setItemDelegateForColumn(i, self.itemDelegate()) cm = self.column_map diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 3084738b27..29f6fffa0b 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -34,6 +34,7 @@ from calibre.library.comments import comments_to_html from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.utils.icu import strcmp from calibre.ptempfile import PersistentTemporaryFile +from calibre.gui2.languages import LanguagesEdit as LE def save_dialog(parent, title, msg, det_msg=''): d = QMessageBox(parent) @@ -1133,6 +1134,43 @@ class TagsEdit(MultiCompleteLineEdit): # {{{ # }}} +class LanguagesEdit(LE): # {{{ + + LABEL = _('&Languages:') + TOOLTIP = _('A comma separated list of languages for this book') + + def __init__(self, *args, **kwargs): + LE.__init__(self, *args, **kwargs) + self.setToolTip(self.TOOLTIP) + + @dynamic_property + def current_val(self): + def fget(self): return self.lang_codes + def fset(self, val): self.lang_codes = val + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + lc = [] + langs = db.languages(id_, index_is_id=True) + if langs: + lc = [x.strip() for x in langs.split(',')] + self.current_val = self.original_val = lc + + def commit(self, db, id_): + bad = self.validate() + if bad: + error_dialog(self, _('Unknown language'), + ngettext('The language %s is not recognized', + 'The languages %s are not recognized', len(bad))%( + ', '.join(bad)), + show=True) + return False + cv = self.current_val + if cv != self.original_val: + db.set_languages(id_, cv) + return True +# }}} + class IdentifiersEdit(QLineEdit): # {{{ LABEL = _('I&ds:') BASE_TT = _('Edit the identifiers for this book. ' diff --git a/src/calibre/gui2/metadata/bulk_download.py b/src/calibre/gui2/metadata/bulk_download.py index f8c07924f4..ad7018401b 100644 --- a/src/calibre/gui2/metadata/bulk_download.py +++ b/src/calibre/gui2/metadata/bulk_download.py @@ -89,6 +89,15 @@ class ConfirmDialog(QDialog): self.identify = False self.accept() +def split_jobs(ids, batch_size=100): + ans = [] + ids = list(ids) + while ids: + jids = ids[:batch_size] + ans.append(jids) + ids = ids[batch_size:] + return ans + def start_download(gui, ids, callback): d = ConfirmDialog(ids, gui) ret = d.exec_() @@ -96,11 +105,13 @@ def start_download(gui, ids, callback): if ret != d.Accepted: return - job = ThreadedJob('metadata bulk download', - _('Download metadata for %d books')%len(ids), - download, (ids, gui.current_db, d.identify, d.covers), {}, callback) - gui.job_manager.run_threaded_job(job) + for batch in split_jobs(ids): + job = ThreadedJob('metadata bulk download', + _('Download metadata for %d books')%len(batch), + download, (batch, gui.current_db, d.identify, d.covers), {}, callback) + gui.job_manager.run_threaded_job(job) gui.status_bar.show_message(_('Metadata download started'), 3000) + # }}} def get_job_details(job): diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 998734511c..7f2ea036d6 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -13,19 +13,21 @@ from functools import partial from PyQt4.Qt import (Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, - QSizePolicy, QPalette, QFrame, QSize, QKeySequence, QMenu) + QSizePolicy, QPalette, QFrame, QSize, QKeySequence, QMenu, QShortcut) from calibre.ebooks.metadata import authors_to_string, string_to_authors from calibre.gui2 import ResizableDialog, error_dialog, gprefs, pixmap_to_data from calibre.gui2.metadata.basic_widgets import (TitleEdit, AuthorsEdit, AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, IdentifiersEdit, RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, - BuddyLabel, DateEdit, PubdateEdit) + BuddyLabel, DateEdit, PubdateEdit, LanguagesEdit) from calibre.gui2.metadata.single_download import FullFetch from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.utils.config import tweaks from calibre.ebooks.metadata.book.base import Metadata +BASE_TITLE = _('Edit Metadata') + class MetadataSingleDialogBase(ResizableDialog): view_format = pyqtSignal(object, object) @@ -43,6 +45,16 @@ class MetadataSingleDialogBase(ResizableDialog): def setupUi(self, *args): # {{{ self.resize(990, 650) + self.download_shortcut = QShortcut(self) + self.download_shortcut.setKey(QKeySequence('Ctrl+D', + QKeySequence.PortableText)) + p = self.parent() + if hasattr(p, 'keyboard'): + kname = u'Interface Action: Edit Metadata (Edit Metadata) : menu action : download' + sc = p.keyboard.keys_map.get(kname, None) + if sc: + self.download_shortcut.setKey(sc[0]) + self.button_box = QDialogButtonBox( QDialogButtonBox.Ok|QDialogButtonBox.Cancel, Qt.Horizontal, self) @@ -77,7 +89,7 @@ class MetadataSingleDialogBase(ResizableDialog): ll.addSpacing(10) self.setWindowIcon(QIcon(I('edit_input.png'))) - self.setWindowTitle(_('Edit Metadata')) + self.setWindowTitle(BASE_TITLE) self.create_basic_metadata_widgets() @@ -183,6 +195,9 @@ class MetadataSingleDialogBase(ResizableDialog): self.publisher = PublisherEdit(self) self.basic_metadata_widgets.append(self.publisher) + self.languages = LanguagesEdit(self) + self.basic_metadata_widgets.append(self.languages) + self.timestamp = DateEdit(self) self.pubdate = PubdateEdit(self) self.basic_metadata_widgets.extend([self.timestamp, self.pubdate]) @@ -190,6 +205,7 @@ class MetadataSingleDialogBase(ResizableDialog): self.fetch_metadata_button = QPushButton( _('&Download metadata'), self) self.fetch_metadata_button.clicked.connect(self.fetch_metadata) + self.download_shortcut.activated.connect(self.fetch_metadata_button.click) font = self.fmb_font = QFont() font.setBold(True) self.fetch_metadata_button.setFont(font) @@ -264,8 +280,11 @@ class MetadataSingleDialogBase(ResizableDialog): title = self.title.current_val if len(title) > 50: title = title[:50] + u'\u2026' - self.setWindowTitle(_('Edit Metadata') + ' - ' + - title) + self.setWindowTitle(BASE_TITLE + ' - ' + + title + ' - ' + + _(' [%(num)d of %(tot)d]')%dict(num= + self.current_row+1, + tot=len(self.row_list))) def swap_title_author(self, *args): title = self.title.current_val @@ -351,6 +370,8 @@ class MetadataSingleDialogBase(ResizableDialog): self.series.current_val = mi.series if mi.series_index is not None: self.series_index.current_val = float(mi.series_index) + if not mi.is_null('languages'): + self.languages.lang_codes = mi.languages if mi.comments and mi.comments.strip(): self.comments.current_val = mi.comments @@ -610,11 +631,13 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{ create_row2(5, self.pubdate, self.pubdate.clear_button) sto(self.pubdate.clear_button, self.publisher) create_row2(6, self.publisher) + sto(self.publisher, self.languages) + create_row2(7, self.languages) self.tabs[0].spc_two = QSpacerItem(10, 10, QSizePolicy.Expanding, QSizePolicy.Expanding) - l.addItem(self.tabs[0].spc_two, 8, 0, 1, 3) - l.addWidget(self.fetch_metadata_button, 9, 0, 1, 2) - l.addWidget(self.config_metadata_button, 9, 2, 1, 1) + l.addItem(self.tabs[0].spc_two, 9, 0, 1, 3) + l.addWidget(self.fetch_metadata_button, 10, 0, 1, 2) + l.addWidget(self.config_metadata_button, 10, 2, 1, 1) self.tabs[0].gb2 = gb = QGroupBox(_('Co&mments'), self) gb.l = l = QVBoxLayout() @@ -717,16 +740,17 @@ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{ create_row(7, self.rating, self.pubdate) create_row(8, self.pubdate, self.publisher, button=self.pubdate.clear_button, icon='trash.png') - create_row(9, self.publisher, self.timestamp) - create_row(10, self.timestamp, self.identifiers, + create_row(9, self.publisher, self.languages) + create_row(10, self.languages, self.timestamp) + create_row(11, self.timestamp, self.identifiers, button=self.timestamp.clear_button, icon='trash.png') - create_row(11, self.identifiers, self.comments, + create_row(12, self.identifiers, self.comments, button=self.clear_identifiers_button, icon='trash.png') sto(self.clear_identifiers_button, self.swap_title_author_button) sto(self.swap_title_author_button, self.manage_authors_button) sto(self.manage_authors_button, self.paste_isbn_button) tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding), - 12, 1, 1 ,1) + 13, 1, 1 ,1) w = getattr(self, 'custom_metadata_widgets_parent', None) if w is not None: @@ -852,16 +876,17 @@ class MetadataSingleDialogAlt2(MetadataSingleDialogBase): # {{{ create_row(7, self.rating, self.pubdate) create_row(8, self.pubdate, self.publisher, button=self.pubdate.clear_button, icon='trash.png') - create_row(9, self.publisher, self.timestamp) - create_row(10, self.timestamp, self.identifiers, + create_row(9, self.publisher, self.languages) + create_row(10, self.languages, self.timestamp) + create_row(11, self.timestamp, self.identifiers, button=self.timestamp.clear_button, icon='trash.png') - create_row(11, self.identifiers, self.comments, + create_row(12, self.identifiers, self.comments, button=self.clear_identifiers_button, icon='trash.png') sto(self.clear_identifiers_button, self.swap_title_author_button) sto(self.swap_title_author_button, self.manage_authors_button) sto(self.manage_authors_button, self.paste_isbn_button) tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding), - 12, 1, 1 ,1) + 13, 1, 1 ,1) # Custom metadata in col 1 w = getattr(self, 'custom_metadata_widgets_parent', None) diff --git a/src/calibre/gui2/preferences/adding.ui b/src/calibre/gui2/preferences/adding.ui index 4a0d01be73..dae050b7ea 100644 --- a/src/calibre/gui2/preferences/adding.ui +++ b/src/calibre/gui2/preferences/adding.ui @@ -130,7 +130,7 @@ Author matching is exact. - When &copying books from one library to another, preserve the date + When using the "&Copy to library" action to copy books between libraries, preserve the date diff --git a/src/calibre/gui2/preferences/metadata_sources.py b/src/calibre/gui2/preferences/metadata_sources.py index d9dd64af6c..541da2e203 100644 --- a/src/calibre/gui2/preferences/metadata_sources.py +++ b/src/calibre/gui2/preferences/metadata_sources.py @@ -161,7 +161,7 @@ class FieldsModel(QAbstractListModel): # {{{ 'tags' : _('Tags'), 'title': _('Title'), 'series': _('Series'), - 'language': _('Language'), + 'languages': _('Languages'), } self.overrides = {} self.exclude = frozenset(['series_index']) diff --git a/src/calibre/gui2/preferences/plugins.py b/src/calibre/gui2/preferences/plugins.py index 246df79d8f..06a503f855 100644 --- a/src/calibre/gui2/preferences/plugins.py +++ b/src/calibre/gui2/preferences/plugins.py @@ -239,6 +239,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.plugin_view.selectionModel().select(idx, self.plugin_view.selectionModel().ClearAndSelect) self.plugin_view.setCurrentIndex(idx) + self.plugin_view.setFocus(Qt.OtherFocusReason) def find_next(self, *args): idx = self.plugin_view.currentIndex() diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index fd085923e2..54a80571e6 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -108,6 +108,12 @@ class SearchBox2(QComboBox): # {{{ self.colorize = colorize self.clear() + def hide_completer_popup(self): + try: + self.lineEdit().completer().popup().setVisible(False) + except: + pass + def normalize_state(self): self.setToolTip(self.tool_tip_text) self.line_edit.setStyleSheet( @@ -163,6 +169,8 @@ class SearchBox2(QComboBox): # {{{ # Comes from the combobox itself def keyPressEvent(self, event): k = event.key() + if k in (Qt.Key_Enter, Qt.Key_Return): + return self.do_search() if k not in (Qt.Key_Up, Qt.Key_Down): QComboBox.keyPressEvent(self, event) else: @@ -183,6 +191,7 @@ class SearchBox2(QComboBox): # {{{ self.do_search() def _do_search(self, store_in_history=True): + self.hide_completer_popup() text = unicode(self.currentText()).strip() if not text: return self.clear() @@ -219,15 +228,15 @@ class SearchBox2(QComboBox): # {{{ self.clear() else: self.normalize_state() - self.lineEdit().setCompleter(None) + # must turn on case sensitivity here so that tag browser strings + # are not case-insensitively replaced from history + self.line_edit.completer().setCaseSensitivity(Qt.CaseSensitive) self.setEditText(txt) self.line_edit.end(False) if emit_changed: self.changed.emit() self._do_search(store_in_history=store_in_history) - c = QCompleter() - self.lineEdit().setCompleter(c) - c.setCompletionMode(c.PopupCompletion) + self.line_edit.completer().setCaseSensitivity(Qt.CaseInsensitive) self.focus_to_library.emit() finally: if not store_in_history: diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 2fa43dc94c..5f9dca6d23 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -15,6 +15,7 @@ from calibre.utils.config import tweaks, prefs from calibre.utils.date import parse_date, now, UNDEFINED_DATE from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.pyparsing import ParseException +from calibre.utils.localization import canonicalize_lang from calibre.ebooks.metadata import title_sort, author_to_author_sort from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre import prints @@ -721,9 +722,13 @@ class ResultCache(SearchQueryParser): # {{{ if loc == db_col['authors']: ### DB stores authors with commas changed to bars, so change query if matchkind == REGEXP_MATCH: - q = query.replace(',', r'\|'); + q = query.replace(',', r'\|') else: - q = query.replace(',', '|'); + q = query.replace(',', '|') + elif loc == db_col['languages']: + q = canonicalize_lang(query) + if q is None: + q = query else: q = query diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 3471d93332..79a441298f 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -39,6 +39,8 @@ from calibre.utils.magick.draw import save_cover_data_to from calibre.utils.recycle_bin import delete_file, delete_tree from calibre.utils.formatter_functions import load_user_template_functions from calibre.db.errors import NoSuchFormat +from calibre.utils.localization import (canonicalize_lang, + calibre_langcode_to_name) copyfile = os.link if hasattr(os, 'link') else shutil.copyfile SPOOL_SIZE = 30*1024*1024 @@ -372,6 +374,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): 'aum_sortconcat(link.id, authors.name, authors.sort, authors.link)'), 'last_modified', '(SELECT identifiers_concat(type, val) FROM identifiers WHERE identifiers.book=books.id) identifiers', + ('languages', 'languages', 'lang_code', + 'sortconcat(link.id, languages.lang_code)'), ] lines = [] for col in columns: @@ -390,7 +394,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): 'size':4, 'rating':5, 'tags':6, 'comments':7, 'series':8, 'publisher':9, 'series_index':10, 'sort':11, 'author_sort':12, 'formats':13, 'path':14, 'pubdate':15, 'uuid':16, 'cover':17, - 'au_map':18, 'last_modified':19, 'identifiers':20} + 'au_map':18, 'last_modified':19, 'identifiers':20, 'languages':21} for k,v in self.FIELD_MAP.iteritems(): self.field_metadata.set_field_record_index(k, v, prefer_custom=False) @@ -469,7 +473,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): 'author_sort', 'authors', 'comment', 'comments', 'publisher', 'rating', 'series', 'series_index', 'tags', 'title', 'timestamp', 'uuid', 'pubdate', 'ondevice', - 'metadata_last_modified', + 'metadata_last_modified', 'languages', ): fm = {'comment':'comments', 'metadata_last_modified': 'last_modified'}.get(prop, prop) @@ -930,6 +934,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): tags = row[fm['tags']] if tags: mi.tags = [i.strip() for i in tags.split(',')] + languages = row[fm['languages']] + if languages: + mi.languages = [i.strip() for i in languages.split(',')] mi.series = row[fm['series']] if mi.series: mi.series_index = row[fm['series_index']] @@ -1390,7 +1397,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): ('authors', 'authors', 'author'), ('publishers', 'publishers', 'publisher'), ('tags', 'tags', 'tag'), - ('series', 'series', 'series') + ('series', 'series', 'series'), + ('languages', 'languages', 'lang_code'), ]: doit(ltable, table, ltable_col) @@ -1507,6 +1515,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): 'series' : self.get_series_with_ids, 'publisher': self.get_publishers_with_ids, 'tags' : self.get_tags_with_ids, + 'languages': self.get_languages_with_ids, 'rating' : self.get_ratings_with_ids, } func = funcs.get(category, None) @@ -1521,6 +1530,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): for l in list: (id, val, sort_val) = (l[0], l[1], l[2]) tids[category][val] = (id, sort_val) + elif category == 'languages': + for l in list: + id, val = l[0], calibre_langcode_to_name(l[1]) + tids[category][l[1]] = (id, val) elif cat['datatype'] == 'series': for l in list: (id, val) = (l[0], l[1]) @@ -1620,6 +1633,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): item.rt += rating item.rc += 1 except: + prints(tid_cat, val) prints('get_categories: item', val, 'is not in', cat, 'list!') #print 'end phase "books":', time.clock() - last, 'seconds' @@ -1684,6 +1698,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # Clean up the authors strings to human-readable form formatter = (lambda x: x.replace('|', ',')) items = [v for v in tcategories[category].values() if v.c > 0] + elif category == 'languages': + # Use a human readable language string + formatter = calibre_langcode_to_name + items = [v for v in tcategories[category].values() if v.c > 0] else: formatter = (lambda x:unicode(x)) items = [v for v in tcategories[category].values() if v.c > 0] @@ -2043,6 +2061,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if should_replace_field('comments'): doit(self.set_comment, id, mi.comments, notify=False, commit=False) + if should_replace_field('languages'): + doit(self.set_languages, id, mi.languages, notify=False, commit=False) + # Setting series_index to zero is acceptable if mi.series_index is not None: doit(self.set_series_index, id, mi.series_index, notify=False, @@ -2265,6 +2286,37 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if notify: self.notify('metadata', [id]) + def set_languages(self, book_id, languages, notify=True, commit=True): + self.conn.execute( + 'DELETE FROM books_languages_link WHERE book=?', (book_id,)) + self.conn.execute('''DELETE FROM languages WHERE (SELECT COUNT(id) + FROM books_languages_link WHERE + lang_code=languages.id) < 1''') + + books_to_refresh = set([book_id]) + final_languages = [] + for l in languages: + lc = canonicalize_lang(l) + if not lc or lc in final_languages or lc in ('und', 'zxx', 'mis', + 'mul'): + continue + final_languages.append(lc) + lc_id = self.conn.get('SELECT id FROM languages WHERE lang_code=?', + (lc,), all=False) + if lc_id is None: + lc_id = self.conn.execute('''INSERT INTO languages(lang_code) + VALUES (?)''', (lc,)).lastrowid + self.conn.execute('''INSERT INTO books_languages_link(book, lang_code) + VALUES (?,?)''', (book_id, lc_id)) + self.dirtied(books_to_refresh, commit=False) + if commit: + self.conn.commit() + self.data.set(book_id, self.FIELD_MAP['languages'], + u','.join(final_languages), row_is_id=True) + if notify: + self.notify('metadata', [book_id]) + return books_to_refresh + def set_timestamp(self, id, dt, notify=True, commit=True): if dt: self.conn.execute('UPDATE books SET timestamp=? WHERE id=?', (dt, id)) @@ -2363,6 +2415,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return [] return result + def get_languages_with_ids(self): + result = self.conn.get('SELECT id,lang_code FROM languages') + if not result: + return [] + return result + def rename_tag(self, old_id, new_name): # It is possible that new_name is in fact a set of names. Split it on # comma to find out. If it is, then rename the first one and append the diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index f802ae7f7b..eff3fd1fed 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -17,7 +17,7 @@ class TagsIcons(dict): category_icons = ['authors', 'series', 'formats', 'publisher', 'rating', 'news', 'tags', 'custom:', 'user:', 'search', - 'identifiers', 'gst'] + 'identifiers', 'languages', 'gst'] def __init__(self, icon_dict): for a in self.category_icons: if a not in icon_dict: @@ -37,6 +37,7 @@ category_icon_map = { 'search' : 'search.png', 'identifiers': 'identifiers.png', 'gst' : 'catalog.png', + 'languages' : 'languages.png', } @@ -114,6 +115,21 @@ class FieldMetadata(dict): 'is_custom':False, 'is_category':True, 'is_csp': False}), + ('languages', {'table':'languages', + 'column':'lang_code', + 'link_column':'lang_code', + 'category_sort':'lang_code', + 'datatype':'text', + 'is_multiple':{'cache_to_list': ',', + 'ui_to_list': ',', + 'list_to_ui': ', '}, + 'kind':'field', + 'name':_('Languages'), + 'search_terms':['languages', 'language'], + 'is_custom':False, + 'is_category':True, + 'is_csp': False}), + ('series', {'table':'series', 'column':'name', 'link_column':'series', diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py index cacb607d56..71fc1375ad 100644 --- a/src/calibre/library/save_to_disk.py +++ b/src/calibre/library/save_to_disk.py @@ -111,12 +111,12 @@ def config(defaults=None): 'to supports unicode.')) x('timefmt', default='%b, %Y', help=_('The format in which to display dates. %(day)s - day,' - ' %(month)s - month, %(year)s - year. Default is: %(default)s' - )%dict(day='%d', month='%b', year='%Y', default='%b, %Y')) + ' %(month)s - month, %(mn)s - month number, %(year)s - year. Default is: %(default)s' + )%dict(day='%d', month='%b', mn='%m', year='%Y', default='%b, %Y')) x('send_timefmt', default='%b, %Y', help=_('The format in which to display dates. %(day)s - day,' - ' %(month)s - month, %(year)s - year. Default is: %(default)s' - )%dict(day='%d', month='%b', year='%Y', default='%b, %Y')) + ' %(month)s - month, %(mn)s - month number, %(year)s - year. Default is: %(default)s' + )%dict(day='%d', month='%b', mn='%m', year='%Y', default='%b, %Y')) x('to_lowercase', default=False, help=_('Convert paths to lowercase.')) x('replace_whitespace', default=False, diff --git a/src/calibre/manual/conversion.rst b/src/calibre/manual/conversion.rst index b9092fd14d..7ced74c70d 100644 --- a/src/calibre/manual/conversion.rst +++ b/src/calibre/manual/conversion.rst @@ -565,7 +565,9 @@ Convert Microsoft Word documents |app| does not directly convert .doc/.docx files from Microsoft Word. However, in Word, you can save the document as HTML and then convert the resulting HTML file with |app|. When saving as HTML, be sure to use the "Save as Web Page, Filtered" option as this will produce clean HTML that will convert well. Note that Word -produces really messy HTML, converting it can take a long time, so be patient. +produces really messy HTML, converting it can take a long time, so be patient. Another alternative is to +use the free OpenOffice. Open your .doc file in OpenOffice and save it in OpenOffice's format .odt. |app| can +directly convert .odt files. There is a Word macro package that can automate the conversion of Word documents using |app|. It also makes generating the Table of Contents much simpler. It is called BookCreator and is available for free diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 03b6e9bcf0..05c05c00c9 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -555,7 +555,7 @@ If you still cannot get the installer to work and you are on windows, you can us My antivirus program claims |app| is a virus/trojan? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Your antivirus program is wrong. |app| is a completely open source product. You can actually browse the source code yourself (or hire someone to do it for you) to verify that it is not a virus. Please report the false identification to whatever company you buy your antivirus software from. If the antivirus program is preventing you from downloading/installing |app|, disable it temporarily, install |app| and then re-enable it. +Your antivirus program is wrong. Antivirus programs use heuristics, patterns of code that "looks suspicuous" to detect viruses. It's rather like racial profiling. |app| is a completely open source product. You can actually browse the source code yourself (or hire someone to do it for you) to verify that it is not a virus. Please report the false identification to whatever company you buy your antivirus software from. If the antivirus program is preventing you from downloading/installing |app|, disable it temporarily, install |app| and then re-enable it. How do I backup |app|? ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/calibre/utils/filenames.py b/src/calibre/utils/filenames.py index 8c6daa5adf..f031362d39 100644 --- a/src/calibre/utils/filenames.py +++ b/src/calibre/utils/filenames.py @@ -6,8 +6,9 @@ meaning as possible. import os from math import ceil -from calibre import sanitize_file_name -from calibre.constants import preferred_encoding, iswindows +from calibre import sanitize_file_name, isbytestring, force_unicode +from calibre.constants import (preferred_encoding, iswindows, + filesystem_encoding) from calibre.utils.localization import get_udc def ascii_text(orig): @@ -114,3 +115,83 @@ def is_case_sensitive(path): os.remove(f1) return is_case_sensitive +def case_preserving_open_file(path, mode='wb', mkdir_mode=0777): + ''' + Open the file pointed to by path with the specified mode. If any + directories in path do not exist, they are created. Returns the + opened file object and the path to the opened file object. This path is + guaranteed to have the same case as the on disk path. For case insensitive + filesystems, the returned path may be different from the passed in path. + The returned path is always unicode and always an absolute path. + + If mode is None, then this function assumes that path points to a directory + and return the path to the directory as the file object. + + mkdir_mode specifies the mode with which any missing directories in path + are created. + ''' + if isbytestring(path): + path = path.decode(filesystem_encoding) + + path = os.path.abspath(path) + + sep = force_unicode(os.sep, 'ascii') + + if path.endswith(sep): + path = path[:-1] + if not path: + raise ValueError('Path must not point to root') + + components = path.split(sep) + if not components: + raise ValueError('Invalid path: %r'%path) + + cpath = sep + if iswindows: + # Always upper case the drive letter and add a trailing slash so that + # the first os.listdir works correctly + cpath = components[0].upper() + sep + + bdir = path if mode is None else os.path.dirname(path) + if not os.path.exists(bdir): + os.makedirs(bdir, mkdir_mode) + + # Walk all the directories in path, putting the on disk case version of + # the directory into cpath + dirs = components[1:] if mode is None else components[1:-1] + for comp in dirs: + cdir = os.path.join(cpath, comp) + cl = comp.lower() + try: + candidates = [c for c in os.listdir(cpath) if c.lower() == cl] + except: + # Dont have permission to do the listdir, assume the case is + # correct as we have no way to check it. + pass + else: + if len(candidates) == 1: + cdir = os.path.join(cpath, candidates[0]) + # else: We are on a case sensitive file system so cdir must already + # be correct + cpath = cdir + + if mode is None: + ans = fpath = cpath + else: + fname = components[-1] + ans = open(os.path.join(cpath, fname), mode) + # Ensure file and all its metadata is written to disk so that subsequent + # listdir() has file name in it. I don't know if this is actually + # necessary, but given the diversity of platforms, best to be safe. + ans.flush() + os.fsync(ans.fileno()) + + cl = fname.lower() + candidates = [c for c in os.listdir(cpath) if c.lower() == cl] + if len(candidates) == 1: + fpath = os.path.join(cpath, candidates[0]) + else: + # We are on a case sensitive filesystem + fpath = os.path.join(cpath, fname) + return ans, fpath + diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index c1af65f9e3..144d130564 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -963,7 +963,7 @@ class BuiltinListSort(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, list1, direction, separator): res = [l.strip() for l in list1.split(separator) if l.strip()] - return ', '.join(sorted(res, key=sort_key, reverse=direction != 0)) + return ', '.join(sorted(res, key=sort_key, reverse=direction != "0")) class BuiltinToday(BuiltinFormatterFunction): name = 'today' diff --git a/src/calibre/utils/localization.py b/src/calibre/utils/localization.py index 1b3347c5bd..947ee823c6 100644 --- a/src/calibre/utils/localization.py +++ b/src/calibre/utils/localization.py @@ -192,6 +192,80 @@ def get_language(lang): ans = iso639['by_3t'].get(lang, ans) return translate(ans) +def calibre_langcode_to_name(lc, localize=True): + iso639 = _load_iso639() + translate = _ if localize else lambda x: x + try: + return translate(iso639['by_3t'][lc]) + except: + pass + return lc + +def canonicalize_lang(raw): + if not raw: + return None + if not isinstance(raw, unicode): + raw = raw.decode('utf-8', 'ignore') + raw = raw.lower().strip() + if not raw: + return None + raw = raw.replace('_', '-').partition('-')[0].strip() + if not raw: + return None + iso639 = _load_iso639() + m2to3 = iso639['2to3'] + + if len(raw) == 2: + ans = m2to3.get(raw, None) + if ans is not None: + return ans + elif len(raw) == 3: + if raw in iso639['by_3t']: + return raw + if raw in iso639['3bto3t']: + return iso639['3bto3t'][raw] + + return iso639['name_map'].get(raw, None) + +_lang_map = None + +def lang_map(): + ' Return mapping of ISO 639 3 letter codes to localized language names ' + iso639 = _load_iso639() + translate = _ + global _lang_map + if _lang_map is None: + _lang_map = {k:translate(v) for k, v in iso639['by_3t'].iteritems()} + return _lang_map + +def langnames_to_langcodes(names): + ''' + Given a list of localized language names return a mapping of the names to 3 + letter ISO 639 language codes. If a name is not recognized, it is mapped to + None. + ''' + iso639 = _load_iso639() + translate = _ + ans = {} + names = set(names) + for k, v in iso639['by_3t'].iteritems(): + tv = translate(v) + if tv in names: + names.remove(tv) + ans[tv] = k + if not names: + break + for x in names: + ans[x] = None + + return ans + +def lang_as_iso639_1(name_or_code): + code = canonicalize_lang(name_or_code) + if code is not None: + iso639 = _load_iso639() + return iso639['3to2'].get(code, None) + _udc = None def get_udc():