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
+
+ LanguagesEdit
+ QComboBox
+
+
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 ©ing 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():