diff --git a/resources/images/donate.svg b/resources/images/donate.svg
index b17d0ec7a0..603e672f6f 100644
--- a/resources/images/donate.svg
+++ b/resources/images/donate.svg
@@ -1,24 +1,31 @@
+
\ No newline at end of file
diff --git a/resources/images/lt.png b/resources/images/lt.png
new file mode 100644
index 0000000000..c29efb9f88
Binary files /dev/null and b/resources/images/lt.png differ
diff --git a/resources/images/news/elpais_impreso.png b/resources/images/news/elpais_impreso.png
new file mode 100644
index 0000000000..35dcaf2d44
Binary files /dev/null and b/resources/images/news/elpais_impreso.png differ
diff --git a/resources/recipes/ap.recipe b/resources/recipes/ap.recipe
index 572c0aa392..0118cf0726 100644
--- a/resources/recipes/ap.recipe
+++ b/resources/recipes/ap.recipe
@@ -12,9 +12,9 @@ class AssociatedPress(BasicNewsRecipe):
max_articles_per_feed = 15
html2lrf_options = ['--force-page-break-before-tag="chapter"']
-
-
- preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
+
+
+ preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
[
(r'
.*?' , lambda match : ''),
(r'.*?', lambda match : ''),
@@ -25,10 +25,10 @@ class AssociatedPress(BasicNewsRecipe):
(r'
', lambda match : '
'),
(r'Learn more about our Privacy Policy.*?', lambda match : ''),
]
- ]
-
+ ]
+
+
-
feeds = [ ('AP Headlines', 'http://hosted.ap.org/lineups/TOPHEADS-rss_2.0.xml?SITE=ORAST&SECTION=HOME'),
('AP US News', 'http://hosted.ap.org/lineups/USHEADS-rss_2.0.xml?SITE=CAVIC&SECTION=HOME'),
('AP World News', 'http://hosted.ap.org/lineups/WORLDHEADS-rss_2.0.xml?SITE=SCAND&SECTION=HOME'),
@@ -38,4 +38,4 @@ class AssociatedPress(BasicNewsRecipe):
('AP Health News', 'http://hosted.ap.org/lineups/HEALTHHEADS-rss_2.0.xml?SITE=FLDAY&SECTION=HOME'),
('AP Science News', 'http://hosted.ap.org/lineups/SCIENCEHEADS-rss_2.0.xml?SITE=OHCIN&SECTION=HOME'),
('AP Strange News', 'http://hosted.ap.org/lineups/STRANGEHEADS-rss_2.0.xml?SITE=WCNC&SECTION=HOME'),
- ]
\ No newline at end of file
+ ]
diff --git a/resources/recipes/elpais_impreso.recipe b/resources/recipes/elpais_impreso.recipe
new file mode 100644
index 0000000000..b30db0707a
--- /dev/null
+++ b/resources/recipes/elpais_impreso.recipe
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+__license__ = 'GPL v3'
+__copyright__ = '2010, Darko Miletic '
+'''
+www.elpais.com/diario/
+'''
+
+from calibre import strftime
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class ElPaisImpresa(BasicNewsRecipe):
+ title = 'El País - edicion impresa'
+ __author__ = 'Darko Miletic'
+ description = 'el periodico global en Español'
+ publisher = 'EDICIONES EL PAIS, S.L.'
+ category = 'news, politics,Spain,actualidad,noticias,informacion,videos,fotografias,audios,graficos,nacional,internacional,deportes,economia,tecnologia,cultura,gente,television,sociedad,opinion,blogs,foros,chats,encuestas,entrevistas,participacion'
+ no_stylesheets = True
+ encoding = 'latin1'
+ use_embedded_content = False
+ language = 'es'
+ publication_type = 'newspaper'
+ masthead_url = 'http://www.elpais.com/im/tit_logo_global.gif'
+ index = 'http://www.elpais.com/diario/'
+ extra_css = ' p{text-align: justify} body{ text-align: left; font-family: Georgia,"Times New Roman",Times,serif } h2{font-family: Arial,Helvetica,sans-serif} img{margin-bottom: 0.4em} '
+
+ conversion_options = {
+ 'comment' : description
+ , 'tags' : category
+ , 'publisher' : publisher
+ , 'language' : language
+ }
+
+ feeds = [
+ (u'Internacional' , index + u'internacional/' )
+ ,(u'España' , index + u'espana/' )
+ ,(u'Economia' , index + u'economia/' )
+ ,(u'Opinion' , index + u'opinion/' )
+ ,(u'Viñetas' , index + u'vineta/' )
+ ,(u'Sociedad' , index + u'sociedad/' )
+ ,(u'Cultura' , index + u'cultura/' )
+ ,(u'Tendencias' , index + u'tendencias/' )
+ ,(u'Gente' , index + u'gente/' )
+ ,(u'Obituarios' , index + u'obituarios/' )
+ ,(u'Deportes' , index + u'deportes/' )
+ ,(u'Pantallas' , index + u'radioytv/' )
+ ,(u'Ultima' , index + u'ultima/' )
+ ,(u'Educacion' , index + u'educacion/' )
+ ,(u'Saludo' , index + u'salud/' )
+ ,(u'Ciberpais' , index + u'ciberpais/' )
+ ,(u'EP3' , index + u'ep3/' )
+ ,(u'Cine' , index + u'cine/' )
+ ,(u'Babelia' , index + u'babelia/' )
+ ,(u'El viajero' , index + u'viajero/' )
+ ,(u'Negocios' , index + u'negocios/' )
+ ,(u'Domingo' , index + u'domingo/' )
+ ,(u'El Pais semanal' , index + u'eps/' )
+ ,(u'Quadern Catalunya' , index + u'quadern-catalunya/' )
+ ]
+
+ keep_only_tags=[dict(attrs={'class':['cabecera_noticia','contenido_noticia']})]
+ remove_attributes=['width','height']
+ remove_tags=[dict(name='link')]
+
+ def parse_index(self):
+ totalfeeds = []
+ lfeeds = self.get_feeds()
+ for feedobj in lfeeds:
+ feedtitle, feedurl = feedobj
+ self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
+ articles = []
+ soup = self.index_to_soup(feedurl)
+ for item in soup.findAll('a',attrs={'class':['g19r003','g19i003','g17r003','g17i003']}):
+ url = 'http://www.elpais.com' + item['href'].rpartition('/')[0]
+ title = self.tag_to_string(item)
+ date = strftime(self.timefmt)
+ articles.append({
+ 'title' :title
+ ,'date' :date
+ ,'url' :url
+ ,'description':''
+ })
+ totalfeeds.append((feedtitle, articles))
+ return totalfeeds
+
+ def print_version(self, url):
+ return url + '?print=1'
diff --git a/resources/recipes/greader.recipe b/resources/recipes/greader.recipe
index cbf4c0226b..2c9d5aa015 100644
--- a/resources/recipes/greader.recipe
+++ b/resources/recipes/greader.recipe
@@ -4,29 +4,27 @@ from calibre import __appname__
class GoogleReader(BasicNewsRecipe):
title = 'Google Reader'
- description = 'This recipe downloads feeds you have tagged from your Google Reader account.'
+ description = 'This recipe fetches from your Google Reader account unread Starred items and unread Feeds you have placed in a folder via the manage subscriptions feature.'
needs_subscription = True
- __author__ = 'davec'
+ __author__ = 'davec, rollercoaster, Starson17'
base_url = 'http://www.google.com/reader/atom/'
- max_articles_per_feed = 50
+ oldest_article = 365
+ max_articles_per_feed = 250
get_options = '?n=%d&xt=user/-/state/com.google/read' % max_articles_per_feed
use_embedded_content = True
def get_browser(self):
- br = BasicNewsRecipe.get_browser()
-
+ br = BasicNewsRecipe.get_browser(self)
if self.username is not None and self.password is not None:
request = urllib.urlencode([('Email', self.username), ('Passwd', self.password),
- ('service', 'reader'), ('source', __appname__)])
+ ('service', 'reader'), ('accountType', 'HOSTED_OR_GOOGLE'), ('source', __appname__)])
response = br.open('https://www.google.com/accounts/ClientLogin', request)
- sid = re.search('SID=(\S*)', response.read()).group(1)
-
+ auth = re.search('Auth=(\S*)', response.read()).group(1)
cookies = mechanize.CookieJar()
br = mechanize.build_opener(mechanize.HTTPCookieProcessor(cookies))
- cookies.set_cookie(mechanize.Cookie(None, 'SID', sid, None, False, '.google.com', True, True, '/', True, False, None, True, '', '', None))
+ br.addheaders = [('Authorization', 'GoogleLogin auth='+auth)]
return br
-
def get_feeds(self):
feeds = []
soup = self.index_to_soup('http://www.google.com/reader/api/0/tag/list')
diff --git a/resources/recipes/greader_uber.recipe b/resources/recipes/greader_uber.recipe
index ee48e7069d..5e02cdef5d 100644
--- a/resources/recipes/greader_uber.recipe
+++ b/resources/recipes/greader_uber.recipe
@@ -3,10 +3,10 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
from calibre import __appname__
class GoogleReaderUber(BasicNewsRecipe):
- title = 'Google Reader Uber'
- description = 'This recipe downloads all unread feedsfrom your Google Reader account.'
+ title = 'Google Reader uber'
+ description = 'Fetches all feeds from your Google Reader account including the uncategorized items.'
needs_subscription = True
- __author__ = 'rollercoaster, davec'
+ __author__ = 'davec, rollercoaster, Starson17'
base_url = 'http://www.google.com/reader/atom/'
oldest_article = 365
max_articles_per_feed = 250
@@ -14,20 +14,17 @@ class GoogleReaderUber(BasicNewsRecipe):
use_embedded_content = True
def get_browser(self):
- br = BasicNewsRecipe.get_browser()
-
+ br = BasicNewsRecipe.get_browser(self)
if self.username is not None and self.password is not None:
request = urllib.urlencode([('Email', self.username), ('Passwd', self.password),
- ('service', 'reader'), ('source', __appname__)])
+ ('service', 'reader'), ('accountType', 'HOSTED_OR_GOOGLE'), ('source', __appname__)])
response = br.open('https://www.google.com/accounts/ClientLogin', request)
- sid = re.search('SID=(\S*)', response.read()).group(1)
-
+ auth = re.search('Auth=(\S*)', response.read()).group(1)
cookies = mechanize.CookieJar()
br = mechanize.build_opener(mechanize.HTTPCookieProcessor(cookies))
- cookies.set_cookie(mechanize.Cookie(None, 'SID', sid, None, False, '.google.com', True, True, '/', True, False, None, True, '', '', None))
+ br.addheaders = [('Authorization', 'GoogleLogin auth='+auth)]
return br
-
def get_feeds(self):
feeds = []
soup = self.index_to_soup('http://www.google.com/reader/api/0/tag/list')
diff --git a/resources/recipes/orlando_sentinel.recipe b/resources/recipes/orlando_sentinel.recipe
new file mode 100644
index 0000000000..7a59f6f6ba
--- /dev/null
+++ b/resources/recipes/orlando_sentinel.recipe
@@ -0,0 +1,38 @@
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class AdvancedUserRecipe1279258912(BasicNewsRecipe):
+ title = u'Orlando Sentinel'
+ oldest_article = 3
+ max_articles_per_feed = 100
+
+ feeds = [
+ (u'News', u'http://feeds.feedburner.com/orlandosentinel/news'),
+ (u'Opinion', u'http://feeds.feedburner.com/orlandosentinel/news/opinion'),
+ (u'Business', u'http://feeds.feedburner.com/orlandosentinel/business'),
+ (u'Technology', u'http://feeds.feedburner.com/orlandosentinel/technology'),
+ (u'Space and Science', u'http://feeds.feedburner.com/orlandosentinel/news/space'),
+ (u'Entertainment', u'http://feeds.feedburner.com/orlandosentinel/entertainment'),
+ (u'Life and Family', u'http://feeds.feedburner.com/orlandosentinel/features/lifestyle'),
+ ]
+ __author__ = 'rty'
+ pubisher = 'OrlandoSentinel.com'
+ description = 'Orlando, Florida, Newspaper'
+ category = 'News, Orlando, Florida'
+
+
+ remove_javascript = True
+ use_embedded_content = False
+ no_stylesheets = True
+ language = 'en'
+ encoding = 'utf-8'
+ conversion_options = {'linearize_tables':True}
+ masthead_url = 'http://www.orlandosentinel.com/media/graphic/2009-07/46844851.gif'
+ keep_only_tags = [
+ dict(name='div', attrs={'class':'story'})
+ ]
+ remove_tags = [
+ dict(name='div', attrs={'class':['articlerail','tools','comment-group','clearfix']}),
+ ]
+ remove_tags_after = [
+ dict(name='p', attrs={'class':'copyright'}),
+ ]
diff --git a/resources/recipes/waco_tribune.recipe b/resources/recipes/waco_tribune.recipe
new file mode 100644
index 0000000000..18eb61fb26
--- /dev/null
+++ b/resources/recipes/waco_tribune.recipe
@@ -0,0 +1,34 @@
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class AdvancedUserRecipe1278773519(BasicNewsRecipe):
+ title = u'Waco Tribune Herald'
+ __author__ = 'rty'
+ pubisher = 'A Robinson Media Company'
+ description = 'Waco, Texas, Newspaper'
+ category = 'News, Texas, Waco'
+ oldest_article = 7
+ max_articles_per_feed = 100
+
+ feeds = [
+ (u'News', u'http://www.wacotrib.com/news/index.rss2'),
+ (u'Sports', u'http://www.wacotrib.com/sports/index.rss2'),
+ (u'AccessWaco', u'http://www.wacotrib.com/accesswaco/index.rss2'),
+ (u'Opinions', u'http://www.wacotrib.com/opinion/index.rss2')
+ ]
+
+ remove_javascript = True
+ use_embedded_content = False
+ no_stylesheets = True
+ language = 'en'
+ encoding = 'utf-8'
+ conversion_options = {'linearize_tables':True}
+ masthead_url = 'http://media.wacotrib.com/designimages/wacotrib_logo.jpg'
+ keep_only_tags = [
+ dict(name='div', attrs={'class':'twoColumn left'}),
+ ]
+ remove_tags = [
+ dict(name='div', attrs={'class':'right blueLinks'}),
+ ]
+ remove_tags_after = [
+ dict(name='div', attrs={'class':'dottedRule'}),
+ ]
diff --git a/resources/templates/rtf.xsl b/resources/templates/rtf.xsl
index 9199654665..74696f0857 100644
--- a/resources/templates/rtf.xsl
+++ b/resources/templates/rtf.xsl
@@ -111,7 +111,6 @@
or (@shadow = 'true')
or (@hidden = 'true')
or (@outline = 'true')
-
">
@@ -277,6 +276,26 @@
]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/setup/installer/linux/freeze.py b/setup/installer/linux/freeze.py
index 8c56ed4fb7..382c7ffeee 100644
--- a/setup/installer/linux/freeze.py
+++ b/setup/installer/linux/freeze.py
@@ -40,6 +40,7 @@ class LinuxFreeze(Command):
'/usr/bin/pdftohtml',
'/usr/lib/libwmflite-0.2.so.7',
'/usr/lib/liblcms.so.1',
+ '/usr/lib/liblcms2.so.2',
'/usr/lib/libstlport.so.5.1',
'/tmp/calibre-mount-helper',
'/usr/lib/libunrar.so',
@@ -50,10 +51,9 @@ class LinuxFreeze(Command):
'/usr/lib/libpodofo.so.0.8.1',
'/lib/libz.so.1',
'/lib/libuuid.so.1',
- '/usr/lib/libtiff.so.3',
+ '/usr/lib/libtiff.so.5',
'/lib/libbz2.so.1',
- '/usr/lib/libpoppler.so.5',
- '/usr/lib/libpoppler-qt4.so.3',
+ '/usr/lib/libpoppler.so.6',
'/usr/lib/libxml2.so.2',
'/usr/lib/libopenjpeg.so.2',
'/usr/lib/libxslt.so.1',
@@ -62,10 +62,10 @@ class LinuxFreeze(Command):
'/usr/lib/libgthread-2.0.so.0',
stdcpp,
ffi,
- '/usr/lib/libpng12.so.0',
+ '/usr/lib/libpng14.so.14',
'/usr/lib/libexslt.so.0',
- '/usr/lib/libMagickWand.so.2',
- '/usr/lib/libMagickCore.so.2',
+ '/usr/lib/libMagickWand.so.3',
+ '/usr/lib/libMagickCore.so.3',
'/usr/lib/libgcrypt.so.11',
'/usr/lib/libgpg-error.so.0',
'/usr/lib/libphonon.so.4',
diff --git a/setup/installer/windows/freeze.py b/setup/installer/windows/freeze.py
index 0b04cc11cc..29809907ab 100644
--- a/setup/installer/windows/freeze.py
+++ b/setup/installer/windows/freeze.py
@@ -13,7 +13,7 @@ from setup import Command, modules, functions, basenames, __version__, \
from setup.build_environment import msvc, MT, RC
from setup.installer.windows.wix import WixMixIn
-QT_DIR = 'C:\\Qt\\4.6.0'
+QT_DIR = 'C:\\Qt\\4.6.3'
QT_DLLS = ['Core', 'Gui', 'Network', 'Svg', 'WebKit', 'Xml', 'XmlPatterns']
LIBUSB_DIR = 'C:\\libusb'
LIBUNRAR = 'C:\\Program Files\\UnrarDLL\\unrar.dll'
diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py
index 92ee2ca6d2..15bd54c80c 100644
--- a/src/calibre/__init__.py
+++ b/src/calibre/__init__.py
@@ -361,6 +361,8 @@ def strftime(fmt, t=None):
before 1900 '''
if t is None:
t = time.localtime()
+ if hasattr(t, 'timetuple'):
+ t = t.timetuple()
early_year = t[0] < 1900
if early_year:
replacement = 1900 if t[0]%4 == 0 else 1901
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index 9d876b42d1..35cb0ad3d2 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -446,7 +446,7 @@ from calibre.devices.eb600.driver import EB600, COOL_ER, SHINEBOOK, \
BOOQ, ELONEX, POCKETBOOK301, MENTOR
from calibre.devices.iliad.driver import ILIAD
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
-from calibre.devices.jetbook.driver import JETBOOK
+from calibre.devices.jetbook.driver import JETBOOK, MIBUK
from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX
from calibre.devices.nook.driver import NOOK
from calibre.devices.prs505.driver import PRS505
@@ -467,12 +467,12 @@ from calibre.devices.kobo.driver import KOBO
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
LibraryThing
from calibre.ebooks.metadata.douban import DoubanBooks
-from calibre.library.catalog import CSV_XML, EPUB_MOBI
+from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX
from calibre.ebooks.epub.fix.unmanifested import Unmanifested
from calibre.ebooks.epub.fix.epubcheck import Epubcheck
plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon,
- LibraryThing, DoubanBooks, CSV_XML, EPUB_MOBI, Unmanifested, Epubcheck]
+ LibraryThing, DoubanBooks, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested, Epubcheck]
plugins += [
ComicInput,
EPUBInput,
@@ -517,6 +517,7 @@ plugins += [
IREXDR1000,
IREXDR800,
JETBOOK,
+ MIBUK,
SHINEBOOK,
POCKETBOOK360,
POCKETBOOK301,
diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py
index 5642235b31..5d9d094b26 100644
--- a/src/calibre/devices/android/driver.py
+++ b/src/calibre/devices/android/driver.py
@@ -30,7 +30,8 @@ class ANDROID(USBMS):
0x18d1 : { 0x4e11 : [0x0100, 0x226], 0x4e12: [0x0100, 0x226]},
# Samsung
- 0x04e8 : { 0x681d : [0x0222, 0x0400], 0x681c : [0x0222, 0x0224]},
+ 0x04e8 : { 0x681d : [0x0222, 0x0400],
+ 0x681c : [0x0222, 0x0224, 0x0400]},
# Acer
0x502 : { 0x3203 : [0x0100]},
@@ -70,6 +71,16 @@ class ANDROID(USBMS):
dirs = [x.strip() for x in dirs.split(',')]
self.EBOOK_DIR_MAIN = dirs
+ def get_main_ebook_dir(self, for_upload=False):
+ dirs = self.EBOOK_DIR_MAIN
+ if not for_upload:
+ def aldiko_tweak(x):
+ return 'eBooks' if x == 'eBooks/import' else x
+ if isinstance(dirs, basestring):
+ dirs = [dirs]
+ dirs = list(map(aldiko_tweak, dirs))
+ return dirs
+
class S60(USBMS):
name = 'S60 driver'
diff --git a/src/calibre/devices/jetbook/driver.py b/src/calibre/devices/jetbook/driver.py
index 671fea5d75..6a3bc635ff 100644
--- a/src/calibre/devices/jetbook/driver.py
+++ b/src/calibre/devices/jetbook/driver.py
@@ -80,3 +80,21 @@ class JETBOOK(USBMS):
return mi
+class MIBUK(USBMS):
+
+ name = 'MiBuk Wolder Device Interface'
+ description = _('Communicate with the MiBuk Wolder reader.')
+ author = 'Kovid Goyal'
+ supported_platforms = ['windows', 'osx', 'linux']
+
+ FORMATS = ['epub', 'mobi', 'prc', 'fb2', 'txt', 'rtf', 'pdf']
+
+ VENDOR_ID = [0x0525]
+ PRODUCT_ID = [0xa4a5]
+ BCD = [0x314]
+ SUPPORTS_SUB_DIRS = True
+
+ VENDOR_NAME = 'LINUX'
+ WINDOWS_MAIN_MEM = 'WOLDERMIBUK'
+
+
diff --git a/src/calibre/devices/prs505/sony_cache.py b/src/calibre/devices/prs505/sony_cache.py
index b87ca937bc..3ac35df9b2 100644
--- a/src/calibre/devices/prs505/sony_cache.py
+++ b/src/calibre/devices/prs505/sony_cache.py
@@ -10,10 +10,10 @@ from base64 import b64decode
from uuid import uuid4
from lxml import etree
-from calibre import prints, guess_type
+from calibre import prints, guess_type, isbytestring
from calibre.devices.errors import DeviceError
from calibre.devices.usbms.driver import debug_print
-from calibre.constants import DEBUG
+from calibre.constants import DEBUG, preferred_encoding
from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.metadata import authors_to_string, title_sort
@@ -473,6 +473,13 @@ class XMLCache(object):
# if the case of a tie, and hope it is right.
timestamp = os.path.getmtime(path)
rec_date = record.get('date', None)
+
+ def clean(x):
+ if isbytestring(x):
+ x = x.decode(preferred_encoding, 'replace')
+ x.replace(u'\0', '')
+ return x
+
if not getattr(book, '_new_book', False): # book is not new
if strftime(timestamp, zone=time.gmtime) == rec_date:
gtz_count += 1
@@ -486,19 +493,19 @@ class XMLCache(object):
tz = time.gmtime
debug_print("Using GMT TZ for new book", book.lpath)
date = strftime(timestamp, zone=tz)
- record.set('date', date)
+ record.set('date', clean(date))
- record.set('size', str(os.stat(path).st_size))
+ record.set('size', clean(str(os.stat(path).st_size)))
title = book.title if book.title else _('Unknown')
- record.set('title', title)
+ record.set('title', clean(title))
ts = book.title_sort
if not ts:
ts = title_sort(title)
- record.set('titleSorter', ts)
+ record.set('titleSorter', clean(ts))
if self.use_author_sort and book.author_sort is not None:
- record.set('author', book.author_sort)
+ record.set('author', clean(book.author_sort))
else:
- record.set('author', authors_to_string(book.authors))
+ record.set('author', clean(authors_to_string(book.authors)))
ext = os.path.splitext(path)[1]
if ext:
ext = ext[1:].lower()
@@ -506,7 +513,7 @@ class XMLCache(object):
if mime is None:
mime = guess_type('a.'+ext)[0]
if mime is not None:
- record.set('mime', mime)
+ record.set('mime', clean(mime))
if 'sourceid' not in record.attrib:
record.set('sourceid', '1')
if 'id' not in record.attrib:
diff --git a/src/calibre/devices/scanner.py b/src/calibre/devices/scanner.py
index ceba5d37d0..dd789dd668 100644
--- a/src/calibre/devices/scanner.py
+++ b/src/calibre/devices/scanner.py
@@ -98,6 +98,9 @@ class LinuxScanner(object):
def __call__(self):
ans = set([])
+ if not self.ok:
+ raise RuntimeError('DeviceScanner requires the /sys filesystem to work.')
+
for x in os.listdir(self.base):
base = os.path.join(self.base, x)
ven = os.path.join(base, 'idVendor')
@@ -145,8 +148,6 @@ class DeviceScanner(object):
def __init__(self, *args):
if isosx and osx_scanner is None:
raise RuntimeError('The Python extension usbobserver must be available on OS X.')
- if islinux and not linux_scanner.ok:
- raise RuntimeError('DeviceScanner requires the /sys filesystem to work.')
self.scanner = win_scanner if iswindows else osx_scanner if isosx else linux_scanner
self.devices = []
diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py
index 42d0f3c863..cdba980642 100644
--- a/src/calibre/devices/usbms/books.py
+++ b/src/calibre/devices/usbms/books.py
@@ -72,13 +72,13 @@ class Book(MetaInformation):
def thumbnail(self):
return None
- def smart_update(self, other):
+ def smart_update(self, other, replace_metadata=False):
'''
Merge the information in C{other} into self. In case of conflicts, the information
in C{other} takes precedence, unless the information in C{other} is NULL.
'''
- MetaInformation.smart_update(self, other, replace_tags=True)
+ MetaInformation.smart_update(self, other, replace_metadata)
for attr in self.BOOK_ATTRS:
if hasattr(other, attr):
@@ -116,7 +116,7 @@ class BookList(_BookList):
self.append(book)
return True
if replace_metadata:
- self[b].smart_update(book)
+ self[b].smart_update(book, replace_metadata=True)
return True
return False
@@ -132,6 +132,8 @@ class CollectionsBookList(BookList):
return True
def get_collections(self, collection_attributes):
+ from calibre.devices.usbms.driver import debug_print
+ debug_print('Starting get_collections:', prefs['manage_device_metadata'])
collections = {}
series_categories = set([])
# This map of sets is used to avoid linear searches when testing for
@@ -146,14 +148,19 @@ class CollectionsBookList(BookList):
# book in all existing collections. Do not add any new ones.
attrs = ['device_collections']
if getattr(book, '_new_book', False):
- if prefs['preserve_user_collections']:
+ if prefs['manage_device_metadata'] == 'manual':
# Ensure that the book is in all the book's existing
# collections plus all metadata collections
attrs += collection_attributes
else:
- # The book's existing collections are ignored. Put the book
- # in collections defined by its metadata.
+ # For new books, both 'on_send' and 'on_connect' do the same
+ # thing. The book's existing collections are ignored. Put
+ # the book in collections defined by its metadata.
attrs = collection_attributes
+ elif prefs['manage_device_metadata'] == 'on_connect':
+ # For existing books, modify the collections only if the user
+ # specified 'on_connect'
+ attrs = collection_attributes
for attr in attrs:
attr = attr.strip()
val = getattr(book, attr, None)
diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py
index 55790420f2..c07b7fd761 100644
--- a/src/calibre/devices/usbms/device.py
+++ b/src/calibre/devices/usbms/device.py
@@ -732,7 +732,7 @@ class Device(DeviceConfig, DevicePlugin):
traceback.print_exc()
self._main_prefix = self._card_a_prefix = self._card_b_prefix = None
- def get_main_ebook_dir(self):
+ def get_main_ebook_dir(self, for_upload=False):
return self.EBOOK_DIR_MAIN
def _sanity_check(self, on_card, files):
@@ -750,7 +750,7 @@ class Device(DeviceConfig, DevicePlugin):
path = os.path.join(self._card_b_prefix,
*(self.EBOOK_DIR_CARD_B.split('/')))
else:
- candidates = self.get_main_ebook_dir()
+ candidates = self.get_main_ebook_dir(for_upload=True)
if isinstance(candidates, basestring):
candidates = [candidates]
candidates = [
diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py
index 377ec36c16..73a329be58 100644
--- a/src/calibre/devices/usbms/driver.py
+++ b/src/calibre/devices/usbms/driver.py
@@ -58,7 +58,7 @@ class USBMS(CLI, Device):
debug_print ('USBMS: Fetching list of books from device. oncard=', oncard)
- dummy_bl = BookList(None, None, None)
+ dummy_bl = self.booklist_class(None, None, None)
if oncard == 'carda' and not self._card_a_prefix:
self.report_progress(1.0, _('Getting list of books on device...'))
@@ -78,6 +78,8 @@ class USBMS(CLI, Device):
self.EBOOK_DIR_CARD_B if oncard == 'cardb' else \
self.get_main_ebook_dir()
+ debug_print ('USBMS: dirs are:', prefix, ebook_dirs)
+
# get the metadata cache
bl = self.booklist_class(oncard, prefix, self.settings)
need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE)
diff --git a/src/calibre/ebooks/epub/fix/container.py b/src/calibre/ebooks/epub/fix/container.py
index 7a7c17427a..b9af66d708 100644
--- a/src/calibre/ebooks/epub/fix/container.py
+++ b/src/calibre/ebooks/epub/fix/container.py
@@ -5,7 +5,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-import os, posixpath, urllib, sys
+import os, posixpath, urllib, sys, re
from lxml import etree
@@ -160,8 +160,26 @@ class Container(object):
mt = mimetype.lower()
if mt.endswith('+xml'):
parser = etree.XMLParser(no_network=True, huge_tree=not iswindows)
- return etree.fromstring(xml_to_unicode(raw,
- strip_encoding_pats=True, assume_utf8=True)[0], parser=parser)
+ raw = xml_to_unicode(raw,
+ strip_encoding_pats=True, assume_utf8=True,
+ resolve_entities=True)[0].strip()
+ idx = raw.find(' -1:
+ pre = raw[:idx]
+ raw = raw[idx:]
+ if ']+)', pre):
+ val = match.group(2)
+ if val.startswith('"') and val.endswith('"'):
+ val = val[1:-1]
+ user_entities[match.group(1)] = val
+ if user_entities:
+ pat = re.compile(r'&(%s);'%('|'.join(user_entities.keys())))
+ raw = pat.sub(lambda m:user_entities[m.group(1)], raw)
+ return etree.fromstring(raw, parser=parser)
return raw
def write(self, path):
diff --git a/src/calibre/ebooks/epub/fix/epubcheck.py b/src/calibre/ebooks/epub/fix/epubcheck.py
index f5c8086e7c..fd913a654b 100644
--- a/src/calibre/ebooks/epub/fix/epubcheck.py
+++ b/src/calibre/ebooks/epub/fix/epubcheck.py
@@ -21,7 +21,7 @@ class Epubcheck(ePubFixer):
def long_description(self):
return _('Workarounds for bugs in the latest release of epubcheck. '
'epubcheck reports many things as errors that are not '
- 'actually errors. %prog will try to detect these and replace '
+ 'actually errors. epub-fix will try to detect these and replace '
'them with constructs that epubcheck likes. This may cause '
'significant changes to your epub, complain to the epubcheck '
'project.')
diff --git a/src/calibre/ebooks/epub/fix/unmanifested.py b/src/calibre/ebooks/epub/fix/unmanifested.py
index 71913e9d50..da7a9a9d0e 100644
--- a/src/calibre/ebooks/epub/fix/unmanifested.py
+++ b/src/calibre/ebooks/epub/fix/unmanifested.py
@@ -18,7 +18,7 @@ class Unmanifested(ePubFixer):
@property
def long_description(self):
- return _('Fix unmanifested files. %prog can either add them to '
+ return _('Fix unmanifested files. epub-fix can either add them to '
'the manifest or delete them as specified by the '
'delete unmanifested option.')
diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py
index 0dbffd5f7f..e45334777e 100644
--- a/src/calibre/ebooks/metadata/__init__.py
+++ b/src/calibre/ebooks/metadata/__init__.py
@@ -268,10 +268,12 @@ class MetaInformation(object):
):
prints(x, getattr(self, x, 'None'))
- def smart_update(self, mi, replace_tags=False):
+ def smart_update(self, mi, replace_metadata=False):
'''
- Merge the information in C{mi} into self. In case of conflicts, the information
- in C{mi} takes precedence, unless the information in mi is NULL.
+ Merge the information in C{mi} into self. In case of conflicts, the
+ information in C{mi} takes precedence, unless the information in mi is
+ NULL. If replace_metadata is True, then the information in mi always
+ takes precedence.
'''
if mi.title and mi.title != _('Unknown'):
self.title = mi.title
@@ -285,13 +287,16 @@ class MetaInformation(object):
'cover', 'guide', 'book_producer',
'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate', 'rights',
'publication_type', 'uuid'):
- if hasattr(mi, attr):
+ if replace_metadata:
+ setattr(self, attr, getattr(mi, attr, 1.0 if \
+ attr == 'series_index' else None))
+ elif hasattr(mi, attr):
val = getattr(mi, attr)
if val is not None:
setattr(self, attr, val)
if mi.tags:
- if replace_tags:
+ if replace_metadata:
self.tags = mi.tags
else:
self.tags += mi.tags
diff --git a/src/calibre/gui2/catalog/catalog_bibtex.py b/src/calibre/gui2/catalog/catalog_bibtex.py
new file mode 100644
index 0000000000..ea222603b7
--- /dev/null
+++ b/src/calibre/gui2/catalog/catalog_bibtex.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+from __future__ import with_statement
+
+__license__ = 'GPL v3'
+__copyright__ = '2009, Kovid Goyal '
+__docformat__ = 'restructuredtext en'
+
+
+from calibre.gui2 import gprefs
+from calibre.gui2.catalog.catalog_bibtex_ui import Ui_Form
+from PyQt4.Qt import QWidget, QListWidgetItem
+
+class PluginWidget(QWidget, Ui_Form):
+
+ TITLE = _('BibTeX Options')
+ HELP = _('Options specific to')+' BibTeX '+_('output')
+ OPTION_FIELDS = [('bib_cit','{authors}{id}'),
+ ('bib_entry', 0), #mixed
+ ('bibfile_enc', 0), #utf-8
+ ('bibfile_enctag', 0), #strict
+ ('impcit', True) ]
+
+ sync_enabled = False
+ formats = set(['bib'])
+
+ def __init__(self, parent=None):
+ QWidget.__init__(self, parent)
+ self.setupUi(self)
+ from calibre.library.catalog import FIELDS
+ self.all_fields = []
+ for x in FIELDS :
+ if x != 'all':
+ self.all_fields.append(x)
+ QListWidgetItem(x, self.db_fields)
+
+ def initialize(self, name): #not working properly to update
+ self.name = name
+ fields = gprefs.get(name+'_db_fields', self.all_fields)
+ # Restore the activated db_fields from last use
+ for x in xrange(self.db_fields.count()):
+ item = self.db_fields.item(x)
+ item.setSelected(unicode(item.text()) in fields)
+ # Update dialog fields from stored options
+ for opt in self.OPTION_FIELDS:
+ opt_value = gprefs.get(self.name + '_' + opt[0], opt[1])
+ if opt[0] in ['bibfile_enc', 'bibfile_enctag', 'bib_entry']:
+ getattr(self, opt[0]).setCurrentIndex(opt_value)
+ elif opt[0] == 'impcit' :
+ getattr(self, opt[0]).setChecked(opt_value)
+ else:
+ getattr(self, opt[0]).setText(opt_value)
+
+ def options(self):
+
+ # Save the currently activated fields
+ fields = []
+ for x in xrange(self.db_fields.count()):
+ item = self.db_fields.item(x)
+ if item.isSelected():
+ fields.append(unicode(item.text()))
+ gprefs.set(self.name+'_db_fields', fields)
+
+ # Dictionary currently activated fields
+ if len(self.db_fields.selectedItems()):
+ opts_dict = {'fields':[unicode(item.text()) for item in self.db_fields.selectedItems()]}
+ else:
+ opts_dict = {'fields':['all']}
+
+ # Save/return the current options
+ # bib_cit stores as text
+ # 'bibfile_enc','bibfile_enctag' stores as int (Indexes)
+ for opt in self.OPTION_FIELDS:
+ if opt[0] in ['bibfile_enc', 'bibfile_enctag', 'bib_entry']:
+ opt_value = getattr(self,opt[0]).currentIndex()
+ elif opt[0] == 'impcit' :
+ opt_value = getattr(self, opt[0]).isChecked()
+ else :
+ opt_value = unicode(getattr(self, opt[0]).text())
+ gprefs.set(self.name + '_' + opt[0], opt_value)
+
+ opts_dict[opt[0]] = opt_value
+
+ return opts_dict
diff --git a/src/calibre/gui2/catalog/catalog_bibtex.ui b/src/calibre/gui2/catalog/catalog_bibtex.ui
new file mode 100644
index 0000000000..7f4920655d
--- /dev/null
+++ b/src/calibre/gui2/catalog/catalog_bibtex.ui
@@ -0,0 +1,173 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 579
+ 411
+
+
+
+ Form
+
+
+
+
+
+ Bib file encoding:
+
+
+
+
+
+
+ Fields to include in output:
+
+
+
+
+
+
+
+ utf-8
+
+
+
+
+ cp1252
+
+
+
+
+ ascii/LaTeX
+
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ QAbstractItemView::MultiSelection
+
+
+
+
+
+
+ Encoding configuration (change if you have errors) :
+
+
+
+
+
+
+
+ strict
+
+
+
+
+ replace
+
+
+
+
+ ignore
+
+
+
+
+ backslashreplace
+
+
+
+
+
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 60
+
+
+
+
+
+
+
+ BibTeX entry type:
+
+
+
+
+
+
+
+ mixed
+
+
+
+
+ misc
+
+
+
+
+ book
+
+
+
+
+
+
+
+ Create a citation tag?
+
+
+
+
+
+
+ Expression to form the BibTeX citation tag:
+
+
+
+
+
+
+
+
+
+ Some explanation about this template:
+ -The fields availables are 'author_sort', 'authors', 'id',
+ 'isbn', 'pubdate', 'publisher', 'series_index', 'series',
+ 'tags', 'timestamp', 'title', 'uuid'
+ -For list types ie authors and tags, only the first element
+ wil be selected.
+ -For time field, only the date will be used.
+
+
+ false
+
+
+
+
+
+
+
+
diff --git a/src/calibre/gui2/convert/metadata.ui b/src/calibre/gui2/convert/metadata.ui
index ec5a913f18..7bc45e234e 100644
--- a/src/calibre/gui2/convert/metadata.ui
+++ b/src/calibre/gui2/convert/metadata.ui
@@ -20,6 +20,30 @@
Book Cover
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+ Use cover from &source file
+
+
+ true
+
+
+
@@ -71,30 +95,6 @@
-
-
-
- Use cover from &source file
-
-
- true
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
-
- opt_prefer_metadata_cover
@@ -232,9 +232,6 @@
QComboBox::InsertAlphabetically
-
- QComboBox::AdjustToContents
-
diff --git a/src/calibre/gui2/convert/regex_builder.py b/src/calibre/gui2/convert/regex_builder.py
index 6fa0fa5fe4..b10772b86c 100644
--- a/src/calibre/gui2/convert/regex_builder.py
+++ b/src/calibre/gui2/convert/regex_builder.py
@@ -28,9 +28,10 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
if not db or not book_id:
self.button_box.addButton(QDialogButtonBox.Open)
- else:
- self.select_format(db, book_id)
-
+ elif not self.select_format(db, book_id):
+ self.cancelled = True
+ return
+ self.cancelled = False
self.connect(self.button_box, SIGNAL('clicked(QAbstractButton*)'), self.button_clicked)
self.connect(self.regex, SIGNAL('textChanged(QString)'), self.regex_valid)
self.connect(self.test, SIGNAL('clicked()'), self.do_test)
@@ -79,10 +80,12 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
format = d.format()
if not format:
- error_dialog(self, _('No formats available'), _('Cannot build regex using the GUI builder without a book.'))
- QDialog.reject()
- else:
- self.open_book(db.format_abspath(book_id, format, index_is_id=True))
+ error_dialog(self, _('No formats available'),
+ _('Cannot build regex using the GUI builder without a book.'),
+ show=True)
+ return False
+ self.open_book(db.format_abspath(book_id, format, index_is_id=True))
+ return True
def open_book(self, pathtoebook):
self.iterator = EbookIterator(pathtoebook)
@@ -117,6 +120,8 @@ class RegexEdit(QWidget, Ui_Edit):
def builder(self):
bld = RegexBuilder(self.db, self.book_id, self.edit.text(), self)
+ if bld.cancelled:
+ return
if bld.exec_() == bld.Accepted:
self.edit.setText(bld.regex.text())
diff --git a/src/calibre/gui2/convert/structure_detection.ui b/src/calibre/gui2/convert/structure_detection.ui
index e4414473f5..2e97c0d3ca 100644
--- a/src/calibre/gui2/convert/structure_detection.ui
+++ b/src/calibre/gui2/convert/structure_detection.ui
@@ -28,7 +28,11 @@
-
+
+
+ 20
+
+
diff --git a/src/calibre/gui2/convert/xexp_edit.ui b/src/calibre/gui2/convert/xexp_edit.ui
index 1b0196a8a1..f98eb8b1b8 100644
--- a/src/calibre/gui2/convert/xexp_edit.ui
+++ b/src/calibre/gui2/convert/xexp_edit.ui
@@ -43,6 +43,15 @@
0
+
+
+ 500
+ 16777215
+
+
+
+ 30
+
diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py
index 2a9c81e8ee..96232fe85f 100644
--- a/src/calibre/gui2/custom_column_widgets.py
+++ b/src/calibre/gui2/custom_column_widgets.py
@@ -10,9 +10,10 @@ from functools import partial
from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
QDate, QGroupBox, QVBoxLayout, QPlainTextEdit, QSizePolicy, \
- QSpacerItem, QIcon, QCheckBox, QWidget, QHBoxLayout, SIGNAL
+ QSpacerItem, QIcon, QCheckBox, QWidget, QHBoxLayout, SIGNAL, \
+ QPushButton
-from calibre.utils.date import qt_to_dt
+from calibre.utils.date import qt_to_dt, now
from calibre.gui2.widgets import TagsLineEdit, EnComboBox
from calibre.gui2 import UNDEFINED_QDATE
from calibre.utils.config import tweaks
@@ -132,20 +133,30 @@ class DateEdit(QDateEdit):
def focusInEvent(self, x):
self.setSpecialValueText('')
+ QDateEdit.focusInEvent(self, x)
def focusOutEvent(self, x):
self.setSpecialValueText(_('Undefined'))
+ QDateEdit.focusOutEvent(self, x)
+
+ def set_to_today(self):
+ self.setDate(now())
class DateTime(Base):
def setup_ui(self, parent):
- self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
- DateEdit(parent)]
+ cm = self.col_metadata
+ self.widgets = [QLabel('&'+cm['name']+':', parent), DateEdit(parent),
+ QLabel(''), QPushButton(_('Set \'%s\' to today')%cm['name'], parent)]
w = self.widgets[1]
- w.setDisplayFormat('dd MMM yyyy')
+ format = cm['display'].get('date_format','')
+ if not format:
+ format = 'dd MMM yyyy'
+ w.setDisplayFormat(format)
w.setCalendarPopup(True)
w.setMinimumDate(UNDEFINED_QDATE)
w.setSpecialValueText(_('Undefined'))
+ self.widgets[3].clicked.connect(w.set_to_today)
def setter(self, val):
if val is None:
diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py
index e2a99864ec..bc8ba7c381 100644
--- a/src/calibre/gui2/device.py
+++ b/src/calibre/gui2/device.py
@@ -33,6 +33,7 @@ from calibre.devices.apple.driver import ITUNES_ASYNC
from calibre.devices.folder_device.driver import FOLDER_DEVICE
from calibre.ebooks.metadata.meta import set_metadata
from calibre.constants import DEBUG
+from calibre.utils.config import prefs
# }}}
@@ -764,6 +765,7 @@ class DeviceMixin(object): # {{{
self.book_details.reset_info()
self.location_view.setCurrentIndex(self.location_view.model().index(0))
self.refresh_ondevice_info (device_connected = False)
+ self.tool_bar.device_status_changed(bool(connected))
def info_read(self, job):
'''
@@ -1424,19 +1426,25 @@ class DeviceMixin(object): # {{{
aus = re.sub('(?u)\W|[_]', '', aus)
self.db_book_title_cache[title]['author_sort'][aus] = mi
self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi
- self.db_book_uuid_cache[mi.uuid] = mi.application_id
+ self.db_book_uuid_cache[mi.uuid] = mi
# Now iterate through all the books on the device, setting the
# in_library field Fastest and most accurate key is the uuid. Second is
# the application_id, which is really the db key, but as this can
# accidentally match across libraries we also verify the title. The
# db_id exists on Sony devices. Fallback is title and author match
+
+ update_metadata = prefs['manage_device_metadata'] == 'on_connect'
for booklist in booklists:
for book in booklist:
if getattr(book, 'uuid', None) in self.db_book_uuid_cache:
+ if update_metadata:
+ book.smart_update(self.db_book_uuid_cache[book.uuid],
+ replace_metadata=True)
book.in_library = True
# ensure that the correct application_id is set
- book.application_id = self.db_book_uuid_cache[book.uuid]
+ book.application_id = \
+ self.db_book_uuid_cache[book.uuid].application_id
continue
book_title = book.title.lower() if book.title else ''
@@ -1446,11 +1454,15 @@ class DeviceMixin(object): # {{{
if d is not None:
if getattr(book, 'application_id', None) in d['db_ids']:
book.in_library = True
- book.smart_update(d['db_ids'][book.application_id])
+ if update_metadata:
+ book.smart_update(d['db_ids'][book.application_id],
+ replace_metadata=True)
continue
if book.db_id in d['db_ids']:
book.in_library = True
- book.smart_update(d['db_ids'][book.db_id])
+ if update_metadata:
+ book.smart_update(d['db_ids'][book.db_id],
+ replace_metadata=True)
continue
if book.authors:
# Compare against both author and author sort, because
@@ -1459,14 +1471,21 @@ class DeviceMixin(object): # {{{
book_authors = re.sub('(?u)\W|[_]', '', book_authors)
if book_authors in d['authors']:
book.in_library = True
- book.smart_update(d['authors'][book_authors])
+ if update_metadata:
+ book.smart_update(d['authors'][book_authors],
+ replace_metadata=True)
elif book_authors in d['author_sort']:
book.in_library = True
- book.smart_update(d['author_sort'][book_authors])
+ if update_metadata:
+ book.smart_update(d['author_sort'][book_authors],
+ replace_metadata=True)
# Set author_sort if it isn't already
asort = getattr(book, 'author_sort', None)
if not asort and book.authors:
book.author_sort = self.library_view.model().db.author_sort_from_authors(book.authors)
+ if update_metadata:
+ if self.device_manager.is_device_connected:
+ self.device_manager.sync_booklists(None, booklists)
# }}}
diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py
index bf9dc0a623..b064dc53c2 100644
--- a/src/calibre/gui2/dialogs/config/__init__.py
+++ b/src/calibre/gui2/dialogs/config/__init__.py
@@ -334,7 +334,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
def __init__(self, parent, library_view, server=None):
ResizableDialog.__init__(self, parent)
- self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
self._category_model = CategoryModel()
self.category_view.currentChanged = self.category_current_changed
@@ -389,10 +388,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.add_custcol_button.clicked.connect(self.add_custcol)
self.edit_custcol_button.clicked.connect(self.edit_custcol)
- icons = config['toolbar_icon_size']
- self.toolbar_button_size.setCurrentIndex(0 if icons == self.ICON_SIZES[0] else 1 if icons == self.ICON_SIZES[1] else 2)
- self.show_toolbar_text.setChecked(config['show_text_in_toolbar'])
-
output_formats = sorted(available_output_formats())
output_formats.remove('oeb')
for f in output_formats:
@@ -845,8 +840,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
must_restart = self.apply_custom_column_changes()
- config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
- config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
config['separate_cover_flow'] = bool(self.separate_cover_flow.isChecked())
config['disable_tray_notification'] = not self.systray_notifications.isChecked()
p = {0:'normal', 1:'high', 2:'low'}[self.priority.currentIndex()]
diff --git a/src/calibre/gui2/dialogs/config/add_save.py b/src/calibre/gui2/dialogs/config/add_save.py
index b1f5621f44..8eb6cf7bd0 100644
--- a/src/calibre/gui2/dialogs/config/add_save.py
+++ b/src/calibre/gui2/dialogs/config/add_save.py
@@ -45,7 +45,12 @@ class AddSave(QTabWidget, Ui_TabWidget):
self.metadata_box.layout().insertWidget(0, self.filename_pattern)
self.opt_swap_author_names.setChecked(prefs['swap_author_names'])
self.opt_add_formats_to_existing.setChecked(prefs['add_formats_to_existing'])
- self.preserve_user_collections.setChecked(prefs['preserve_user_collections'])
+ if prefs['manage_device_metadata'] == 'manual':
+ self.manage_device_metadata.setCurrentIndex(0)
+ elif prefs['manage_device_metadata'] == 'on_send':
+ self.manage_device_metadata.setCurrentIndex(1)
+ else:
+ self.manage_device_metadata.setCurrentIndex(2)
help = '\n'.join(textwrap.wrap(c.get_option('template').help, 75))
self.save_template.initialize('save_to_disk', opts.template, help)
self.send_template.initialize('send_to_device', opts.send_template, help)
@@ -72,12 +77,14 @@ class AddSave(QTabWidget, Ui_TabWidget):
prefs['filename_pattern'] = pattern
prefs['swap_author_names'] = bool(self.opt_swap_author_names.isChecked())
prefs['add_formats_to_existing'] = bool(self.opt_add_formats_to_existing.isChecked())
- prefs['preserve_user_collections'] = bool(self.preserve_user_collections.isChecked())
-
+ if self.manage_device_metadata.currentIndex() == 0:
+ prefs['manage_device_metadata'] = 'manual'
+ elif self.manage_device_metadata.currentIndex() == 1:
+ prefs['manage_device_metadata'] = 'on_send'
+ else:
+ prefs['manage_device_metadata'] = 'on_connect'
return True
-
-
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app=QApplication([])
diff --git a/src/calibre/gui2/dialogs/config/add_save.ui b/src/calibre/gui2/dialogs/config/add_save.ui
index 64a8137aa1..c8ee0419a9 100644
--- a/src/calibre/gui2/dialogs/config/add_save.ui
+++ b/src/calibre/gui2/dialogs/config/add_save.ui
@@ -6,7 +6,7 @@
00
- 588
+ 671516
@@ -177,32 +177,81 @@ Title match ignores leading indefinite articles ("the", "a",
Sending to &device
-
-
-
+
+
+
+
+
+ 0
+ 0
+
+
- Preserve device collections.
+ Metadata &management:
+
+
+ manage_device_metadata
-
+
+
+
+
+ 0
+ 0
+
+
+
+
+ Manual management
+
+
+
+
+ Only on send
+
+
+
+
+ Automatic management
+
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+
+ 313
+ 20
+
+
+
+
+
- If checked, collections will not be deleted even if a book with changed metadata is resent and the collection is not in the book's metadata. In addition, editing collections in the device view will be enabled. If unchecked, collections will be always reflect only the metadata in the calibre library.
+ <li><b>Manual Management</b>: Calibre updates the metadata and adds collections only when a book is sent. With this option, calibre will never remove a collection.</li>
+<li><b>Only on send</b>: Calibre updates metadata and adds/removes collections for a book only when it is sent to the device. </li>
+<li><b>Automatic management</b>: Calibre automatically keeps metadata on the device in sync with the calibre library, on every connect</li></ul>true
-
+
-
+
-
+ Here you can control how calibre will save your books when you click the Send to Device button. This setting can be overriden for individual devices by customizing the device interface plugins in Preferences->Plugins
@@ -212,7 +261,7 @@ Title match ignores leading indefinite articles ("the", "a",
-
+
diff --git a/src/calibre/gui2/dialogs/config/config.ui b/src/calibre/gui2/dialogs/config/config.ui
index b473ee7846..5f890631b2 100644
--- a/src/calibre/gui2/dialogs/config/config.ui
+++ b/src/calibre/gui2/dialogs/config/config.ui
@@ -422,54 +422,6 @@
-
-
- Toolbar
-
-
-
-
-
-
- Large
-
-
-
-
- Medium
-
-
-
-
- Small
-
-
-
-
-
-
-
- &Button size in toolbar
-
-
- toolbar_button_size
-
-
-
-
-
-
- Show &text in toolbar buttons
-
-
- true
-
-
-
-
-
-
-
diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui
index 4efb48d870..5da9d37d04 100644
--- a/src/calibre/gui2/dialogs/metadata_single.ui
+++ b/src/calibre/gui2/dialogs/metadata_single.ui
@@ -277,12 +277,6 @@
-
-
- 0
- 0
-
- List of known series. You can add new series.
@@ -295,9 +289,6 @@
QComboBox::InsertAlphabetically
-
- QComboBox::AdjustToContents
-
diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py
index 9a4bb22f82..2474685522 100644
--- a/src/calibre/gui2/init.py
+++ b/src/calibre/gui2/init.py
@@ -176,12 +176,6 @@ class ToolbarMixin(object): # {{{
def show_help(self, *args):
open_url(QUrl('http://calibre-ebook.com/user_manual'))
- def read_toolbar_settings(self):
- self.tool_bar.setIconSize(config['toolbar_icon_size'])
- self.tool_bar.setToolButtonStyle(
- Qt.ToolButtonTextUnderIcon if \
- config['show_text_in_toolbar'] else \
- Qt.ToolButtonIconOnly)
# }}}
@@ -409,7 +403,8 @@ class StatusBar(QStatusBar): # {{{
self.clearMessage()
def message_changed(self, msg):
- if not msg or msg.isEmpty() or msg.isNull():
+ if not msg or msg.isEmpty() or msg.isNull() or \
+ not unicode(msg).strip():
extra = ''
if self.device_string:
extra = ' ..::.. ' + self.device_string
diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py
index f3b650f531..c1ff3ab505 100644
--- a/src/calibre/gui2/layout.py
+++ b/src/calibre/gui2/layout.py
@@ -5,48 +5,21 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
+from operator import attrgetter
+
from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, QVariant, \
QAbstractListModel, QFont, QApplication, QPalette, pyqtSignal, QToolButton, \
QModelIndex, QListView, QAbstractButton, QPainter, QPixmap, QColor, \
- QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QComboBox
+ QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout
from calibre.constants import __appname__, filesystem_encoding
from calibre.gui2.search_box import SearchBox2, SavedSearchBox
from calibre.gui2.throbber import ThrobbingButton
-from calibre.gui2 import NONE
+from calibre.gui2 import NONE, config
+from calibre.gui2.widgets import ComboBoxWithHelp
from calibre import human_readable
-class ToolBar(QToolBar): # {{{
-
- def __init__(self, parent=None):
- QToolBar.__init__(self, parent)
- self.setContextMenuPolicy(Qt.PreventContextMenu)
- self.setMovable(False)
- self.setFloatable(False)
- self.setOrientation(Qt.Horizontal)
- self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
- self.setIconSize(QSize(48, 48))
- self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
-
- def add_actions(self, *args):
- self.left_space = QWidget(self)
- self.left_space.setSizePolicy(QSizePolicy.Expanding,
- QSizePolicy.Minimum)
- self.addWidget(self.left_space)
- for action in args:
- if action is None:
- self.addSeparator()
- else:
- self.addAction(action)
- self.right_space = QWidget(self)
- self.right_space.setSizePolicy(QSizePolicy.Expanding,
- QSizePolicy.Minimum)
- self.addWidget(self.right_space)
-
- def contextMenuEvent(self, *args):
- pass
-
-# }}}
+ICON_SIZE = 48
# Location View {{{
@@ -165,7 +138,7 @@ class LocationModel(QAbstractListModel): # {{{
class LocationView(QListView):
- unmount_device = pyqtSignal()
+ umount_device = pyqtSignal()
location_selected = pyqtSignal(object)
def __init__(self, parent):
@@ -190,22 +163,27 @@ class LocationView(QListView):
self.setTabKeyNavigation(True)
self.setProperty("showDropIndicator", True)
self.setSelectionMode(self.SingleSelection)
- self.setIconSize(QSize(40, 40))
+ self.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
self.setMovement(self.Static)
self.setFlow(self.LeftToRight)
- self.setGridSize(QSize(175, 90))
+ self.setGridSize(QSize(175, ICON_SIZE))
self.setViewMode(self.ListMode)
self.setWordWrap(True)
self.setObjectName("location_view")
- self.setMaximumHeight(74)
+ self.setMaximumSize(QSize(600, ICON_SIZE+16))
+ self.setMinimumWidth(400)
def eject_clicked(self, *args):
- self.unmount_device.emit()
+ self.umount_device.emit()
def count_changed(self, new_count):
self.model().count = new_count
self.model().reset()
+ @property
+ def book_count(self):
+ return self.model().count
+
def current_changed(self, current, previous):
if current.isValid():
i = current.row()
@@ -247,12 +225,15 @@ class EjectButton(QAbstractButton):
def __init__(self, parent):
QAbstractButton.__init__(self, parent)
self.mouse_over = False
+ self.setMouseTracking(True)
def enterEvent(self, event):
self.mouse_over = True
+ QAbstractButton.enterEvent(self, event)
def leaveEvent(self, event):
self.mouse_over = False
+ QAbstractButton.leaveEvent(self, event)
def paintEvent(self, event):
painter = QPainter(self)
@@ -280,12 +261,7 @@ class SearchBar(QWidget): # {{{
self._layout = l = QHBoxLayout()
self.setLayout(self._layout)
- self.restriction_label = QLabel(_("&Restrict to:"))
- l.addWidget(self.restriction_label)
- self.restriction_label.setSizePolicy(QSizePolicy.Minimum,
- QSizePolicy.Minimum)
-
- x = QComboBox(self)
+ x = ComboBoxWithHelp(self)
x.setMaximumSize(QSize(150, 16777215))
x.setObjectName("search_restriction")
x.setToolTip(_("Books display will be restricted to those matching the selected saved search"))
@@ -344,38 +320,88 @@ class SearchBar(QWidget): # {{{
x.setToolTip(_("Delete current saved search"))
self.label.setBuddy(parent.search)
- self.restriction_label.setBuddy(parent.search_restriction)
# }}}
-class LocationBar(ToolBar): # {{{
+class ToolBar(QToolBar): # {{{
def __init__(self, actions, donate, location_view, parent=None):
- ToolBar.__init__(self, parent)
-
- for ac in actions:
- self.addAction(ac)
-
- self.addWidget(location_view)
- self.w = QWidget()
- self.w.setLayout(QVBoxLayout())
- self.w.layout().addWidget(donate)
- donate.setAutoRaise(True)
- donate.setCursor(Qt.PointingHandCursor)
- self.addWidget(self.w)
- self.setIconSize(QSize(50, 50))
+ QToolBar.__init__(self, parent)
+ self.setContextMenuPolicy(Qt.PreventContextMenu)
+ self.setMovable(False)
+ self.setFloatable(False)
+ self.setOrientation(Qt.Horizontal)
+ self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
+ self.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
- def button_for_action(self, ac):
- b = QToolButton(self)
- b.setDefaultAction(ac)
- for x in ('ToolTip', 'StatusTip', 'WhatsThis'):
- getattr(b, 'set'+x)(b.text())
+ self.showing_device = False
+ self.all_actions = actions
+ self.donate = donate
+ self.location_view = location_view
+ self.d_widget = QWidget()
+ self.d_widget.setLayout(QVBoxLayout())
+ self.d_widget.layout().addWidget(donate)
+ donate.setAutoRaise(True)
+ donate.setCursor(Qt.PointingHandCursor)
+ self.build_bar()
+
+ def contextMenuEvent(self, *args):
+ pass
+
+ def device_status_changed(self, connected):
+ self.showing_device = connected
+ self.build_bar()
+
+ def build_bar(self):
+ order_field = 'device' if self.showing_device else 'normal'
+ o = attrgetter(order_field+'_order')
+ sepvals = [2] if self.showing_device else [1]
+ sepvals += [3]
+ actions = [x for x in self.all_actions if o(x) > -1]
+ actions.sort(cmp=lambda x,y : cmp(o(x), o(y)))
+ self.clear()
+ for x in actions:
+ self.addAction(x)
+ ch = self.widgetForAction(x)
+ ch.setCursor(Qt.PointingHandCursor)
+ ch.setAutoRaise(True)
+
+ if x.action_name == 'choose_library':
+ self.location_action = self.addWidget(self.location_view)
+ self.choose_action = x
+ if config['show_donate_button']:
+ self.addWidget(self.d_widget)
+ if x.action_name not in ('choose_library', 'help'):
+ ch.setPopupMode(ch.MenuButtonPopup)
+
+
+ for x in actions:
+ if x.separator_before in sepvals:
+ self.insertSeparator(x)
+
+
+ self.location_action.setVisible(self.showing_device)
+ self.choose_action.setVisible(not self.showing_device)
+
+ def count_changed(self, new_count):
+ text = _('%d books')%new_count
+ a = self.choose_action
+ a.setText(text)
+
+ def resizeEvent(self, ev):
+ style = Qt.ToolButtonTextUnderIcon
+ if self.size().width() < 1260:
+ style = Qt.ToolButtonIconOnly
+ self.setToolButtonStyle(style)
+ QToolBar.resizeEvent(self, ev)
- return b
# }}}
+class Action(QAction):
+ pass
+
class MainWindowMixin(object):
def __init__(self):
@@ -390,12 +416,19 @@ class MainWindowMixin(object):
self.centralwidget.setLayout(self._central_widget_layout)
self.resize(1012, 740)
self.donate_button = ThrobbingButton(self.centralwidget)
- self.donate_button.set_normal_icon_size(64, 64)
+ self.donate_button.set_normal_icon_size(ICON_SIZE, ICON_SIZE)
# Actions {{{
- def ac(name, text, icon, shortcut=None, tooltip=None):
- action = QAction(QIcon(I(icon)), text, self)
+ all_actions = []
+
+ def ac(normal_order, device_order, separator_before,
+ name, text, icon, shortcut=None, tooltip=None):
+ action = Action(QIcon(I(icon)), text, self)
+ action.normal_order = normal_order
+ action.device_order = device_order
+ action.separator_before = separator_before
+ action.action_name = name
text = tooltip if tooltip else text
action.setToolTip(text)
action.setStatusTip(text)
@@ -405,56 +438,46 @@ class MainWindowMixin(object):
if shortcut:
action.setShortcut(shortcut)
setattr(self, 'action_'+name, action)
+ all_actions.append(action)
- ac('add', _('Add books'), 'add_book.svg', _('A'))
- ac('del', _('Remove books'), 'trash.svg', _('Del'))
- ac('edit', _('Edit meta info'), 'edit_input.svg', _('E'))
- ac('merge', _('Merge book records'), 'merge_books.svg', _('M'))
- ac('sync', _('Send to device'), 'sync.svg')
- ac('save', _('Save to disk'), 'save.svg', _('S'))
- ac('news', _('Fetch news'), 'news.svg', _('F'))
- ac('convert', _('Convert books'), 'convert.svg', _('C'))
- ac('view', _('View'), 'view.svg', _('V'))
- ac('open_containing_folder', _('Open containing folder'),
+ ac(0, 7, 0, 'add', _('Add books'), 'add_book.svg', _('A'))
+ ac(1, 1, 0, 'edit', _('Edit metadata'), 'edit_input.svg', _('E'))
+ ac(2, 2, 3, 'convert', _('Convert books'), 'convert.svg', _('C'))
+ ac(3, 3, 0, 'view', _('View'), 'view.svg', _('V'))
+ ac(4, 4, 3, 'choose_library', _('%d books')%0, 'lt.png',
+ tooltip=_('Choose calibre library to work with'))
+ ac(5, 5, 3, 'news', _('Fetch news'), 'news.svg', _('F'))
+ ac(6, 6, 0, 'save', _('Save to disk'), 'save.svg', _('S'))
+ ac(7, 0, 0, 'sync', _('Send to device'), 'sync.svg')
+ ac(8, 8, 3, 'del', _('Remove books'), 'trash.svg', _('Del'))
+ ac(9, 9, 3, 'help', _('Help'), 'help.svg', _('F1'), _("Browse the calibre User Manual"))
+ ac(10, 10, 0, 'preferences', _('Preferences'), 'config.svg', _('Ctrl+P'))
+
+ ac(-1, -1, 0, 'merge', _('Merge book records'), 'merge_books.svg', _('M'))
+ ac(-1, -1, 0, 'open_containing_folder', _('Open containing folder'),
'document_open.svg')
- ac('show_book_details', _('Show book details'),
+ ac(-1, -1, 0, 'show_book_details', _('Show book details'),
'dialog_information.svg')
- ac('books_by_same_author', _('Books by same author'),
+ ac(-1, -1, 0, 'books_by_same_author', _('Books by same author'),
'user_profile.svg')
- ac('books_in_this_series', _('Books in this series'),
+ ac(-1, -1, 0, 'books_in_this_series', _('Books in this series'),
'books_in_series.svg')
- ac('books_by_this_publisher', _('Books by this publisher'),
+ ac(-1, -1, 0, 'books_by_this_publisher', _('Books by this publisher'),
'publisher.png')
- ac('books_with_the_same_tags', _('Books with the same tags'),
+ ac(-1, -1, 0, 'books_with_the_same_tags', _('Books with the same tags'),
'tags.svg')
- ac('preferences', _('Preferences'), 'config.svg', _('Ctrl+P'))
- ac('help', _('Help'), 'help.svg', _('F1'), _("Browse the calibre User Manual"))
# }}}
- self.tool_bar = ToolBar(self)
- self.addToolBar(Qt.BottomToolBarArea, self.tool_bar)
- self.tool_bar.add_actions(self.action_convert, self.action_view,
- None, self.action_edit, None,
- self.action_save, self.action_del,
- None,
- self.action_help, None, self.action_preferences)
-
self.location_view = LocationView(self.centralwidget)
self.search_bar = SearchBar(self)
- self.location_bar = LocationBar([self.action_add, self.action_sync,
- self.action_news], self.donate_button, self.location_view, self)
- self.addToolBar(Qt.TopToolBarArea, self.location_bar)
+ self.tool_bar = ToolBar(all_actions, self.donate_button, self.location_view, self)
+ self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
l = self.centralwidget.layout()
l.addWidget(self.search_bar)
- for ch in list(self.tool_bar.children()) + list(self.location_bar.children()):
- if isinstance(ch, QToolButton):
- ch.setCursor(Qt.PointingHandCursor)
- ch.setAutoRaise(True)
- if ch is not self.donate_button:
- ch.setPopupMode(ch.MenuButtonPopup)
-
+ def read_toolbar_settings(self):
+ pass
diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py
index 529055ecd2..40f7a2e4e0 100644
--- a/src/calibre/gui2/library/delegates.py
+++ b/src/calibre/gui2/library/delegates.py
@@ -96,7 +96,7 @@ class DateDelegate(QStyledItemDelegate): # {{{
def displayText(self, val, locale):
d = val.toDate()
- if d == UNDEFINED_QDATE:
+ if d <= UNDEFINED_QDATE:
return ''
return format_date(d.toPyDate(), 'dd MMM yyyy')
@@ -116,7 +116,7 @@ class PubDateDelegate(QStyledItemDelegate): # {{{
def displayText(self, val, locale):
d = val.toDate()
- if d == UNDEFINED_QDATE:
+ if d <= UNDEFINED_QDATE:
return ''
format = tweaks['gui_pubdate_display_format']
if format is None:
@@ -194,7 +194,7 @@ class CcDateDelegate(QStyledItemDelegate): # {{{
def displayText(self, val, locale):
d = val.toDate()
- if d == UNDEFINED_QDATE:
+ if d <= UNDEFINED_QDATE:
return ''
return format_date(d.toPyDate(), self.format)
@@ -217,7 +217,7 @@ class CcDateDelegate(QStyledItemDelegate): # {{{
def setModelData(self, editor, model, index):
val = editor.date()
- if val == UNDEFINED_QDATE:
+ if val <= UNDEFINED_QDATE:
val = None
model.setData(index, QVariant(val), Qt.EditRole)
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index 3bbab52b33..89008735fe 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -944,7 +944,7 @@ class DeviceBooksModel(BooksModel): # {{{
(cname == 'collections' and \
callable(getattr(self.db, 'supports_collections', None)) and \
self.db.supports_collections() and \
- prefs['preserve_user_collections']):
+ prefs['manage_device_metadata']=='manual'):
flags |= Qt.ItemIsEditable
return flags
@@ -1216,7 +1216,9 @@ class DeviceBooksModel(BooksModel): # {{{
return done
def set_editable(self, editable):
- self.editable = editable
+ # Cannot edit if metadata is sent on connect. Reason: changes will
+ # revert to what is in the library on next connect.
+ self.editable = editable and prefs['manage_device_metadata']!='on_connect'
def set_search_restriction(self, s):
pass
diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py
index 9d85dce075..c6c32f86f7 100644
--- a/src/calibre/gui2/library/views.py
+++ b/src/calibre/gui2/library/views.py
@@ -503,7 +503,7 @@ class DeviceBooksView(BooksView): # {{{
self.edit_collections_menu.setVisible(
callable(getattr(self._model.db, 'supports_collections', None)) and \
self._model.db.supports_collections() and \
- prefs['preserve_user_collections'])
+ prefs['manage_device_metadata'] == 'manual')
self.context_menu.popup(event.globalPos())
event.accept()
diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py
index 3a71fa3de0..a4186ad8d1 100644
--- a/src/calibre/gui2/search_restriction_mixin.py
+++ b/src/calibre/gui2/search_restriction_mixin.py
@@ -7,11 +7,13 @@ Created on 10 Jun 2010
class SearchRestrictionMixin(object):
def __init__(self):
- self.search_restriction.activated[str].connect(self.apply_search_restriction)
+ self.search_restriction.initialize(help_text=_('Restrict to'))
+ self.search_restriction.activated[int].connect(self.apply_search_restriction)
self.library_view.model().count_changed_signal.connect(self.restriction_count_changed)
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
self.search_restriction.setMinimumContentsLength(10)
self.search_restriction.setStatusTip(self.search_restriction.toolTip())
+ self.search_count.setText(_("(all books)"))
'''
Adding and deleting books while restricted creates a complexity. When added,
@@ -27,8 +29,8 @@ class SearchRestrictionMixin(object):
if self.restriction_in_effect:
self.set_number_of_books_shown()
- def apply_search_restriction(self, r):
- r = unicode(r)
+ def apply_search_restriction(self, i):
+ r = unicode(self.search_restriction.currentText())
if r is not None and r != '':
self.restriction_in_effect = True
restriction = 'search:"%s"'%(r)
diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py
index 6bd7b2b502..ba4c637932 100644
--- a/src/calibre/gui2/ui.py
+++ b/src/calibre/gui2/ui.py
@@ -167,8 +167,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
self.eject_action = self.system_tray_menu.addAction(
QIcon(I('eject.svg')), _('&Eject connected device'))
self.eject_action.setEnabled(False)
- if not config['show_donate_button']:
- self.donate_button.setVisible(False)
self.addAction(self.quit_action)
self.action_restart = QAction(_('&Restart'), self)
self.addAction(self.action_restart)
@@ -220,8 +218,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
if self.system_tray_icon.isVisible() and opts.start_in_tray:
self.hide_windows()
- self.library_view.model().count_changed_signal.connect \
- (self.location_view.count_changed)
+ for t in (self.location_view, self.tool_bar):
+ self.library_view.model().count_changed_signal.connect \
+ (t.count_changed)
if not gprefs.get('quick_start_guide_added', False):
from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
@@ -274,8 +273,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, ToolbarMixin, # {{{
SIGNAL('start_recipe_fetch(PyQt_PyObject)'),
self.download_scheduled_recipe, Qt.QueuedConnection)
- self.location_view.setCurrentIndex(self.location_view.model().index(0))
-
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
AddAction.__init__(self)
diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py
index e3fd503872..994fa4575f 100644
--- a/src/calibre/gui2/widgets.py
+++ b/src/calibre/gui2/widgets.py
@@ -490,6 +490,7 @@ class EnComboBox(QComboBox):
QComboBox.__init__(self, *args)
self.setLineEdit(EnLineEdit(self))
self.setAutoCompletionCaseSensitivity(Qt.CaseSensitive)
+ self.setMinimumContentsLength(20)
def text(self):
return unicode(self.currentText())
@@ -538,6 +539,53 @@ class HistoryLineEdit(QComboBox):
def text(self):
return self.currentText()
+class ComboBoxWithHelp(QComboBox):
+ '''
+ A combobox where item 0 is help text. CurrentText will return '' for item 0.
+ Be sure to always fetch the text with currentText. Don't use the signals
+ that pass a string, because they will not correct the text.
+ '''
+ def __init__(self, parent=None):
+ QComboBox.__init__(self, parent)
+ self.currentIndexChanged[int].connect(self.index_changed)
+ self.help_text = ''
+ self.state_set = False
+
+ def initialize(self, help_text=_('Search')):
+ self.help_text = help_text
+ self.set_state()
+
+ def set_state(self):
+ if not self.state_set:
+ if self.currentIndex() == 0:
+ self.setItemText(0, self.help_text)
+ self.setStyleSheet('QComboBox { color: gray }')
+ else:
+ self.setItemText(0, '')
+ self.setStyleSheet('QComboBox { color: black }')
+
+ def index_changed(self, index):
+ self.state_set = False
+ self.set_state()
+
+ def currentText(self):
+ if self.currentIndex() == 0:
+ return ''
+ return QComboBox.currentText(self)
+
+ def itemText(self, idx):
+ if idx == 0:
+ return ''
+ return QComboBox.itemText(self, idx)
+
+ def showPopup(self):
+ self.setItemText(0, '')
+ QComboBox.showPopup(self)
+
+ def hidePopup(self):
+ QComboBox.hidePopup(self)
+ self.set_state()
+
class PythonHighlighter(QSyntaxHighlighter):
Rules = []
diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py
index d46ae23d90..af950a36fc 100644
--- a/src/calibre/library/caches.py
+++ b/src/calibre/library/caches.py
@@ -209,13 +209,13 @@ class ResultCache(SearchQueryParser):
if query == 'false':
for item in self._data:
if item is None: continue
- if item[loc] is None or item[loc] == UNDEFINED_DATE:
+ if item[loc] is None or item[loc] <= UNDEFINED_DATE:
matches.add(item[0])
return matches
if query == 'true':
for item in self._data:
if item is None: continue
- if item[loc] is not None and item[loc] != UNDEFINED_DATE:
+ if item[loc] is not None and item[loc] > UNDEFINED_DATE:
matches.add(item[0])
return matches
diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py
index 21aa863031..a540a8a660 100644
--- a/src/calibre/library/catalog.py
+++ b/src/calibre/library/catalog.py
@@ -1,7 +1,9 @@
+# -*- coding: utf-8 -*-
+
__license__ = 'GPL v3'
__copyright__ = '2010, Greg Riker '
-import datetime, htmlentitydefs, os, re, shutil
+import datetime, htmlentitydefs, os, re, shutil, codecs
from collections import namedtuple
from copy import deepcopy
@@ -9,6 +11,7 @@ from copy import deepcopy
from xml.sax.saxutils import escape
from calibre import filesystem_encoding, prints, prepare_string_for_xml, strftime
+from calibre.constants import preferred_encoding
from calibre.customize import CatalogPlugin
from calibre.customize.conversion import OptionRecommendation, DummyReporter
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
@@ -21,6 +24,10 @@ FIELDS = ['all', 'author_sort', 'authors', 'comments',
'series_index', 'series', 'size', 'tags', 'timestamp', 'title',
'uuid']
+#Allowed fields for template
+TEMPLATE_ALLOWED_FIELDS = [ 'author_sort', 'authors', 'id', 'isbn', 'pubdate',
+ 'publisher', 'series_index', 'series', 'tags', 'timestamp', 'title', 'uuid' ]
+
class CSV_XML(CatalogPlugin):
'CSV/XML catalog generator'
@@ -89,17 +96,20 @@ class CSV_XML(CatalogPlugin):
fields = self.get_output_fields(opts)
if self.fmt == 'csv':
- outfile = open(path_to_output, 'w')
+ outfile = codecs.open(path_to_output, 'w', 'utf8')
# Output the field headers
outfile.write(u'%s\n' % u','.join(fields))
# Output the entry fields
for entry in data:
- outstr = ''
- for (x, field) in enumerate(fields):
+ outstr = []
+ for field in fields:
item = entry[field]
- if field == 'formats':
+ if item is None:
+ outstr.append('""')
+ continue
+ elif field == 'formats':
fmt_list = []
for format in item:
fmt_list.append(format.rpartition('.')[2].lower())
@@ -111,18 +121,13 @@ class CSV_XML(CatalogPlugin):
item = u'%s' % re.sub(r'[\D]', '', item)
elif field in ['pubdate', 'timestamp']:
item = isoformat(item)
+ elif field == 'comments':
+ item = item.replace(u'\r\n',u' ')
+ item = item.replace(u'\n',u' ')
- if x < len(fields) - 1:
- if item is not None:
- outstr += u'"%s",' % unicode(item).replace('"','""')
- else:
- outstr += '"",'
- else:
- if item is not None:
- outstr += u'"%s"\n' % unicode(item).replace('"','""')
- else:
- outstr += '""\n'
- outfile.write(outstr.encode('utf-8'))
+ outstr.append(u'"%s"' % unicode(item).replace('"','""'))
+
+ outfile.write(u','.join(outstr) + u'\n')
outfile.close()
elif self.fmt == 'xml':
@@ -181,6 +186,329 @@ class CSV_XML(CatalogPlugin):
f.write(etree.tostring(root, encoding='utf-8',
xml_declaration=True, pretty_print=True))
+class BIBTEX(CatalogPlugin):
+ 'BIBTEX catalog generator'
+
+ Option = namedtuple('Option', 'option, default, dest, action, help')
+
+ name = 'Catalog_BIBTEX'
+ description = 'BIBTEX catalog generator'
+ supported_platforms = ['windows', 'osx', 'linux']
+ author = 'Sengian'
+ version = (1, 0, 0)
+ file_types = set(['bib'])
+
+ cli_options = [
+ Option('--fields',
+ default = 'all',
+ dest = 'fields',
+ action = None,
+ help = _('The fields to output when cataloging books in the '
+ 'database. Should be a comma-separated list of fields.\n'
+ 'Available fields: %s.\n'
+ "Default: '%%default'\n"
+ "Applies to: BIBTEX output format")%', '.join(FIELDS)),
+
+ Option('--sort-by',
+ default = 'id',
+ dest = 'sort_by',
+ action = None,
+ help = _('Output field to sort on.\n'
+ 'Available fields: author_sort, id, rating, size, timestamp, title.\n'
+ "Default: '%default'\n"
+ "Applies to: BIBTEX output format")),
+
+ Option('--create-citation',
+ default = 'True',
+ dest = 'impcit',
+ action = None,
+ help = _('Create a citation for BibTeX entries.\n'
+ 'Boolean value: True, False\n'
+ "Default: '%default'\n"
+ "Applies to: BIBTEX output format")),
+
+ Option('--citation-template',
+ default = '{authors}{id}',
+ dest = 'bib_cit',
+ action = None,
+ help = _('The template for citation creation from database fields.\n'
+ ' Should be a template with {} enclosed fields.\n'
+ 'Available fields: %s.\n'
+ "Default: '%%default'\n"
+ "Applies to: BIBTEX output format")%', '.join(TEMPLATE_ALLOWED_FIELDS)),
+
+ Option('--choose-encoding',
+ default = 'utf8',
+ dest = 'bibfile_enc',
+ action = None,
+ help = _('BibTeX file encoding output.\n'
+ 'Available types: utf8, cp1252, ascii.\n'
+ "Default: '%default'\n"
+ "Applies to: BIBTEX output format")),
+
+ Option('--choose-encoding-configuration',
+ default = 'strict',
+ dest = 'bibfile_enctag',
+ action = None,
+ help = _('BibTeX file encoding flag.\n'
+ 'Available types: strict, replace, ignore, backslashreplace.\n'
+ "Default: '%default'\n"
+ "Applies to: BIBTEX output format")),
+
+ Option('--entry-type',
+ default = 'book',
+ dest = 'bib_entry',
+ action = None,
+ help = _('Entry type for BibTeX catalog.\n'
+ 'Available types: book, misc, mixed.\n'
+ "Default: '%default'\n"
+ "Applies to: BIBTEX output format"))]
+
+ def run(self, path_to_output, opts, db, notification=DummyReporter()):
+
+ from types import StringType, UnicodeType
+
+ from calibre.library.save_to_disk import preprocess_template
+ #Bibtex functions
+ from calibre.utils.bibtex import bibtex_author_format, utf8ToBibtex, ValidateCitationKey
+
+ def create_bibtex_entry(entry, fields, mode, template_citation,
+ asccii_bibtex = True, citation_bibtex = True):
+
+ #Bibtex doesn't like UTF-8 but keep unicode until writing
+ #Define starting chain or if book valid strict and not book return a Fail string
+
+ bibtex_entry = []
+ if mode != "misc" and check_entry_book_valid(entry) :
+ bibtex_entry.append(u'@book{')
+ elif mode != "book" :
+ bibtex_entry.append(u'@misc{')
+ else :
+ #case strict book
+ return ''
+
+ if citation_bibtex :
+ # Citation tag
+ bibtex_entry.append(make_bibtex_citation(entry, template_citation, asccii_bibtex))
+ bibtex_entry = [u' '.join(bibtex_entry)]
+
+ for field in fields:
+ item = entry[field]
+ #check if the field should be included (none or empty)
+ if item is None:
+ continue
+ try:
+ if len(item) == 0 :
+ continue
+ except TypeError:
+ pass
+
+ if field == 'authors' :
+ bibtex_entry.append(u'author = "%s"' % bibtex_author_format(item))
+
+ elif field in ['title', 'publisher', 'cover', 'uuid',
+ 'author_sort', 'series'] :
+ bibtex_entry.append(u'%s = "%s"' % (field, utf8ToBibtex(item, asccii_bibtex)))
+
+ elif field == 'id' :
+ bibtex_entry.append(u'calibreid = "%s"' % int(item))
+
+ elif field == 'rating' :
+ bibtex_entry.append(u'rating = "%s"' % int(item))
+
+ elif field == 'size' :
+ bibtex_entry.append(u'%s = "%s octets"' % (field, int(item)))
+
+ elif field == 'tags' :
+ #A list to flatten
+ bibtex_entry.append(u'tags = "%s"' % utf8ToBibtex(u', '.join(item), asccii_bibtex))
+
+ elif field == 'comments' :
+ #\n removal
+ item = item.replace(u'\r\n',u' ')
+ item = item.replace(u'\n',u' ')
+ bibtex_entry.append(u'note = "%s"' % utf8ToBibtex(item, asccii_bibtex))
+
+ elif field == 'isbn' :
+ # Could be 9, 10 or 13 digits
+ bibtex_entry.append(u'isbn = "%s"' % re.sub(u'[\D]', u'', item))
+
+ elif field == 'formats' :
+ item = u', '.join([format.rpartition('.')[2].lower() for format in item])
+ bibtex_entry.append(u'formats = "%s"' % item)
+
+ elif field == 'series_index' :
+ bibtex_entry.append(u'volume = "%s"' % int(item))
+
+ elif field == 'timestamp' :
+ bibtex_entry.append(u'timestamp = "%s"' % isoformat(item).partition('T')[0])
+
+ elif field == 'pubdate' :
+ bibtex_entry.append(u'year = "%s"' % item.year)
+ bibtex_entry.append(u'month = "%s"' % utf8ToBibtex(strftime("%b", item),
+ asccii_bibtex))
+
+ bibtex_entry = u',\n '.join(bibtex_entry)
+ bibtex_entry += u' }\n\n'
+
+ return bibtex_entry
+
+ def check_entry_book_valid(entry):
+ #Check that the required fields are ok for a book entry
+ for field in ['title', 'authors', 'publisher'] :
+ if entry[field] is None or len(entry[field]) == 0 :
+ return False
+ if entry['pubdate'] is None :
+ return False
+ else :
+ return True
+
+ def make_bibtex_citation(entry, template_citation, asccii_bibtex):
+
+ #define a function to replace the template entry by its value
+ def tpl_replace(objtplname) :
+
+ tpl_field = re.sub(u'[\{\}]', u'', objtplname.group())
+
+ if tpl_field in TEMPLATE_ALLOWED_FIELDS :
+ if tpl_field in ['pubdate', 'timestamp'] :
+ tpl_field = isoformat(entry[tpl_field]).partition('T')[0]
+ elif tpl_field in ['tags', 'authors'] :
+ tpl_field =entry[tpl_field][0]
+ elif tpl_field in ['id', 'series_index'] :
+ tpl_field = str(entry[tpl_field])
+ else :
+ tpl_field = entry[tpl_field]
+ return tpl_field
+ else:
+ return u''
+
+ if len(template_citation) >0 :
+ tpl_citation = utf8ToBibtex(ValidateCitationKey(re.sub(u'\{[^{}]*\}',
+ tpl_replace, template_citation)), asccii_bibtex)
+
+ if len(tpl_citation) >0 :
+ return tpl_citation
+
+ if len(entry["isbn"]) > 0 :
+ template_citation = u'%s' % re.sub(u'[\D]',u'', entry["isbn"])
+
+ else :
+ template_citation = u'%s' % str(entry["id"])
+
+ if asccii_bibtex :
+ return ValidateCitationKey(template_citation.encode('ascii', 'replace'))
+ else :
+ return ValidateCitationKey(template_citation)
+
+ self.fmt = path_to_output.rpartition('.')[2]
+ self.notification = notification
+
+ # Combobox options
+ bibfile_enc = ['utf8', 'cp1252', 'ascii']
+ bibfile_enctag = ['strict', 'replace', 'ignore', 'backslashreplace']
+ bib_entry = ['mixed', 'misc', 'book']
+
+ # Needed beacause CLI return str vs int by widget
+ try:
+ bibfile_enc = bibfile_enc[opts.bibfile_enc]
+ bibfile_enctag = bibfile_enctag[opts.bibfile_enctag]
+ bib_entry = bib_entry[opts.bib_entry]
+ except:
+ if opts.bibfile_enc in bibfile_enc :
+ bibfile_enc = opts.bibfile_enc
+ else :
+ log(" WARNING: incorrect --choose-encoding flag, revert to default")
+ bibfile_enc = bibfile_enc[0]
+ if opts.bibfile_enctag in bibfile_enctag :
+ bibfile_enctag = opts.bibfile_enctag
+ else :
+ log(" WARNING: incorrect --choose-encoding-configuration flag, revert to default")
+ bibfile_enctag = bibfile_enctag[0]
+ if opts.bib_entry in bib_entry :
+ bib_entry = opts.bib_entry
+ else :
+ log(" WARNING: incorrect --entry-type flag, revert to default")
+ bib_entry = bib_entry[0]
+
+ if opts.verbose:
+ opts_dict = vars(opts)
+ log("%s(): Generating %s" % (self.name,self.fmt))
+ if opts_dict['search_text']:
+ log(" --search='%s'" % opts_dict['search_text'])
+
+ if opts_dict['ids']:
+ log(" Book count: %d" % len(opts_dict['ids']))
+ if opts_dict['search_text']:
+ log(" (--search ignored when a subset of the database is specified)")
+
+ if opts_dict['fields']:
+ if opts_dict['fields'] == 'all':
+ log(" Fields: %s" % ', '.join(FIELDS[1:]))
+ else:
+ log(" Fields: %s" % opts_dict['fields'])
+
+ log(" Output file will be encoded in %s with %s flag" % (bibfile_enc, bibfile_enctag))
+
+ log(" BibTeX entry type is %s with a citation like '%s' flag" % (bib_entry, opts_dict['bib_cit']))
+
+ # If a list of ids are provided, don't use search_text
+ if opts.ids:
+ opts.search_text = None
+
+ data = self.search_sort_db(db, opts)
+
+ if not len(data):
+ log.error("\nNo matching database entries for search criteria '%s'" % opts.search_text)
+
+ # Get the requested output fields as a list
+ fields = self.get_output_fields(opts)
+
+ if not len(data):
+ log.error("\nNo matching database entries for search criteria '%s'" % opts.search_text)
+
+ #Entries writing after Bibtex formating (or not)
+ if bibfile_enc != 'ascii' :
+ asccii_bibtex = False
+ else :
+ asccii_bibtex = True
+
+ #Check and go to default in case of bad CLI
+ if isinstance(opts.impcit, (StringType, UnicodeType)) :
+ if opts.impcit == 'False' :
+ citation_bibtex= False
+ elif opts.impcit == 'True' :
+ citation_bibtex= True
+ else :
+ log(" WARNING: incorrect --create-citation, revert to default")
+ citation_bibtex= True
+ else :
+ citation_bibtex= opts.impcit
+
+ template_citation = preprocess_template(opts.bib_cit)
+
+ #Open output and write entries
+ outfile = codecs.open(path_to_output, 'w', bibfile_enc, bibfile_enctag)
+
+ #File header
+ nb_entries = len(data)
+
+ #check in book strict if all is ok else throw a warning into log
+ if bib_entry == 'book' :
+ nb_books = len(filter(check_entry_book_valid, data))
+ if nb_books < nb_entries :
+ log(" WARNING: only %d entries in %d are book compatible" % (nb_books, nb_entries))
+ nb_entries = nb_books
+
+ outfile.write(u'%%%Calibre catalog\n%%%{0} entries in catalog\n\n'.format(nb_entries))
+ outfile.write(u'@preamble{"This catalog of %d entries was generated by calibre on %s"}\n\n'
+ % (nb_entries, nowf().strftime("%A, %d. %B %Y %H:%M").decode(preferred_encoding)))
+
+ for entry in data:
+ outfile.write(create_bibtex_entry(entry, fields, bib_entry, template_citation,
+ asccii_bibtex, citation_bibtex))
+
+ outfile.close()
class EPUB_MOBI(CatalogPlugin):
'ePub catalog generator'
diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py
index e039f5a817..5b459c6d2a 100644
--- a/src/calibre/library/custom_columns.py
+++ b/src/calibre/library/custom_columns.py
@@ -12,6 +12,7 @@ from math import floor
from calibre import prints
from calibre.constants import preferred_encoding
+from calibre.library.field_metadata import FieldMetadata
from calibre.utils.date import parse_date
class CustomColumns(object):
@@ -30,6 +31,10 @@ class CustomColumns(object):
def __init__(self):
+ # Verify that CUSTOM_DATA_TYPES is a (possibly improper) subset of
+ # VALID_DATA_TYPES
+ if len(self.CUSTOM_DATA_TYPES - FieldMetadata.VALID_DATA_TYPES) > 0:
+ raise ValueError('Unknown custom column type in set')
# Delete marked custom columns
for record in self.conn.get(
'SELECT id FROM custom_columns WHERE mark_for_delete=1'):
diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py
index 626683fee5..f29b432eec 100644
--- a/src/calibre/library/field_metadata.py
+++ b/src/calibre/library/field_metadata.py
@@ -30,8 +30,8 @@ class FieldMetadata(dict):
label: the actual column label. No prefixing.
- datatype: the type of the information in the field. Valid values are float,
- int, rating, bool, comments, datetime, text.
+ datatype: the type of information in the field. Valid values are listed in
+ VALID_DATA_TYPES below.
is_multiple: valid for the text datatype. If None, the field is to be
treated as a single term. If not None, it contains a string, and the field
is assumed to contain a list of terms separated by that string
@@ -65,6 +65,10 @@ class FieldMetadata(dict):
rec_index: the index of the field in the db metadata record.
'''
+
+ VALID_DATA_TYPES = frozenset([None, 'rating', 'text', 'comments', 'datetime',
+ 'int', 'float', 'bool', 'series'])
+
_field_metadata = [
('authors', {'table':'authors',
'column':'name',
@@ -296,6 +300,8 @@ class FieldMetadata(dict):
self._search_term_map = {}
self.custom_label_to_key_map = {}
for k,v in self._field_metadata:
+ if v['kind'] == 'field' and v['datatype'] not in self.VALID_DATA_TYPES:
+ raise ValueError('Unknown datatype %s for field %s'%(v['datatype'], k))
self._tb_cats[k] = v
self._tb_cats[k]['label'] = k
self._tb_cats[k]['display'] = {}
@@ -377,6 +383,8 @@ class FieldMetadata(dict):
key = self.custom_field_prefix + label
if key in self._tb_cats:
raise ValueError('Duplicate custom field [%s]'%(label))
+ if datatype not in self.VALID_DATA_TYPES:
+ raise ValueError('Unknown datatype %s for field %s'%(datatype, key))
self._tb_cats[key] = {'table':table, 'column':column,
'datatype':datatype, 'is_multiple':is_multiple,
'kind':'field', 'name':name,
diff --git a/src/calibre/translations/calibre.pot b/src/calibre/translations/calibre.pot
index 02db8dde80..16a7eae7ec 100644
--- a/src/calibre/translations/calibre.pot
+++ b/src/calibre/translations/calibre.pot
@@ -5,8 +5,8 @@
msgid ""
msgstr ""
"Project-Id-Version: calibre 0.7.8\n"
-"POT-Creation-Date: 2010-07-09 15:14+MDT\n"
-"PO-Revision-Date: 2010-07-09 15:14+MDT\n"
+"POT-Creation-Date: 2010-07-10 10:05+MDT\n"
+"PO-Revision-Date: 2010-07-10 10:05+MDT\n"
"Last-Translator: Automatically generated\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
@@ -26,7 +26,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/nook/driver.py:71
#: /home/kovid/work/calibre/src/calibre/devices/prs500/books.py:267
#: /home/kovid/work/calibre/src/calibre/devices/prs505/sony_cache.py:492
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:396
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:398
#: /home/kovid/work/calibre/src/calibre/ebooks/chm/input.py:97
#: /home/kovid/work/calibre/src/calibre/ebooks/chm/input.py:100
#: /home/kovid/work/calibre/src/calibre/ebooks/chm/metadata.py:56
@@ -110,10 +110,10 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:110
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:135
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:137
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:898
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:907
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1190
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1193
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:899
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:908
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1191
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1194
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf.py:47
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:120
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:155
@@ -629,30 +629,30 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:64
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:67
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:70
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:130
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:137
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:160
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:132
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:139
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:162
msgid "Getting list of books on device..."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:219
#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:263
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:244
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:262
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:246
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:264
msgid "Removing books from device..."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:267
#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:274
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:269
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:274
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:271
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:276
msgid "Removing books from device metadata listing..."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:279
#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:313
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:208
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:238
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:210
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:240
msgid "Adding books to device metadata listing..."
msgstr ""
@@ -813,12 +813,12 @@ msgstr ""
msgid "Get device information..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:188
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:190
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:192
msgid "Transferring books to device..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:305
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:307
msgid "Sending metadata to device..."
msgstr ""
@@ -1301,7 +1301,7 @@ msgid "Workaround epubcheck bugs"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/epub/fix/epubcheck.py:22
-msgid "Workarounds for bugs in the latest release of epubcheck. epubcheck reports many things as errors that are not actually errors. %prog will try to detect these and replace them with constructs that epubcheck likes. This may cause significant changes to your epub, complain to the epubcheck project."
+msgid "Workarounds for bugs in the latest release of epubcheck. epubcheck reports many things as errors that are not actually errors. epub-fix will try to detect these and replace them with constructs that epubcheck likes. This may cause significant changes to your epub, complain to the epubcheck project."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/epub/fix/main.py:19
@@ -1322,7 +1322,7 @@ msgid "Fix unmanifested files"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/epub/fix/unmanifested.py:21
-msgid "Fix unmanifested files. %prog can either add them to the manifest or delete them as specified by the delete unmanifested option."
+msgid "Fix unmanifested files. epub-fix can either add them to the manifest or delete them as specified by the delete unmanifested option."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/epub/fix/unmanifested.py:32
@@ -2580,14 +2580,14 @@ msgid "Main memory"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions.py:519
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:444
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:453
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:445
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:454
msgid "Storage Card A"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions.py:520
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:446
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:455
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:447
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:456
msgid "Storage Card B"
msgstr ""
@@ -2645,7 +2645,7 @@ msgid "Failed to download metadata:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions.py:659
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:637
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:638
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:523
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:951
#: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:53
@@ -2797,7 +2797,7 @@ msgid "The specified directory could not be processed."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/add.py:263
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:840
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:841
msgid "No books"
msgstr ""
@@ -2942,12 +2942,12 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:267
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:269
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:270
-#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:248
-#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:253
-#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:259
-#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:261
-#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:263
-#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:265
+#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:132
+#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:137
+#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:143
+#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:145
+#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:147
+#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:149
#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts_ui.py:75
#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts_ui.py:80
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:186
@@ -3012,7 +3012,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts.py:47
#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts_ui.py:73
#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts_ui.py:78
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:467
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:274
msgid "None"
msgstr ""
@@ -3836,12 +3836,12 @@ msgid "Footer regular expression:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:56
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:77
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:76
msgid "Invalid regular expression"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:57
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:78
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:77
msgid "Invalid regular expression: %s"
msgstr ""
@@ -4109,250 +4109,250 @@ msgstr ""
msgid "tags to remove"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:48
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:49
#: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:135
msgid "No details available."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:154
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:155
msgid "Device no longer connected."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:270
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:271
msgid "Get device information"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:281
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:282
msgid "Get list of books on device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:291
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:292
msgid "Get annotations from device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:300
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:301
msgid "Send metadata to device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:305
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:306
msgid "Send collections to device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:329
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:330
msgid "Upload %d books to device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:344
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:345
msgid "Delete books from device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:361
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:362
msgid "Download books from device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:371
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:372
msgid "View book on device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:407
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:408
msgid "Set default send to device action"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:413
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:420
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:422
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:424
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:414
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:421
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:423
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:425
msgid "Email to"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:424
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:425
msgid " and delete from library"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:433
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:434
msgid "Send to main memory"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:435
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:436
msgid "Send to storage card A"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:437
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:438
msgid "Send to storage card B"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:442
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:451
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:443
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:452
msgid "Main Memory"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:469
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:470
msgid "Send and delete from library"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:470
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:471
msgid "Send specific format"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:509
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:510
msgid "Connect to folder"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:515
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:516
msgid "Connect to iTunes"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:520
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:521
msgid "Eject device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:528
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:529
msgid "Fetch annotations (experimental)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:638
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:639
msgid "Error communicating with device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:659
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:660
msgid "Select folder to open as device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:704
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:705
msgid "Failed"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:710
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:711
msgid "Error talking to device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:711
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:712
msgid "There was a temporary error talking to the device. Please unplug and reconnect the device and or reboot."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:753
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:754
msgid "Device: "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:755
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:756
msgid " detected."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:841
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:842
msgid "selected to send"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:846
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:847
msgid "Choose format to send to device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:855
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:856
msgid "No device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:856
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:857
msgid "Cannot send: No device is connected"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:859
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:863
-msgid "No card"
-msgstr ""
-
#: /home/kovid/work/calibre/src/calibre/gui2/device.py:860
#: /home/kovid/work/calibre/src/calibre/gui2/device.py:864
+msgid "No card"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:861
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:865
msgid "Cannot send: Device has no storage card"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:905
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:906
msgid "E-book:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:908
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:909
msgid "Attached, you will find the e-book"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:909
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:910
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:181
msgid "by"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:910
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:911
msgid "in the %s format."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:923
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:924
msgid "Sending email to"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:953
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:961
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1053
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1115
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1234
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1242
-msgid "No suitable formats"
-msgstr ""
-
#: /home/kovid/work/calibre/src/calibre/gui2/device.py:954
-msgid "Auto convert the following books before sending via email?"
-msgstr ""
-
#: /home/kovid/work/calibre/src/calibre/gui2/device.py:962
-msgid "Could not email the following books as no suitable formats were found:"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:980
-msgid "Failed to email books"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:981
-msgid "Failed to email the following books:"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:985
-msgid "Sent by email:"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1012
-msgid "News:"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1013
-msgid "Attached is the"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1024
-msgid "Sent news to"
-msgstr ""
-
#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1054
#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1116
#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1235
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1243
+msgid "No suitable formats"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:955
+msgid "Auto convert the following books before sending via email?"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:963
+msgid "Could not email the following books as no suitable formats were found:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:981
+msgid "Failed to email books"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:982
+msgid "Failed to email the following books:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:986
+msgid "Sent by email:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1013
+msgid "News:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1014
+msgid "Attached is the"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1025
+msgid "Sent news to"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1055
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1117
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1236
msgid "Auto convert the following books before uploading to the device?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1084
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1085
msgid "Sending catalogs to device."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1148
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1149
msgid "Sending news to device."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1201
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1202
msgid "Sending books to device."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1243
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1244
msgid "Could not upload the following books to the device, as no suitable formats were found. Convert the book(s) to a format supported by your device first."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1304
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1305
msgid "No space on device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1305
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1306
msgid "
Cannot upload books to device there is no more free space available "
msgstr ""
@@ -4613,7 +4613,7 @@ msgid "Access log:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/__init__.py:794
-#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:332
+#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:331
msgid "Failed to start content server"
msgstr ""
@@ -4671,27 +4671,27 @@ msgstr ""
msgid "The following books had formats listed in the database that are not actually available. The entries for the formats have been removed. You should check them manually. This can happen if you manipulate the files in the library folder directly."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:113
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:133
msgid "TabWidget"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:114
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:134
msgid "Here you can control how calibre will read metadata from the files you add to it. calibre can either read metadata from the contents of the file, or from the filename."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:115
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:135
msgid "Read metadata only from &file name"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:116
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:136
msgid "Swap the firstname and lastname of the author. This affects only metadata read from file names."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:117
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:137
msgid "&Swap author firstname and lastname"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:118
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:138
msgid ""
"If an existing book with a similar title and author is found that does not have the format being added, the format is added\n"
"to the existing book, instead of creating a new entry. If the existing book already has the format, then it is silently ignored.\n"
@@ -4699,81 +4699,93 @@ msgid ""
"Title match ignores leading indefinite articles (\"the\", \"a\", \"an\"), punctuation, case, etc. Author match is exact."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:122
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:142
msgid "If books with similar titles and authors found, &merge the new files automatically"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:123
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:143
msgid "&Configure metadata from file name"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:124
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:144
msgid "&Adding books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:125
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:145
msgid "Here you can control how calibre will save your books when you click the Save to Disk button:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:126
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:146
msgid "Save &cover separately"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:127
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:147
msgid "Update &metadata in saved copies"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:128
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:148
msgid "Save metadata in &OPF file"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:129
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:149
msgid "Convert non-English characters to &English equivalents"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:130
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:150
msgid "Format &dates as:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:131
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:151
msgid "File &formats to save:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:132
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:152
msgid "Replace space with &underscores"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:133
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:153
msgid "Change paths to &lowercase"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:134
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:154
msgid "&Saving books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:135
-msgid "Preserve device collections."
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:155
+msgid "Metadata &management:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:136
-msgid "If checked, collections will not be deleted even if a book with changed metadata is resent and the collection is not in the book's metadata. In addition, editing collections in the device view will be enabled. If unchecked, collections will be always reflect only the metadata in the calibre library."
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:156
+msgid "Manual management"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:137
-msgid " "
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:157
+msgid "Only on send"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:138
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:158
+msgid "Automatic management"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:159
+msgid ""
+"
Manual Management: Calibre updates the metadata and adds collections only when a book is sent. With this option, calibre will never remove a collection.
\n"
+"
Only on send: Calibre updates metadata and adds/removes collections for a book only when it is sent to the device.
\n"
+"
Automatic management: Calibre automatically keeps metadata on the device in sync with the calibre library, on every connect
"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:162
msgid "Here you can control how calibre will save your books when you click the Send to Device button. This setting can be overriden for individual devices by customizing the device interface plugins in Preferences->Plugins"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:139
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/add_save_ui.py:163
msgid "Sending to &device"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config/config_ui.py:554
#: /home/kovid/work/calibre/src/calibre/gui2/init.py:168
-#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:290
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:425
+#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:174
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:201
msgid "Preferences"
msgstr ""
@@ -6410,11 +6422,11 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/filename_pattern_ui.py:126
#: /home/kovid/work/calibre/src/calibre/gui2/filename_pattern_ui.py:129
#: /home/kovid/work/calibre/src/calibre/gui2/filename_pattern_ui.py:132
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:84
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:88
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:93
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:98
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:100
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:83
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:87
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:92
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:97
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:99
msgid "No match"
msgstr ""
@@ -6503,7 +6515,8 @@ msgid "Add Empty book. (Book entry with no formats)"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/init.py:103
-#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:276
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:409
+#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:160
msgid "Save to disk"
msgstr ""
@@ -6512,17 +6525,18 @@ msgid "Save to disk in a single directory"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/init.py:107
-#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:394
+#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:393
msgid "Save only %s format to disk"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/init.py:111
-#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:397
+#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:396
msgid "Save only %s format to disk in a single directory"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/init.py:120
-#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:282
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:412
+#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:166
msgid "View"
msgstr ""
@@ -6566,46 +6580,46 @@ msgstr ""
msgid "Run welcome wizard"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/init.py:205
+#: /home/kovid/work/calibre/src/calibre/gui2/init.py:191
msgid "Similar books..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/init.py:237
+#: /home/kovid/work/calibre/src/calibre/gui2/init.py:223
msgid "Add books to library"
msgstr ""
+#: /home/kovid/work/calibre/src/calibre/gui2/init.py:225
+#: /home/kovid/work/calibre/src/calibre/gui2/init.py:232
#: /home/kovid/work/calibre/src/calibre/gui2/init.py:239
-#: /home/kovid/work/calibre/src/calibre/gui2/init.py:246
-#: /home/kovid/work/calibre/src/calibre/gui2/init.py:253
msgid "Manage collections"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/init.py:333
+#: /home/kovid/work/calibre/src/calibre/gui2/init.py:319
msgid "Cover Browser"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/init.py:351
+#: /home/kovid/work/calibre/src/calibre/gui2/init.py:337
msgid "Tag Browser"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/init.py:372
+#: /home/kovid/work/calibre/src/calibre/gui2/init.py:358
msgid "version"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/init.py:373
+#: /home/kovid/work/calibre/src/calibre/gui2/init.py:359
msgid "created by Kovid Goyal"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/init.py:391
+#: /home/kovid/work/calibre/src/calibre/gui2/init.py:377
msgid "Connected "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/init.py:400
+#: /home/kovid/work/calibre/src/calibre/gui2/init.py:386
msgid "Update found"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/init.py:445
-#: /home/kovid/work/calibre/src/calibre/gui2/init.py:454
+#: /home/kovid/work/calibre/src/calibre/gui2/init.py:430
+#: /home/kovid/work/calibre/src/calibre/gui2/init.py:439
msgid "Book Details"
msgstr ""
@@ -6668,6 +6682,215 @@ msgstr ""
msgid " - Jobs"
msgstr ""
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:64
+msgid ""
+"Library\n"
+"%d books"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:65
+msgid ""
+"Reader\n"
+"%s"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:66
+msgid ""
+"Card A\n"
+"%s"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:67
+msgid ""
+"Card B\n"
+"%s"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:71
+msgid "Click to see the books available on your computer"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:74
+msgid "Click to see the books in the main memory of your reader"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:75
+msgid "Click to see the books on storage card A in your reader"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:76
+msgid "Click to see the books on storage card B in your reader"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:84
+msgid "Books located at"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:100
+msgid "free"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:287
+#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:134
+msgid "Books display will be restricted to those matching the selected saved search"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:300
+#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:136
+msgid "Advanced search"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:309
+#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:140
+#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:141
+msgid "
Search the list of books by title, author, publisher, tags, comments, etc.