diff --git a/resources/images/news/latimes.png b/resources/images/news/latimes.png
new file mode 100644
index 0000000000..62bb4d0b8a
Binary files /dev/null and b/resources/images/news/latimes.png differ
diff --git a/resources/mime.types b/resources/mime.types
index ab98b3bf4a..a2a67c38f9 100644
--- a/resources/mime.types
+++ b/resources/mime.types
@@ -585,7 +585,6 @@ application/vnd.osa.netdeploy
application/vnd.osgi.bundle
application/vnd.osgi.dp dp
application/vnd.otps.ct-kip+xml
-application/vnd.palm oprc pdb pqa
application/vnd.paos.xml
application/vnd.pg.format str
application/vnd.pg.osasli ei6
@@ -1082,7 +1081,6 @@ chemical/x-ncbi-asn1 asn
chemical/x-ncbi-asn1-ascii ent prt
chemical/x-ncbi-asn1-binary aso val
chemical/x-ncbi-asn1-spec asn
-chemical/x-pdb ent pdb
chemical/x-rosdal ros
chemical/x-swissprot sw
chemical/x-vamas-iso14976 vms
@@ -1379,3 +1377,5 @@ application/x-cbr cbr
application/x-cb7 cb7
application/x-koboreader-ebook kobo
image/wmf wmf
+application/ereader pdb
+
diff --git a/resources/recipes/20_minutos.recipe b/resources/recipes/20_minutos.recipe
index cb3002a76c..106c0dcffa 100644
--- a/resources/recipes/20_minutos.recipe
+++ b/resources/recipes/20_minutos.recipe
@@ -1,25 +1,25 @@
-# -*- coding: utf-8
__license__ = 'GPL v3'
__author__ = 'Luis Hernandez'
__copyright__ = 'Luis Hernandez'
-description = 'Periódico gratuito en español - v0.8 - 27 Jan 2011'
+__version__ = 'v0.85'
+__date__ = '31 January 2011'
'''
www.20minutos.es
'''
-
+import re
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1294946868(BasicNewsRecipe):
- title = u'20 Minutos'
+ title = u'20 Minutos new'
publisher = u'Grupo 20 Minutos'
- __author__ = 'Luis Hernández'
- description = 'Periódico gratuito en español'
+ __author__ = 'Luis Hernandez'
+ description = 'Free spanish newspaper'
cover_url = 'http://estaticos.20minutos.es/mmedia/especiales/corporativo/css/img/logotipos_grupo20minutos.gif'
- oldest_article = 5
+ oldest_article = 2
max_articles_per_feed = 100
remove_javascript = True
@@ -29,6 +29,7 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
encoding = 'ISO-8859-1'
language = 'es'
timefmt = '[%a, %d %b, %Y]'
+ remove_empty_feeds = True
keep_only_tags = [
dict(name='div', attrs={'id':['content','vinetas',]})
@@ -43,13 +44,21 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
remove_tags = [
dict(name='ol', attrs={'class':['navigation',]})
,dict(name='span', attrs={'class':['action']})
- ,dict(name='div', attrs={'class':['twitter comments-list hidden','related-news','col','photo-gallery','calendario','article-comment','postto estirar','otras_vinetas estirar','kment','user-actions']})
+ ,dict(name='div', attrs={'class':['twitter comments-list hidden','related-news','col','photo-gallery','photo-gallery side-art-block','calendario','article-comment','postto estirar','otras_vinetas estirar','kment','user-actions']})
,dict(name='div', attrs={'id':['twitter-destacados','eco-tabs','inner','vineta_calendario','vinetistas clearfix','otras_vinetas estirar','MIN1','main','SUP1','INT']})
,dict(name='ul', attrs={'class':['article-user-actions','stripped-list']})
,dict(name='ul', attrs={'id':['site-links']})
,dict(name='li', attrs={'class':['puntuacion','enviar','compartir']})
]
+ extra_css = """
+ p{text-align: justify; font-size: 100%}
+ body{ text-align: left; font-size:100% }
+ h3{font-family: sans-serif; font-size:150%; font-weight:bold; text-align: justify; }
+ """
+
+ preprocess_regexps = [(re.compile(r'', re.DOTALL), lambda m: '')]
+
feeds = [
(u'Portada' , u'http://www.20minutos.es/rss/')
,(u'Nacional' , u'http://www.20minutos.es/rss/nacional/')
@@ -65,6 +74,6 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
,(u'Empleo' , u'http://www.20minutos.es/rss/empleo/')
,(u'Cine' , u'http://www.20minutos.es/rss/cine/')
,(u'Musica' , u'http://www.20minutos.es/rss/musica/')
- ,(u'Vinetas' , u'http://www.20minutos.es/rss/vinetas/')
+ ,(u'Vinetas' , u'http://www.20minutos.es/rss/vinetas/')
,(u'Comunidad20' , u'http://www.20minutos.es/rss/zona20/')
]
diff --git a/resources/recipes/cinco_dias.recipe b/resources/recipes/cinco_dias.recipe
new file mode 100644
index 0000000000..40241aff5c
--- /dev/null
+++ b/resources/recipes/cinco_dias.recipe
@@ -0,0 +1,71 @@
+__license__ = 'GPL v3'
+__author__ = 'Luis Hernandez'
+__copyright__ = 'Luis Hernandez'
+__version__ = 'v1.2'
+__date__ = '31 January 2011'
+
+'''
+http://www.cincodias.com/
+'''
+
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class AdvancedUserRecipe1294946868(BasicNewsRecipe):
+
+ title = u'Cinco Dias'
+ publisher = u'Grupo Prisa'
+
+ __author__ = 'Luis Hernandez'
+ description = 'spanish web about money and bussiness, free edition'
+
+ cover_url = 'http://www.prisa.com/images/logos/logo_cinco_dias.gif'
+ oldest_article = 2
+ max_articles_per_feed = 100
+
+ remove_javascript = True
+ no_stylesheets = True
+ use_embedded_content = False
+
+ language = 'es'
+ remove_empty_feeds = True
+ encoding = 'ISO-8859-1'
+ timefmt = '[%a, %d %b, %Y]'
+
+ keep_only_tags = [
+ dict(name='div', attrs={'class':['cab_articulo cab_noticia','pos_3','txt_noticia','mod_despiece']})
+ ,dict(name='p', attrs={'class':['cintillo']})
+ ]
+
+ remove_tags_before = dict(name='div' , attrs={'class':['publi_h']})
+ remove_tags_after = dict(name='div' , attrs={'class':['tab_util util_estadisticas']})
+
+ remove_tags = [
+ dict(name='div', attrs={'class':['util-1','util-2','util-3','inner estirar','inner1','inner2','inner3','cont','tab_util util_estadisticas','tab_util util_enviar','mod_list_inf','mod_similares','mod_divisas','mod_sectores','mod_termometro','mod post','mod_img','mod_txt','nivel estirar','barra estirar','info_brujula btnBrujula','utilidad_brujula estirar']})
+ ,dict(name='li', attrs={'class':['lnk-fcbook','lnk-retweet','lnk-meneame','desplegable','comentarios','list-options','estirar']})
+ ,dict(name='ul', attrs={'class':['lista-izquierda','list-options','estirar']})
+ ,dict(name='p', attrs={'class':['autor']})
+ ]
+
+ extra_css = """
+ p{text-align: justify; font-size: 100%}
+ body{ text-align: left; font-size:100% }
+ h1{font-family: sans-serif; font-size:150%; font-weight:bold; text-align: justify; }
+ h3{font-family: sans-serif; font-size:100%; font-style: italic; text-align: justify; }
+ """
+
+ feeds = [
+ (u'Ultima Hora' , u'http://www.cincodias.com/rss/feed.html?feedId=17029')
+ ,(u'Empresas' , u'http://www.cincodias.com/rss/feed.html?feedId=19')
+ ,(u'Mercados' , u'http://www.cincodias.com/rss/feed.html?feedId=20')
+ ,(u'Economia' , u'http://www.cincodias.com/rss/feed.html?feedId=21')
+ ,(u'Tecnorama' , u'http://www.cincodias.com/rss/feed.html?feedId=17230')
+ ,(u'Tecnologia' , u'http://www.cincodias.com/rss/feed.html?feedId=17106')
+ ,(u'Finanzas Personales' , u'http://www.cincodias.com/rss/feed.html?feedId=22')
+ ,(u'Fiscalidad' , u'http://www.cincodias.com/rss/feed.html?feedId=17107')
+ ,(u'Vivienda' , u'http://www.cincodias.com/rss/feed.html?feedId=17108')
+ ,(u'Tendencias' , u'http://www.cincodias.com/rss/feed.html?feedId=17109')
+ ,(u'Empleo' , u'http://www.cincodias.com/rss/feed.html?feedId=17110')
+ ,(u'IBEX 35' , u'http://www.cincodias.com/rss/feed.html?feedId=17125')
+ ,(u'Sectores' , u'http://www.cincodias.com/rss/feed.html?feedId=17126')
+ ,(u'Opinion' , u'http://www.cincodias.com/rss/feed.html?feedId=17105')
+ ]
diff --git a/resources/recipes/le_temps.recipe b/resources/recipes/le_temps.recipe
index c33d9a51d2..7e320fe710 100644
--- a/resources/recipes/le_temps.recipe
+++ b/resources/recipes/le_temps.recipe
@@ -15,12 +15,26 @@ class LeTemps(BasicNewsRecipe):
oldest_article = 7
max_articles_per_feed = 100
__author__ = 'Sujata Raman'
+ description = 'French news. Needs a subscription from http://www.letemps.ch'
no_stylesheets = True
remove_javascript = True
recursions = 1
encoding = 'UTF-8'
match_regexps = [r'http://www.letemps.ch/Page/Uuid/[-0-9a-f]+\|[1-9]']
language = 'fr'
+ needs_subscription = True
+
+ def get_browser(self):
+ br = BasicNewsRecipe.get_browser(self)
+ br.open('http://www.letemps.ch/login')
+ br['username'] = self.username
+ br['password'] = self.password
+ raw = br.submit().read()
+ if '>Login' in raw:
+ raise ValueError('Failed to login to letemp.ch. Check '
+ 'your username and password')
+ return br
+
keep_only_tags = [dict(name='div', attrs={'id':'content'}),
dict(name='div', attrs={'class':'story'})
diff --git a/resources/recipes/wsj.recipe b/resources/recipes/wsj.recipe
index 4ce315200c..eb473f1121 100644
--- a/resources/recipes/wsj.recipe
+++ b/resources/recipes/wsj.recipe
@@ -35,7 +35,7 @@ class WallStreetJournal(BasicNewsRecipe):
remove_tags_before = dict(name='h1')
remove_tags = [
- dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow"]),
+ dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow","articleTabs_tab_quotes","articleTabs_tab_document"]),
{'class':['footer_columns','network','insetCol3wide','interactive','video','slideshow','map','insettip','insetClose','more_in', "insetContent", 'articleTools_bottom', 'aTools', "tooltip", "adSummary", "nav-inline"]},
dict(rel='shortcut icon'),
]
@@ -101,7 +101,7 @@ class WallStreetJournal(BasicNewsRecipe):
title = 'Front Section'
url = 'http://online.wsj.com' + a['href']
feeds = self.wsj_add_feed(feeds,title,url)
- title = 'What''s News'
+ title = "What's News"
url = url.replace('pageone','whatsnews')
feeds = self.wsj_add_feed(feeds,title,url)
else:
diff --git a/resources/recipes/wsj_free.recipe b/resources/recipes/wsj_free.recipe
index df8234e8e2..a4a957fc90 100644
--- a/resources/recipes/wsj_free.recipe
+++ b/resources/recipes/wsj_free.recipe
@@ -10,7 +10,10 @@ class WallStreetJournal(BasicNewsRecipe):
title = 'Wall Street Journal (free)'
__author__ = 'Kovid Goyal, Sujata Raman, Joshua Oster-Morris, Starson17'
- description = 'News and current affairs'
+ description = '''News and current affairs. This recipe only fetches complete
+ versions of the articles that are available free on the wsj.com website.
+ To get the rest of the articles, subscribe to the WSJ and use the other WSJ
+ recipe.'''
language = 'en'
cover_url = 'http://dealbreaker.com/images/thumbs/Wall%20Street%20Journal%20A1.JPG'
max_articles_per_feed = 1000
@@ -151,6 +154,4 @@ class WallStreetJournal(BasicNewsRecipe):
return articles
- def cleanup(self):
- self.browser.open('http://online.wsj.com/logout?url=http://online.wsj.com')
diff --git a/src/calibre/debug.py b/src/calibre/debug.py
index e1c3e1809e..3a080fc57b 100644
--- a/src/calibre/debug.py
+++ b/src/calibre/debug.py
@@ -22,13 +22,15 @@ Run an embedded python interpreter.
parser.add_option('-d', '--debug-device-driver', default=False, action='store_true',
help='Debug the specified device driver.')
parser.add_option('-g', '--gui', default=False, action='store_true',
- help='Run the GUI',)
+ help='Run the GUI with debugging enabled. Debug output is '
+ 'printed to stdout and stderr.')
parser.add_option('--gui-debug', default=None,
help='Run the GUI with a debug console, logging to the'
- ' specified path',)
+ ' specified path. For internal use only, use the -g'
+ ' option to run the GUI in debug mode',)
parser.add_option('--show-gui-debug', default=None,
- help='Display the specified log file.',)
-
+ help='Display the specified log file. For internal use'
+ ' only.',)
parser.add_option('-w', '--viewer', default=False, action='store_true',
help='Run the ebook viewer',)
parser.add_option('--paths', default=False, action='store_true',
diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py
index 2a92f46e8d..bc442f5853 100644
--- a/src/calibre/devices/interface.py
+++ b/src/calibre/devices/interface.py
@@ -35,6 +35,16 @@ class DevicePlugin(Plugin):
#: Height for thumbnails on the device
THUMBNAIL_HEIGHT = 68
+ #: Width for thumbnails on the device. Setting this will force thumbnails
+ #: to this size, not preserving aspect ratio. If it is not set, then
+ #: the aspect ratio will be preserved and the thumbnail will be no higher
+ #: than THUMBNAIL_HEIGHT
+ # THUMBNAIL_WIDTH = 68
+
+ #: Set this to True if the device supports updating cover thumbnails during
+ #: sync_booklists. Setting it to true will ask device.py to refresh the
+ #: cover thumbnails during book matching
+ WANTS_UPDATED_THUMBNAILS = False
#: Whether the metadata on books can be set via the GUI.
CAN_SET_METADATA = ['title', 'authors', 'collections']
diff --git a/src/calibre/devices/prs505/__init__.py b/src/calibre/devices/prs505/__init__.py
index 48b7d98123..1a59cb81a6 100644
--- a/src/calibre/devices/prs505/__init__.py
+++ b/src/calibre/devices/prs505/__init__.py
@@ -8,5 +8,5 @@ CACHE_XML = 'Sony Reader/database/cache.xml'
CACHE_EXT = 'Sony Reader/database/cacheExt.xml'
MEDIA_THUMBNAIL = 'database/thumbnail'
-CACHE_THUMBNAIL = 'Sony Reader/database/thumbnail'
+CACHE_THUMBNAIL = 'Sony Reader/thumbnail'
diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py
index 0f6668891a..3768b8be62 100644
--- a/src/calibre/devices/prs505/driver.py
+++ b/src/calibre/devices/prs505/driver.py
@@ -81,12 +81,19 @@ class PRS505(USBMS):
_('Set this option to have separate book covers uploaded '
'every time you connect your device. Unset this option if '
'you have so many books on the reader that performance is '
- 'unacceptable.')
+ 'unacceptable.'),
+ _('Preserve cover aspect ratio when building thumbnails') +
+ ':::' +
+ _('Set this option if you want the cover thumbnails to have '
+ 'the same aspect ratio (width to height) as the cover. '
+ 'Unset it if you want the thumbnail to be the maximum size, '
+ 'ignoring aspect ratio.')
]
EXTRA_CUSTOMIZATION_DEFAULT = [
', '.join(['series', 'tags']),
False,
- False
+ False,
+ True
]
OPT_COLLECTIONS = 0
@@ -96,7 +103,7 @@ class PRS505(USBMS):
plugboard = None
plugboard_func = None
- THUMBNAIL_HEIGHT = 200
+ THUMBNAIL_HEIGHT = 217
MAX_PATH_LEN = 201 # 250 - (max(len(CACHE_THUMBNAIL), len(MEDIA_THUMBNAIL)) +
# len('main_thumbnail.jpg') + 1)
@@ -138,6 +145,13 @@ class PRS505(USBMS):
if not write_cache(self._card_b_prefix):
self._card_b_prefix = None
self.booklist_class.rebuild_collections = self.rebuild_collections
+ # Set the thumbnail width to the theoretical max if the user has asked
+ # that we do not preserve aspect ratio
+ if not self.settings().extra_customization[3]:
+ self.THUMBNAIL_WIDTH = 168
+ # Set WANTS_UPDATED_THUMBNAILS if the user has asked that thumbnails be
+ # updated on every connect
+ self.WANTS_UPDATED_THUMBNAILS = self.settings().extra_customization[2]
def get_device_information(self, end_session=True):
return (self.gui_name, '', '', '')
diff --git a/src/calibre/ebooks/chm/reader.py b/src/calibre/ebooks/chm/reader.py
index 025e252005..04ce6d5efe 100644
--- a/src/calibre/ebooks/chm/reader.py
+++ b/src/calibre/ebooks/chm/reader.py
@@ -139,6 +139,13 @@ class CHMReader(CHMFile):
if self.hhc_path not in files and files:
self.hhc_path = files[0]
+ if self.hhc_path == '.hhc' and self.hhc_path not in files:
+ from calibre import walk
+ for x in walk(output_dir):
+ if os.path.basename(x).lower() in ('index.htm', 'index.html'):
+ self.hhc_path = os.path.relpath(x, output_dir)
+ break
+
def _reformat(self, data, htmlpath):
try:
data = xml_to_unicode(data, strip_encoding_pats=True)[0]
diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py
index 33ae61f16a..975507e2a7 100644
--- a/src/calibre/ebooks/conversion/cli.py
+++ b/src/calibre/ebooks/conversion/cli.py
@@ -46,7 +46,8 @@ HEURISTIC_OPTIONS = ['markup_chapter_headings',
'italicize_common_cases', 'fix_indents',
'html_unwrap_factor', 'unwrap_lines',
'delete_blank_paragraphs', 'format_scene_breaks',
- 'dehyphenate', 'renumber_headings']
+ 'dehyphenate', 'renumber_headings',
+ 'replace_scene_breaks']
def print_help(parser, log):
help = parser.format_help().encode(preferred_encoding, 'replace')
diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py
index 5807ba5f8f..70b6ca657e 100644
--- a/src/calibre/ebooks/conversion/plumber.py
+++ b/src/calibre/ebooks/conversion/plumber.py
@@ -531,6 +531,11 @@ OptionRecommendation(name='format_scene_breaks',
'Replace soft scene breaks that use multiple blank lines with'
'horizontal rules.')),
+OptionRecommendation(name='replace_scene_breaks',
+ recommended_value='', level=OptionRecommendation.LOW,
+ help=_('Replace scene breaks with the specified text. By default, the '
+ 'text from the input document is used.')),
+
OptionRecommendation(name='dehyphenate',
recommended_value=True, level=OptionRecommendation.LOW,
help=_('Analyze hyphenated words throughout the document. The '
diff --git a/src/calibre/ebooks/conversion/utils.py b/src/calibre/ebooks/conversion/utils.py
index d4af7797ae..edd4d54cba 100644
--- a/src/calibre/ebooks/conversion/utils.py
+++ b/src/calibre/ebooks/conversion/utils.py
@@ -26,9 +26,14 @@ class HeuristicProcessor(object):
self.blanks_deleted = False
self.blanks_between_paragraphs = False
self.linereg = re.compile('(?<=)', re.IGNORECASE|re.DOTALL)
- self.blankreg = re.compile(r'\s*(?P]*>)\s*(?P
)', re.IGNORECASE)
+ self.blankreg = re.compile(r'\s*(?P]*>)\s*(?P
)', re.IGNORECASE)
self.anyblank = re.compile(r'\s*(?P]*>)\s*(?P
)', re.IGNORECASE)
self.multi_blank = re.compile(r'(\s*]*>\s*
){2,}(?!\s*]*>\s*
){2,}', re.IGNORECASE)
+ self.line_open = "<(?Pp|div)[^>]*>\s*(<(?Pfont|span|[ibu])[^>]*>)?\s*(<(?Pfont|span|[ibu])[^>]*>)?\s*(<(?Pfont|span|[ibu])[^>]*>)?\s*"
+ self.line_close = "((?P=inner3)>)?\s*((?P=inner2)>)?\s*((?P=inner1)>)?\s*(?P=outer)>"
+ self.single_blank = re.compile(r'(\s*]*>\s*
)', re.IGNORECASE)
+ self.scene_break_open = ''
def is_pdftohtml(self, src):
return '' in src[:1000]
@@ -187,19 +192,17 @@ class HeuristicProcessor(object):
# Build the Regular Expressions in pieces
init_lookahead = "(?=<(p|div))"
- chapter_line_open = "<(?Pp|div)[^>]*>\s*(<(?Pfont|span|[ibu])[^>]*>)?\s*(<(?Pfont|span|[ibu])[^>]*>)?\s*(<(?Pfont|span|[ibu])[^>]*>)?\s*"
+ chapter_line_open = self.line_open
title_line_open = "<(?Pp|div)[^>]*>\s*(<(?Pfont|span|[ibu])[^>]*>)?\s*(<(?Pfont|span|[ibu])[^>]*>)?\s*(<(?Pfont|span|[ibu])[^>]*>)?\s*"
chapter_header_open = r"(?P"
title_header_open = r"(?P"
chapter_header_close = ")\s*"
title_header_close = ")"
- chapter_line_close = "((?P=inner3)>)?\s*((?P=inner2)>)?\s*((?P=inner1)>)?\s*(?P=outer)>"
+ chapter_line_close = self.line_close
title_line_close = "((?P=inner6)>)?\s*((?P=inner5)>)?\s*((?P=inner4)>)?\s*(?P=outer2)>"
is_pdftohtml = self.is_pdftohtml(html)
if is_pdftohtml:
- chapter_line_open = "<(?Pp)[^>]*>(\s*<[ibu][^>]*>)?\s*"
- chapter_line_close = "\s*([ibu][^>]*>\s*)?(?P=outer)>"
title_line_open = "<(?Pp)[^>]*>\s*"
title_line_close = "\s*(?P=outer2)>"
@@ -374,13 +377,15 @@ class HeuristicProcessor(object):
html = re.sub(ur'\s*\s*', ' ', html)
# Delete microsoft 'smart' tags
html = re.sub('(?i)?st1:\w+>', '', html)
- # Delete self closing paragraph tags
- html = re.sub('', '', html)
+ # Re-open self closing paragraph tags
+ html = re.sub('/]*/>', '
', html)
# Get rid of empty span, bold, font, em, & italics tags
html = re.sub(r"\s*]*>\s*(]*>\s*){0,2}\s*\s*", " ", html)
html = re.sub(r"\s*<(font|[ibu]|em|strong)[^>]*>\s*(<(font|[ibu]|em|strong)[^>]*>\s*(font|[ibu]|em|strong)>\s*){0,2}\s*(font|[ibu]|em|strong)>", " ", html)
html = re.sub(r"\s*]*>\s*(]>\s*){0,2}\s*\s*", " ", html)
html = re.sub(r"\s*<(font|[ibu]|em|strong)[^>]*>\s*(<(font|[ibu]|em|strong)[^>]*>\s*(font|[ibu]|em|strong)>\s*){0,2}\s*(font|[ibu]|em|strong)>", " ", html)
+ # Empty heading tags
+ html = re.sub(r'(?i)\s*', '', html)
self.deleted_nbsps = True
return html
@@ -419,32 +424,99 @@ class HeuristicProcessor(object):
return True
return False
- def detect_blank_formatting(self, html):
- blanks_before_headings = re.compile(r'(\s*]*>\s*
){1,}(?=\s*)(\s*]*>\s*
){1,}', re.IGNORECASE)
-
- def markup_spacers(match):
- blanks = match.group(0)
- blanks = self.blankreg.sub('\n
', blanks)
- return blanks
- html = blanks_before_headings.sub(markup_spacers, html)
- html = blanks_after_headings.sub(markup_spacers, html)
+ def merge_blanks(self, html, blanks_count=None):
+ base_em = .5 # Baseline is 1.5em per blank line, 1st line is .5 em css and 1em for the nbsp
+ em_per_line = 1.5 # Add another 1.5 em for each additional blank
+
+ def merge_matches(match):
+ to_merge = match.group(0)
+ lines = float(len(self.single_blank.findall(to_merge))) - 1.
+ em = base_em + (em_per_line * lines)
+ if to_merge.find('whitespace'):
+ newline = self.any_multi_blank.sub('\n
', match.group(0))
+ else:
+ newline = self.any_multi_blank.sub('\n
', match.group(0))
+ return newline
+
+ html = self.any_multi_blank.sub(merge_matches, html)
+ return html
+
+ def detect_whitespace(self, html):
+ blanks_around_headings = re.compile(r'(?P(]*>\s*
\s*){1,}\s*)?(?P\d+)[^>]*>.*?)(?P\s*(]*>\s*
\s*){1,})?', re.IGNORECASE)
+ blanks_n_nopunct = re.compile(r'(?P(]*>\s*
\s*){1,}\s*)?]*>\s*(<(span|[ibu]|em|strong|font)[^>]*>\s*)*.{1,100}?[^\W]((span|[ibu]|em|strong|font)>\s*)*
(?P\s*(]*>\s*
\s*){1,})?', re.IGNORECASE)
+
+ def merge_header_whitespace(match):
+ initblanks = match.group('initparas')
+ endblanks = match.group('initparas')
+ heading = match.group('heading')
+ top_margin = ''
+ bottom_margin = ''
+ if initblanks is not None:
+ top_margin = 'margin-top:'+str(len(self.single_blank.findall(initblanks)))+'em;'
+ if endblanks is not None:
+ bottom_margin = 'margin-bottom:'+str(len(self.single_blank.findall(initblanks)))+'em;'
+
+ if initblanks == None and endblanks == None:
+ return heading
+ else:
+ heading = re.sub('(?i)\d+)[^>]*>', '\n\n'+' style="'+top_margin+bottom_margin+'">', heading)
+ return heading
+
+ html = blanks_around_headings.sub(merge_header_whitespace, html)
+
+ def markup_whitespaces(match):
+ blanks = match.group(0)
+ blanks = self.blankreg.sub('\n
', blanks)
+ return blanks
+
+ html = blanks_n_nopunct.sub(markup_whitespaces, html)
if self.html_preprocess_sections > self.min_chapters:
- html = re.sub('(?si)^.*?(?=
', html)
+ html = self.multi_blank.sub('\n
', html)
else:
- html = self.blankreg.sub('\n
', html)
+ html = self.blankreg.sub('\n
', html)
return html
+ def markup_user_break(self, replacement_break):
+ '''
+ Takes string a user supplies and wraps it in markup that will be centered with
+ appropriate margins.
and
tags are allowed. If the user specifies
+ a style with width attributes in the
tag then the appropriate margins are
+ applied to wrapping divs. This is because many ebook devices don't support margin:auto
+ All other html is converted to text.
+ '''
+ hr_open = ''
+ if re.findall('(<|>)', replacement_break):
+ if re.match('^
\d+).*', '\g', replacement_break))
+ replacement_break = re.sub('(?i)(width=\d+\%?|width:\s*\d+(\%|px|pt|em)?;?)', '', replacement_break)
+ divpercent = (100 - width) / 2
+ hr_open = re.sub('45', str(divpercent), hr_open)
+ scene_break = hr_open+replacement_break+'
'
+ else:
+ scene_break = hr_open+'
'
+ elif re.match('^
'
+ else:
+ from calibre.utils.html2text import html2text
+ replacement_break = html2text(replacement_break)
+ replacement_break = re.sub('\s', ' ', replacement_break)
+ scene_break = self.scene_break_open+replacement_break+''
+ else:
+ replacement_break = re.sub('\s', ' ', replacement_break)
+ scene_break = self.scene_break_open+replacement_break+''
+
+ return scene_break
def __call__(self, html):
self.log.debug("********* Heuristic processing HTML *********")
-
# Count the words in the document to estimate how many chapters to look for and whether
# other types of processing are attempted
try:
@@ -458,7 +530,7 @@ class HeuristicProcessor(object):
# Arrange line feeds and tags so the line_length and no_markup functions work correctly
html = self.arrange_htm_line_endings(html)
-
+ #self.dump(html, 'after_arrange_line_endings')
if self.cleanup_required():
###### Check Markup ######
#
@@ -478,6 +550,11 @@ class HeuristicProcessor(object):
# fix indents must run before this step, as it removes non-breaking spaces
html = self.cleanup_markup(html)
+ is_pdftohtml = self.is_pdftohtml(html)
+ if is_pdftohtml:
+ self.line_open = "<(?Pp)[^>]*>(\s*<[ibu][^>]*>)?\s*"
+ self.line_close = "\s*([ibu][^>]*>\s*)?(?P=outer)>"
+
# ADE doesn't render
, change to empty paragraphs
#html = re.sub('
]*>', u'\u00a0
', html)
@@ -489,6 +566,7 @@ class HeuristicProcessor(object):
if getattr(self.extra_opts, 'markup_chapter_headings', False):
html = self.markup_chapters(html, self.totalwords, self.blanks_between_paragraphs)
+ #self.dump(html, 'after_chapter_markup')
if getattr(self.extra_opts, 'italicize_common_cases', False):
html = self.markup_italicis(html)
@@ -498,7 +576,7 @@ class HeuristicProcessor(object):
if self.blanks_between_paragraphs and getattr(self.extra_opts, 'delete_blank_paragraphs', False):
self.log.debug("deleting blank lines")
self.blanks_deleted = True
- html = self.multi_blank.sub('\n
', html)
+ html = self.multi_blank.sub('\n
', html)
html = self.blankreg.sub('', html)
# Determine line ending type
@@ -539,7 +617,7 @@ class HeuristicProcessor(object):
if self.html_preprocess_sections < self.min_chapters and getattr(self.extra_opts, 'markup_chapter_headings', False):
self.log.debug("Looking for more split points based on punctuation,"
" currently have " + unicode(self.html_preprocess_sections))
- chapdetect3 = re.compile(r'<(?P(p|div)[^>]*)>\s*(?P(]*>)?\s*(?!([*#•]+\s*)+)(<[ibu][^>]*>){0,2}\s*(]*>)?\s*(<[ibu][^>]*>){0,2}\s*(]*>)?\s*.?(?=[a-z#\-*\s]+<)([a-z#-*]+\s*){1,5}\s*\s*()?([ibu]>){0,2}\s*()?\s*([ibu]>){0,2}\s*()?\s*(p|div)>)', re.IGNORECASE)
+ chapdetect3 = re.compile(r'<(?P(p|div)[^>]*)>\s*(?P(]*>)?\s*(?!([\W]+\s*)+)(<[ibu][^>]*>){0,2}\s*(]*>)?\s*(<[ibu][^>]*>){0,2}\s*(]*>)?\s*.?(?=[a-z#\-*\s]+<)([a-z#-*]+\s*){1,5}\s*\s*()?([ibu]>){0,2}\s*()?\s*([ibu]>){0,2}\s*()?\s*(p|div)>)', re.IGNORECASE)
html = chapdetect3.sub(self.chapter_break, html)
if getattr(self.extra_opts, 'renumber_headings', False):
@@ -549,14 +627,32 @@ class HeuristicProcessor(object):
doubleheading = re.compile(r'(?P]*>.+?\s*(<(?!h\d)[^>]*>\s*)*)[^>]*>.+?)', re.IGNORECASE)
html = doubleheading.sub('\g'+'\n'+'
', html)
+ # If scene break formatting is enabled, find all blank paragraphs that definitely aren't scenebreaks,
+ # style it with the 'whitespace' class. All remaining blank lines are styled as softbreaks.
+ # Multiple sequential blank paragraphs are merged with appropriate margins
+ # If non-blank scene breaks exist they are center aligned and styled with appropriate margins.
if getattr(self.extra_opts, 'format_scene_breaks', False):
- html = self.detect_blank_formatting(html)
+ html = self.detect_whitespace(html)
html = self.detect_soft_breaks(html)
- # Center separator lines
- html = re.sub(u'<(?Pp|div)[^>]*>\s*(<(?Pfont|span|[ibu])[^>]*>)?\s*(<(?Pfont|span|[ibu])[^>]*>)?\s*(<(?Pfont|span|[ibu])[^>]*>)?\s*(?P([*#•=✦]+\s*)+)\s*((?P=inner3)>)?\s*((?P=inner2)>)?\s*((?P=inner1)>)?\s*(?P=outer)>', '' + '\g' + '
', html)
- #html = re.sub(']*>\s*
', '
', html)
+ blanks_count = len(self.any_multi_blank.findall(html))
+ if blanks_count >= 1:
+ html = self.merge_blanks(html, blanks_count)
+ scene_break_regex = self.line_open+'(?![\w\'\"])(?P((?P((?!\s)\W))\s*(?P=break_char)?)+)\s*'+self.line_close
+ scene_break = re.compile(r'%s' % scene_break_regex, re.IGNORECASE|re.UNICODE)
+ # If the user has enabled scene break replacement, then either softbreaks
+ # or 'hard' scene breaks are replaced, depending on which is in use
+ # Otherwise separator lines are centered, use a bit larger margin in this case
+ replacement_break = getattr(self.extra_opts, 'replace_scene_breaks', None)
+ if replacement_break:
+ replacement_break = self.markup_user_break(replacement_break)
+ if len(scene_break.findall(html)) >= 1:
+ html = scene_break.sub(replacement_break, html)
+ else:
+ html = re.sub(']*>\s*
', replacement_break, html)
+ else:
+ html = scene_break.sub(self.scene_break_open+'\g'+'', html)
if self.deleted_nbsps:
- # put back non-breaking spaces in empty paragraphs to preserve original formatting
+ # put back non-breaking spaces in empty paragraphs so they render correctly
html = self.anyblank.sub('\n'+r'\g'+u'\u00a0'+r'\g', html)
return html
diff --git a/src/calibre/ebooks/epub/input.py b/src/calibre/ebooks/epub/input.py
index ec2004d81c..e22ed27371 100644
--- a/src/calibre/ebooks/epub/input.py
+++ b/src/calibre/ebooks/epub/input.py
@@ -175,6 +175,19 @@ class EPUBInput(InputFormatPlugin):
raise ValueError(
'EPUB files with DTBook markup are not supported')
+ for x in list(opf.iterspine()):
+ ref = x.get('idref', None)
+ if ref is None:
+ x.getparent().remove(x)
+ continue
+ for y in opf.itermanifest():
+ if y.get('id', None) == ref and y.get('media-type', None) in \
+ ('application/vnd.adobe-page-template+xml',):
+ p = x.getparent()
+ if p is not None:
+ p.remove(x)
+ break
+
with open('content.opf', 'wb') as nopf:
nopf.write(opf.render())
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index 62d57f2251..dfb902b5b9 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -422,6 +422,33 @@ class MetadataField(object):
elem = obj.create_metadata_element(self.name, is_dc=self.is_dc)
obj.set_text(elem, self.renderer(val))
+class TitleSortField(MetadataField):
+
+ def __get__(self, obj, type=None):
+ c = self.__real_get__(obj, type)
+ if c is None:
+ matches = obj.title_path(obj.metadata)
+ if matches:
+ for match in matches:
+ ans = match.get('{%s}file-as'%obj.NAMESPACES['opf'], None)
+ if not ans:
+ ans = match.get('file-as', None)
+ if ans:
+ c = ans
+ if not c:
+ c = self.none_is
+ else:
+ c = c.strip()
+ return c
+
+ def __set__(self, obj, val):
+ MetadataField.__set__(self, obj, val)
+ matches = obj.title_path(obj.metadata)
+ if matches:
+ for match in matches:
+ for attr in list(match.attrib):
+ if attr.endswith('file-as'):
+ del match.attrib[attr]
def serialize_user_metadata(metadata_elem, all_user_metadata, tail='\n'+(' '*8)):
from calibre.utils.config import to_json
@@ -490,6 +517,7 @@ class OPF(object): # {{{
rights = MetadataField('rights')
series = MetadataField('series', is_dc=False)
series_index = MetadataField('series_index', is_dc=False, formatter=float, none_is=1)
+ title_sort = TitleSortField('title_sort', is_dc=False)
rating = MetadataField('rating', is_dc=False, formatter=int)
pubdate = MetadataField('date', formatter=parse_date,
renderer=isoformat)
@@ -776,30 +804,6 @@ class OPF(object): # {{{
return property(fget=fget, fset=fset)
- @dynamic_property
- def title_sort(self):
-
- def fget(self):
- matches = self.title_path(self.metadata)
- if matches:
- for match in matches:
- ans = match.get('{%s}file-as'%self.NAMESPACES['opf'], None)
- if not ans:
- ans = match.get('file-as', None)
- if ans:
- return ans
-
- def fset(self, val):
- matches = self.title_path(self.metadata)
- if matches:
- for key in matches[0].attrib:
- if key.endswith('file-as'):
- matches[0].attrib.pop(key)
- matches[0].set('{%s}file-as'%self.NAMESPACES['opf'], unicode(val))
-
- return property(fget=fget, fset=fset)
-
-
@dynamic_property
def tags(self):
@@ -1129,8 +1133,6 @@ class OPFCreator(Metadata):
metadata = M.metadata()
a = metadata.append
role = {}
- if self.title_sort:
- role = {'file-as':self.title_sort}
a(DC_ELEM('title', self.title if self.title else _('Unknown'),
opf_attrs=role))
for i, author in enumerate(self.authors):
@@ -1165,6 +1167,8 @@ class OPFCreator(Metadata):
a(CAL_ELEM('calibre:series', self.series))
if self.series_index is not None:
a(CAL_ELEM('calibre:series_index', self.format_series_index()))
+ if self.title_sort:
+ a(CAL_ELEM('calibre:title_sort', self.title_sort))
if self.rating is not None:
a(CAL_ELEM('calibre:rating', str(self.rating)))
if self.timestamp is not None:
@@ -1320,7 +1324,6 @@ def test_m2o():
mi.author_sort = 'author sort'
mi.pubdate = nowf()
mi.language = 'en'
- mi.category = 'test'
mi.comments = 'what a fun book\n\n'
mi.publisher = 'publisher'
mi.isbn = 'boooo'
@@ -1335,11 +1338,11 @@ def test_m2o():
opf = metadata_to_opf(mi)
print opf
newmi = MetaInformation(OPF(StringIO(opf)))
- for attr in ('author_sort', 'title_sort', 'comments', 'category',
+ for attr in ('author_sort', 'title_sort', 'comments',
'publisher', 'series', 'series_index', 'rating',
'isbn', 'tags', 'cover_data', 'application_id',
'language', 'cover',
- 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc',
+ 'book_producer', 'timestamp',
'pubdate', 'rights', 'publication_type'):
o, n = getattr(mi, attr), getattr(newmi, attr)
if o != n and o.strip() != n.strip():
@@ -1441,4 +1444,6 @@ def test_user_metadata():
print opf.render()
if __name__ == '__main__':
- test_user_metadata()
+ #test_user_metadata()
+ #test_m2o()
+ test()
diff --git a/src/calibre/ebooks/metadata/sources/base.py b/src/calibre/ebooks/metadata/sources/base.py
new file mode 100644
index 0000000000..89ad8a7956
--- /dev/null
+++ b/src/calibre/ebooks/metadata/sources/base.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+
+__license__ = 'GPL v3'
+__copyright__ = '2011, Kovid Goyal '
+__docformat__ = 'restructuredtext en'
+
+import re
+
+from calibre.customize import Plugin
+
+class Source(Plugin):
+
+ type = _('Metadata source')
+ author = 'Kovid Goyal'
+
+ supported_platforms = ['windows', 'osx', 'linux']
+
+ result_of_identify_is_complete = True
+
+ def get_author_tokens(self, authors):
+ 'Take a list of authors and return a list of tokens useful for a '
+ 'AND search query'
+ # Leave ' in there for Irish names
+ pat = re.compile(r'[-,:;+!@#$%^&*(){}.`~"\s\[\]/]')
+ for au in authors:
+ for tok in au.split():
+ yield pat.sub('', tok)
+
+ def split_jobs(self, jobs, num):
+ 'Split a list of jobs into at most num groups, as evenly as possible'
+ groups = [[] for i in range(num)]
+ jobs = list(jobs)
+ while jobs:
+ for gr in groups:
+ try:
+ job = jobs.pop()
+ except IndexError:
+ break
+ gr.append(job)
+ return [g for g in groups if g]
+
+ def identify(self, log, result_queue, abort, title=None, authors=None, identifiers={}):
+ '''
+ Identify a book by its title/author/isbn/etc.
+
+ :param log: A log object, use it to output debugging information/errors
+ :param result_queue: A result Queue, results should be put into it.
+ Each result is a Metadata object
+ :param abort: If abort.is_set() returns True, abort further processing
+ and return as soon as possible
+ :param title: The title of the book, can be None
+ :param authors: A list of authors of the book, can be None
+ :param identifiers: A dictionary of other identifiers, most commonly
+ {'isbn':'1234...'}
+ :return: None if no errors occurred, otherwise a unicode representation
+ of the error suitable for showing to the user
+
+ '''
+ return None
+
diff --git a/src/calibre/ebooks/metadata/sources/google.py b/src/calibre/ebooks/metadata/sources/google.py
new file mode 100644
index 0000000000..d9efb65ae0
--- /dev/null
+++ b/src/calibre/ebooks/metadata/sources/google.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+
+__license__ = 'GPL v3'
+__copyright__ = '2011, Kovid Goyal '
+__docformat__ = 'restructuredtext en'
+
+import time
+from urllib import urlencode
+from functools import partial
+from threading import Thread
+
+from lxml import etree
+
+from calibre.ebooks.metadata.sources import Source
+from calibre.ebooks.metadata.book.base import Metadata
+from calibre.utils.date import parse_date, utcnow
+from calibre import browser, as_unicode
+
+NAMESPACES = {
+ 'openSearch':'http://a9.com/-/spec/opensearchrss/1.0/',
+ 'atom' : 'http://www.w3.org/2005/Atom',
+ 'dc': 'http://purl.org/dc/terms'
+ }
+XPath = partial(etree.XPath, namespaces=NAMESPACES)
+
+total_results = XPath('//openSearch:totalResults')
+start_index = XPath('//openSearch:startIndex')
+items_per_page = XPath('//openSearch:itemsPerPage')
+entry = XPath('//atom:entry')
+entry_id = XPath('descendant::atom:id')
+creator = XPath('descendant::dc:creator')
+identifier = XPath('descendant::dc:identifier')
+title = XPath('descendant::dc:title')
+date = XPath('descendant::dc:date')
+publisher = XPath('descendant::dc:publisher')
+subject = XPath('descendant::dc:subject')
+description = XPath('descendant::dc:description')
+language = XPath('descendant::dc:language')
+
+
+
+def to_metadata(browser, log, entry_):
+
+ def get_text(extra, x):
+ try:
+ ans = x(extra)
+ if ans:
+ ans = ans[0].text
+ if ans and ans.strip():
+ return ans.strip()
+ except:
+ log.exception('Programming error:')
+ return None
+
+
+ id_url = entry_id(entry_)[0].text
+ title_ = ': '.join([x.text for x in title(entry_)]).strip()
+ authors = [x.text.strip() for x in creator(entry_) if x.text]
+ if not authors:
+ authors = [_('Unknown')]
+ if not id_url or not title:
+ # Silently discard this entry
+ return None
+
+ mi = Metadata(title_, authors)
+ try:
+ raw = browser.open_novisit(id_url).read()
+ feed = etree.fromstring(raw)
+ extra = entry(feed)[0]
+ except:
+ log.exception('Failed to get additional details for', mi.title)
+ return mi
+
+ mi.comments = get_text(extra, description)
+ #mi.language = get_text(extra, language)
+ mi.publisher = get_text(extra, publisher)
+
+ # Author sort
+ for x in creator(extra):
+ for key, val in x.attrib.items():
+ if key.endswith('file-as') and val and val.strip():
+ mi.author_sort = val
+ break
+ # ISBN
+ isbns = []
+ for x in identifier(extra):
+ t = str(x.text).strip()
+ if t[:5].upper() in ('ISBN:', 'LCCN:', 'OCLC:'):
+ if t[:5].upper() == 'ISBN:':
+ isbns.append(t[5:])
+ if isbns:
+ mi.isbn = sorted(isbns, key=len)[-1]
+
+ # Tags
+ try:
+ btags = [x.text for x in subject(extra) if x.text]
+ tags = []
+ for t in btags:
+ tags.extend([y.strip() for y in t.split('/')])
+ tags = list(sorted(list(set(tags))))
+ except:
+ log.exception('Failed to parse tags:')
+ tags = []
+ if tags:
+ mi.tags = [x.replace(',', ';') for x in tags]
+
+ # pubdate
+ pubdate = get_text(extra, date)
+ if pubdate:
+ try:
+ default = utcnow().replace(day=15)
+ mi.pubdate = parse_date(pubdate, assume_utc=True, default=default)
+ except:
+ log.exception('Failed to parse pubdate')
+
+
+ return mi
+
+class Worker(Thread):
+
+ def __init__(self, log, entries, abort, result_queue):
+ self.browser, self.log, self.entries = browser(), log, entries
+ self.abort, self.result_queue = abort, result_queue
+ Thread.__init__(self)
+ self.daemon = True
+
+ def run(self):
+ for i in self.entries:
+ try:
+ ans = to_metadata(self.browser, self.log, i)
+ if isinstance(ans, Metadata):
+ self.result_queue.put(ans)
+ except:
+ self.log.exception(
+ 'Failed to get metadata for identify entry:',
+ etree.tostring(i))
+ if self.abort.is_set():
+ break
+
+
+class GoogleBooks(Source):
+
+ name = 'Google Books'
+
+ def create_query(self, log, title=None, authors=None, identifiers={},
+ start_index=1):
+ BASE_URL = 'http://books.google.com/books/feeds/volumes?'
+ isbn = identifiers.get('isbn', None)
+ q = ''
+ if isbn is not None:
+ q += 'isbn:'+isbn
+ elif title or authors:
+ def build_term(prefix, parts):
+ return ' '.join('in'+prefix + ':' + x for x in parts)
+ if title is not None:
+ q += build_term('title', title.split())
+ if authors:
+ q += ('+' if q else '')+build_term('author',
+ self.get_author_tokens(authors))
+
+ if isinstance(q, unicode):
+ q = q.encode('utf-8')
+ if not q:
+ return None
+ return BASE_URL+urlencode({
+ 'q':q,
+ 'max-results':20,
+ 'start-index':start_index,
+ 'min-viewability':'none',
+ })
+
+
+ def identify(self, log, result_queue, abort, title=None, authors=None, identifiers={}):
+ query = self.create_query(log, title=title, authors=authors,
+ identifiers=identifiers)
+ try:
+ raw = browser().open_novisit(query).read()
+ except Exception, e:
+ log.exception('Failed to make identify query: %r'%query)
+ return as_unicode(e)
+
+ try:
+ parser = etree.XMLParser(recover=True, no_network=True)
+ feed = etree.fromstring(raw, parser=parser)
+ entries = entry(feed)
+ except Exception, e:
+ log.exception('Failed to parse identify results')
+ return as_unicode(e)
+
+
+ groups = self.split_jobs(entries, 5) # At most 5 threads
+ if not groups:
+ return
+ workers = [Worker(log, entries, abort, result_queue) for entries in
+ groups]
+
+ if abort.is_set():
+ return
+
+ for worker in workers: worker.start()
+
+ has_alive_worker = True
+ while has_alive_worker and not abort.is_set():
+ has_alive_worker = False
+ for worker in workers:
+ if worker.is_alive():
+ has_alive_worker = True
+ time.sleep(0.1)
+
+ return None
+
+
+
+
diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py
index 0ae3c9ac9d..9576ccb637 100644
--- a/src/calibre/ebooks/mobi/reader.py
+++ b/src/calibre/ebooks/mobi/reader.py
@@ -103,6 +103,8 @@ class EXTHHeader(object):
pass
elif id == 108:
pass # Producer
+ elif id == 113:
+ pass # ASIN or UUID
#else:
# print 'unhandled metadata record', id, repr(content)
diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py
index 2a71ecd43b..abba173d69 100644
--- a/src/calibre/ebooks/mobi/writer.py
+++ b/src/calibre/ebooks/mobi/writer.py
@@ -1547,6 +1547,31 @@ class MobiWriter(object):
rights = 'Unknown'
exth.write(pack('>II', EXTH_CODES['rights'], len(rights) + 8))
exth.write(rights)
+ nrecs += 1
+
+ # Write UUID as ASIN
+ uuid = None
+ from calibre.ebooks.oeb.base import OPF
+ for x in oeb.metadata['identifier']:
+ if x.get(OPF('scheme'), None).lower() == 'uuid' or unicode(x).startswith('urn:uuid:'):
+ uuid = unicode(x).split(':')[-1]
+ break
+ if uuid is None:
+ from uuid import uuid4
+ uuid = str(uuid4())
+
+ if isinstance(uuid, unicode):
+ uuid = uuid.encode('utf-8')
+ exth.write(pack('>II', 113, len(uuid) + 8))
+ exth.write(uuid)
+ nrecs += 1
+
+ # Write cdetype
+ if not self.opts.mobi_periodical:
+ data = 'EBOK'
+ exth.write(pack('>II', 501, len(data)+8))
+ exth.write(data)
+ nrecs += 1
# Add a publication date entry
if oeb.metadata['date'] != [] :
diff --git a/src/calibre/ebooks/txt/txtml.py b/src/calibre/ebooks/txt/txtml.py
index bf33e5540a..660fd9d38a 100644
--- a/src/calibre/ebooks/txt/txtml.py
+++ b/src/calibre/ebooks/txt/txtml.py
@@ -218,7 +218,7 @@ class TXTMLizer(object):
if tag in SPACE_TAGS:
text.append(u' ')
-
+
# Scene breaks.
if tag == 'hr':
text.append('\n\n* * *\n\n')
diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py
index fb3e627789..b32568f8fd 100644
--- a/src/calibre/gui2/actions/device.py
+++ b/src/calibre/gui2/actions/device.py
@@ -74,23 +74,29 @@ class ShareConnMenu(QMenu): # {{{
opts = email_config().parse()
if opts.accounts:
self.email_to_menu = QMenu(_('Email to')+'...', self)
+ ac = self.addMenu(self.email_to_menu)
+ self.email_actions.append(ac)
+ self.email_to_and_delete_menu = QMenu(
+ _('Email to and delete from library')+'...', self)
keys = sorted(opts.accounts.keys())
for account in keys:
formats, auto, default = opts.accounts[account]
dest = 'mail:'+account+';'+formats
action1 = DeviceAction(dest, False, False, I('mail.png'),
- _('Email to')+' '+account)
+ account)
action2 = DeviceAction(dest, True, False, I('mail.png'),
- _('Email to')+' '+account+ _(' and delete from library'))
- map(self.email_to_menu.addAction, (action1, action2))
+ account + ' ' + _('(delete from library)'))
+ self.email_to_menu.addAction(action1)
+ self.email_to_and_delete_menu.addAction(action2)
map(self.memory.append, (action1, action2))
if default:
- map(self.addAction, (action1, action2))
- map(self.email_actions.append, (action1, action2))
- self.email_to_menu.addSeparator()
+ ac = DeviceAction(dest, False, False,
+ I('mail.png'), _('Email to') + ' ' +account)
+ self.addAction(ac)
+ self.email_actions.append(ac)
action1.a_s.connect(sync_menu.action_triggered)
action2.a_s.connect(sync_menu.action_triggered)
- ac = self.addMenu(self.email_to_menu)
+ ac = self.addMenu(self.email_to_and_delete_menu)
self.email_actions.append(ac)
else:
ac = self.addAction(_('Setup email based sharing of books'))
diff --git a/src/calibre/gui2/complete.py b/src/calibre/gui2/complete.py
new file mode 100644
index 0000000000..bdfbaaf0da
--- /dev/null
+++ b/src/calibre/gui2/complete.py
@@ -0,0 +1,356 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+
+__license__ = 'GPL v3'
+__copyright__ = '2011, Kovid Goyal '
+__docformat__ = 'restructuredtext en'
+
+
+from PyQt4.Qt import QLineEdit, QListView, QAbstractListModel, Qt, QTimer, \
+ QApplication, QPoint, QItemDelegate, QStyleOptionViewItem, \
+ QStyle, QEvent, pyqtSignal
+
+from calibre.utils.icu import sort_key, lower
+from calibre.gui2 import NONE
+from calibre.gui2.widgets import EnComboBox
+
+class CompleterItemDelegate(QItemDelegate): # {{{
+
+ ''' Renders the current item as thought it were selected '''
+
+ def __init__(self, view):
+ self.view = view
+ QItemDelegate.__init__(self, view)
+
+ def paint(self, p, opt, idx):
+ opt = QStyleOptionViewItem(opt)
+ opt.showDecorationSelected = True
+ if self.view.currentIndex() == idx:
+ opt.state |= QStyle.State_HasFocus
+ QItemDelegate.paint(self, p, opt, idx)
+
+# }}}
+
+class CompleteWindow(QListView): # {{{
+
+ '''
+ The completion popup. For keyboard and mouse handling see
+ :meth:`eventFilter`.
+ '''
+
+ #: This signal is emitted when the user selects one of the listed
+ #: completions, by mouse or keyboard
+ completion_selected = pyqtSignal(object)
+
+ def __init__(self, widget, model):
+ self.widget = widget
+ QListView.__init__(self)
+ self.setVisible(False)
+ self.setParent(None, Qt.Popup)
+ self.setAlternatingRowColors(True)
+ self.setFocusPolicy(Qt.NoFocus)
+ self._d = CompleterItemDelegate(self)
+ self.setItemDelegate(self._d)
+ self.setModel(model)
+ self.setFocusProxy(widget)
+ self.installEventFilter(self)
+ self.clicked.connect(self.do_selected)
+ self.entered.connect(self.do_entered)
+ self.setMouseTracking(True)
+
+ def do_entered(self, idx):
+ if idx.isValid():
+ self.setCurrentIndex(idx)
+
+ def do_selected(self, idx=None):
+ idx = self.currentIndex() if idx is None else idx
+ if idx.isValid():
+ data = unicode(self.model().data(idx, Qt.DisplayRole))
+ self.completion_selected.emit(data)
+ self.hide()
+
+ def eventFilter(self, o, e):
+ if o is not self:
+ return False
+ if e.type() == e.KeyPress:
+ key = e.key()
+ if key in (Qt.Key_Escape, Qt.Key_Backtab) or \
+ (key == Qt.Key_F4 and (e.modifiers() & Qt.AltModifier)):
+ self.hide()
+ return True
+ elif key in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab):
+ if key == Qt.Key_Tab and not self.currentIndex().isValid():
+ if self.model().rowCount() > 0:
+ self.setCurrentIndex(self.model().index(0))
+ self.do_selected()
+ return True
+ elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp,
+ Qt.Key_PageDown):
+ return False
+ # Send key event to associated line edit
+ self.widget.eat_focus_out = False
+ try:
+ self.widget.event(e)
+ finally:
+ self.widget.eat_focus_out = True
+ if not self.widget.hasFocus():
+ # Line edit lost focus
+ self.hide()
+ if e.isAccepted():
+ # Line edit consumed event
+ return True
+ elif e.type() == e.MouseButtonPress:
+ # Hide popup if user clicks outside it, otherwise
+ # pass event to popup
+ if not self.underMouse():
+ self.hide()
+ return True
+ elif e.type() in (e.InputMethod, e.ShortcutOverride):
+ QApplication.sendEvent(self.widget, e)
+
+ return False # Do not filter this event
+
+# }}}
+
+class CompleteModel(QAbstractListModel):
+
+ def __init__(self, parent=None):
+ QAbstractListModel.__init__(self, parent)
+ self.sep = ','
+ self.space_before_sep = False
+ self.items = []
+ self.lowered_items = []
+ self.matches = []
+
+ def set_items(self, items):
+ items = [unicode(x.strip()) for x in items]
+ self.items = list(sorted(items, key=lambda x: sort_key(x)))
+ self.lowered_items = [lower(x) for x in self.items]
+ self.matches = []
+ self.reset()
+
+ def rowCount(self, *args):
+ return len(self.matches)
+
+ def data(self, index, role):
+ if role == Qt.DisplayRole:
+ r = index.row()
+ try:
+ return self.matches[r]
+ except IndexError:
+ pass
+ return NONE
+
+ def get_matches(self, prefix):
+ '''
+ Return all matches that (case insensitively) start with prefix
+ '''
+ prefix = lower(prefix)
+ ans = []
+ if prefix:
+ for i, test in enumerate(self.lowered_items):
+ if test.startswith(prefix):
+ ans.append(self.items[i])
+ return ans
+
+ def update_matches(self, matches):
+ self.matches = matches
+ self.reset()
+
+class MultiCompleteLineEdit(QLineEdit):
+ '''
+ A line edit that completes on multiple items separated by a
+ separator. Use the :meth:`update_items_cache` to set the list of
+ all possible completions. Separator can be controlled with the
+ :meth:`set_separator` and :meth:`set_space_before_sep` methods.
+
+ A call to self.set_separator(None) will allow this widget to be used
+ to complete non multiple fields as well.
+ '''
+
+ def __init__(self, parent=None):
+ self.eat_focus_out = True
+ self.max_visible_items = 7
+ self.current_prefix = None
+ QLineEdit.__init__(self, parent)
+
+ self._model = CompleteModel(parent=self)
+ self.complete_window = CompleteWindow(self, self._model)
+ self.textEdited.connect(self.text_edited)
+ self.complete_window.completion_selected.connect(self.completion_selected)
+ self.installEventFilter(self)
+
+ # Interface {{{
+ def update_items_cache(self, complete_items):
+ self.all_items = complete_items
+
+ def set_separator(self, sep):
+ self.sep = sep
+
+ def set_space_before_sep(self, space_before):
+ self.space_before_sep = space_before
+
+ # }}}
+
+ def eventFilter(self, o, e):
+ if self.eat_focus_out and o is self and e.type() == QEvent.FocusOut:
+ if self.complete_window.isVisible():
+ return True # Filter this event since the cw is visible
+ return QLineEdit.eventFilter(self, o, e)
+
+
+ def text_edited(self, *args):
+ self.update_completions()
+
+ def update_completions(self):
+ ' Update the list of completions '
+ if not self.complete_window.isVisible() and not self.hasFocus():
+ return
+ cpos = self.cursorPosition()
+ text = unicode(self.text())
+ prefix = text[:cpos]
+ self.current_prefix = prefix
+ complete_prefix = prefix.lstrip()
+ if self.sep:
+ complete_prefix = prefix = prefix.split(self.sep)[-1].lstrip()
+
+ matches = self._model.get_matches(complete_prefix)
+ self.update_complete_window(matches)
+
+ def get_completed_text(self, text):
+ '''
+ Get completed text from current cursor position and the completion
+ text
+ '''
+ if self.sep is None:
+ return -1, text
+ else:
+ cursor_pos = self.cursorPosition()
+ before_text = unicode(self.text())[:cursor_pos]
+ after_text = unicode(self.text())[cursor_pos:]
+ after_parts = after_text.split(self.sep)
+ if len(after_parts) < 3 and not after_parts[-1].strip():
+ after_text = u''
+ prefix_len = len(before_text.split(self.sep)[-1].lstrip())
+ return prefix_len, \
+ before_text[:cursor_pos - prefix_len] + text + after_text
+
+ def completion_selected(self, text):
+ prefix_len, ctext = self.get_completed_text(text)
+ if self.sep is None:
+ self.setText(ctext)
+ self.setCursorPosition(len(ctext))
+ else:
+ cursor_pos = self.cursorPosition()
+ self.setText(ctext)
+ self.setCursorPosition(cursor_pos - prefix_len + len(text))
+
+ def update_complete_window(self, matches):
+ self._model.update_matches(matches)
+ if matches:
+ self.show_complete_window()
+ else:
+ self.complete_window.hide()
+
+
+ def position_complete_window(self):
+ popup = self.complete_window
+ screen = QApplication.desktop().availableGeometry(self)
+ h = (popup.sizeHintForRow(0) * min(self.max_visible_items,
+ popup.model().rowCount()) + 3) + 3
+ hsb = popup.horizontalScrollBar()
+ if hsb and hsb.isVisible():
+ h += hsb.sizeHint().height()
+
+ rh = self.height()
+ pos = self.mapToGlobal(QPoint(0, self.height() - 2))
+ w = self.width()
+
+ if w > screen.width():
+ w = screen.width()
+ if (pos.x() + w) > (screen.x() + screen.width()):
+ pos.setX(screen.x() + screen.width() - w)
+ if (pos.x() < screen.x()):
+ pos.setX(screen.x())
+
+ top = pos.y() - rh - screen.top() + 2
+ bottom = screen.bottom() - pos.y()
+ h = max(h, popup.minimumHeight())
+ if h > bottom:
+ h = min(max(top, bottom), h)
+ if top > bottom:
+ pos.setY(pos.y() - h - rh + 2)
+
+ popup.setGeometry(pos.x(), pos.y(), w, h)
+
+
+ def show_complete_window(self):
+ self.position_complete_window()
+ self.complete_window.show()
+
+ def moveEvent(self, ev):
+ ret = QLineEdit.moveEvent(self, ev)
+ QTimer.singleShot(0, self.position_complete_window)
+ return ret
+
+ def resizeEvent(self, ev):
+ ret = QLineEdit.resizeEvent(self, ev)
+ QTimer.singleShot(0, self.position_complete_window)
+ return ret
+
+
+ @dynamic_property
+ def all_items(self):
+ def fget(self):
+ return self._model.items
+ def fset(self, items):
+ self._model.set_items(items)
+ return property(fget=fget, fset=fset)
+
+ @dynamic_property
+ def sep(self):
+ def fget(self):
+ return self._model.sep
+ def fset(self, val):
+ self._model.sep = val
+ return property(fget=fget, fset=fset)
+
+ @dynamic_property
+ def space_before_sep(self):
+ def fget(self):
+ return self._model.space_before_sep
+ def fset(self, val):
+ self._model.space_before_sep = val
+ return property(fget=fget, fset=fset)
+
+class MultiCompleteComboBox(EnComboBox):
+
+ def __init__(self, *args):
+ EnComboBox.__init__(self, *args)
+ self.setLineEdit(MultiCompleteLineEdit(self))
+ # Needed to allow changing the case of an existing item
+ # otherwise on focus out, the text is changed to the
+ # item that matches case insensitively
+ c = self.lineEdit().completer()
+ c.setCaseSensitivity(Qt.CaseSensitive)
+
+ def update_items_cache(self, complete_items):
+ self.lineEdit().update_items_cache(complete_items)
+
+ def set_separator(self, sep):
+ self.lineEdit().set_separator(sep)
+
+ def set_space_before_sep(self, space_before):
+ self.lineEdit().set_space_before_sep(space_before)
+
+
+
+if __name__ == '__main__':
+ from PyQt4.Qt import QDialog, QVBoxLayout
+ app = QApplication([])
+ d = QDialog()
+ d.setLayout(QVBoxLayout())
+ le = MultiCompleteLineEdit(d)
+ d.layout().addWidget(le)
+ le.all_items = ['one', 'otwo', 'othree', 'ooone', 'ootwo', 'oothree']
+ d.exec_()
diff --git a/src/calibre/gui2/convert/heuristics.py b/src/calibre/gui2/convert/heuristics.py
index e788888257..5e7e4aa506 100644
--- a/src/calibre/gui2/convert/heuristics.py
+++ b/src/calibre/gui2/convert/heuristics.py
@@ -6,6 +6,7 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import Qt
+from calibre.gui2 import gprefs
from calibre.gui2.convert.heuristics_ui import Ui_Form
from calibre.gui2.convert import Widget
@@ -21,17 +22,38 @@ class HeuristicsWidget(Widget, Ui_Form):
['enable_heuristics', 'markup_chapter_headings',
'italicize_common_cases', 'fix_indents',
'html_unwrap_factor', 'unwrap_lines',
- 'delete_blank_paragraphs', 'format_scene_breaks',
+ 'delete_blank_paragraphs',
+ 'format_scene_breaks', 'replace_scene_breaks',
'dehyphenate', 'renumber_headings']
)
self.db, self.book_id = db, book_id
+ self.rssb_defaults = [u'', u'
', u'∗ ∗ ∗', u'• • •', u'♦ ♦ ♦',
+ u'† †', u'‡ ‡ ‡', u'∞ ∞ ∞', u'¤ ¤ ¤', u'§']
self.initialize_options(get_option, get_help, db, book_id)
+ self.load_histories()
+
self.opt_enable_heuristics.stateChanged.connect(self.enable_heuristics)
self.opt_unwrap_lines.stateChanged.connect(self.enable_unwrap)
self.enable_heuristics(self.opt_enable_heuristics.checkState())
+ def restore_defaults(self, get_option):
+ Widget.restore_defaults(self, get_option)
+
+ self.save_histories()
+ rssb_hist = gprefs['replace_scene_breaks_history']
+ for x in self.rssb_defaults:
+ if x in rssb_hist:
+ del rssb_hist[rssb_hist.index(x)]
+ gprefs['replace_scene_breaks_history'] = self.rssb_defaults + gprefs['replace_scene_breaks_history']
+ self.load_histories()
+
+ def commit_options(self, save_defaults=False):
+ self.save_histories()
+
+ return Widget.commit_options(self, save_defaults)
+
def break_cycles(self):
Widget.break_cycles(self)
@@ -45,6 +67,33 @@ class HeuristicsWidget(Widget, Ui_Form):
if val is None and g is self.opt_html_unwrap_factor:
g.setValue(0.0)
return True
+ if not val and g is self.opt_replace_scene_breaks:
+ g.lineEdit().setText('')
+ return True
+
+ def load_histories(self):
+ self.opt_replace_scene_breaks.clear()
+ self.opt_replace_scene_breaks.lineEdit().setText('')
+
+ val = unicode(self.opt_replace_scene_breaks.currentText())
+ rssb_hist = gprefs.get('replace_scene_breaks_history', self.rssb_defaults)
+ if val in rssb_hist:
+ del rssb_hist[rssb_hist.index(val)]
+ rssb_hist.insert(0, val)
+ for v in rssb_hist:
+ # Ensure we don't have duplicate items.
+ if self.opt_replace_scene_breaks.findText(v) == -1:
+ self.opt_replace_scene_breaks.addItem(v)
+ self.opt_replace_scene_breaks.setCurrentIndex(0)
+
+ def save_histories(self):
+ rssb_history = []
+ history_pats = [unicode(self.opt_replace_scene_breaks.lineEdit().text())] + [unicode(self.opt_replace_scene_breaks.itemText(i)) for i in xrange(self.opt_replace_scene_breaks.count())]
+ for p in history_pats[:10]:
+ # Ensure we don't have duplicate items.
+ if p not in rssb_history:
+ rssb_history.append(p)
+ gprefs['replace_scene_breaks_history'] = rssb_history
def enable_heuristics(self, state):
state = state == Qt.Checked
diff --git a/src/calibre/gui2/convert/heuristics.ui b/src/calibre/gui2/convert/heuristics.ui
index 6863fcf8e6..46d62061af 100644
--- a/src/calibre/gui2/convert/heuristics.ui
+++ b/src/calibre/gui2/convert/heuristics.ui
@@ -150,6 +150,45 @@
+ -
+
+
+ QLayout::SetDefaultConstraint
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Replace soft scene &breaks:
+
+
+ opt_replace_scene_breaks
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ true
+
+
+ QComboBox::InsertAtTop
+
+
+
+
+
-
diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py
index 23cac74cf8..81274f25a8 100644
--- a/src/calibre/gui2/convert/metadata.py
+++ b/src/calibre/gui2/convert/metadata.py
@@ -70,9 +70,6 @@ class MetadataWidget(Widget, Ui_Form):
def initialize_metadata_options(self):
self.initialize_combos()
self.author.editTextChanged.connect(self.deduce_author_sort)
- self.author.set_separator('&')
- self.author.set_space_before_sep(True)
- self.author.update_items_cache(self.db.all_author_names())
mi = self.db.get_metadata(self.book_id, index_is_id=True)
self.title.setText(mi.title)
@@ -109,6 +106,9 @@ class MetadataWidget(Widget, Ui_Form):
def initalize_authors(self):
all_authors = self.db.all_authors()
all_authors.sort(key=lambda x : sort_key(x[1]))
+ self.author.set_separator('&')
+ self.author.set_space_before_sep(True)
+ self.author.update_items_cache(self.db.all_author_names())
for i in all_authors:
id, name = i
@@ -124,6 +124,8 @@ class MetadataWidget(Widget, Ui_Form):
def initialize_series(self):
all_series = self.db.all_series()
all_series.sort(key=lambda x : sort_key(x[1]))
+ self.series.set_separator(None)
+ self.series.update_items_cache([x[1] for x in all_series])
for i in all_series:
id, name = i
@@ -133,6 +135,8 @@ class MetadataWidget(Widget, Ui_Form):
def initialize_publisher(self):
all_publishers = self.db.all_publishers()
all_publishers.sort(key=lambda x : sort_key(x[1]))
+ self.publisher.set_separator(None)
+ self.publisher.update_items_cache([x[1] for x in all_publishers])
for i in all_publishers:
id, name = i
diff --git a/src/calibre/gui2/convert/metadata.ui b/src/calibre/gui2/convert/metadata.ui
index 61c27594c4..95ccac6890 100644
--- a/src/calibre/gui2/convert/metadata.ui
+++ b/src/calibre/gui2/convert/metadata.ui
@@ -190,7 +190,7 @@
-
-
+
Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.
@@ -213,7 +213,7 @@
-
-
+
10
@@ -248,14 +248,14 @@
-
-
+
true
-
-
+
true
@@ -277,19 +277,14 @@
- EnComboBox
+ MultiCompleteComboBox
QComboBox
-
+
- CompleteComboBox
- QComboBox
-
-
-
- CompleteLineEdit
+ MultiCompleteLineEdit
QLineEdit
-
+
ImageView
diff --git a/src/calibre/gui2/convert/structure_detection.ui b/src/calibre/gui2/convert/structure_detection.ui
index ef0677a67c..f80e6f8182 100644
--- a/src/calibre/gui2/convert/structure_detection.ui
+++ b/src/calibre/gui2/convert/structure_detection.ui
@@ -48,10 +48,10 @@
- -
+
-
- -
+
-
Qt::Vertical
@@ -77,6 +77,16 @@
+ -
+
+
+ The header and footer removal options have been replaced by the Search & Replace options. Click the Search & Replace category in the bar to the left to use these options. Leave the replace field blank and enter your header/footer removal regexps into the search field.
+
+
+ true
+
+
+
diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py
index 360a5bcd0a..5180999379 100644
--- a/src/calibre/gui2/custom_column_widgets.py
+++ b/src/calibre/gui2/custom_column_widgets.py
@@ -14,7 +14,7 @@ from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
QPushButton
from calibre.utils.date import qt_to_dt, now
-from calibre.gui2.widgets import CompleteLineEdit, EnComboBox
+from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
from calibre.gui2.comments_editor import Editor as CommentsEditor
from calibre.gui2 import UNDEFINED_QDATE, error_dialog
from calibre.utils.config import tweaks
@@ -228,10 +228,12 @@ class Text(Base):
values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key)
if self.col_metadata['is_multiple']:
- w = CompleteLineEdit(parent, values)
+ w = MultiCompleteLineEdit(parent)
+ w.update_items_cache(values)
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
else:
- w = EnComboBox(parent)
+ w = MultiCompleteComboBox(parent)
+ w.set_separator(None)
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
w.setMinimumContentsLength(25)
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
@@ -240,9 +242,10 @@ class Text(Base):
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
self.initial_val = val
val = self.normalize_db_val(val)
+ self.widgets[1].update_items_cache(self.all_values)
+
if self.col_metadata['is_multiple']:
self.setter(val)
- self.widgets[1].update_items_cache(self.all_values)
else:
idx = None
for i, c in enumerate(self.all_values):
@@ -276,7 +279,7 @@ class Series(Base):
def setup_ui(self, parent):
values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key)
- w = EnComboBox(parent)
+ w = MultiCompleteComboBox(parent)
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
w.setMinimumContentsLength(25)
self.name_widget = w
@@ -305,6 +308,7 @@ class Series(Base):
if c == val:
idx = i
self.name_widget.addItem(c)
+ self.name_widget.update_items_cache(self.all_values)
self.name_widget.setEditText('')
if idx is not None:
self.widgets[1].setCurrentIndex(idx)
@@ -670,7 +674,7 @@ class BulkDateTime(BulkBase):
class BulkSeries(BulkBase):
def setup_ui(self, parent):
- self.make_widgets(parent, EnComboBox)
+ self.make_widgets(parent, MultiCompleteComboBox)
values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key)
self.main_widget.setSizeAdjustPolicy(self.main_widget.AdjustToMinimumContentsLengthWithIcon)
@@ -705,6 +709,8 @@ class BulkSeries(BulkBase):
def initialize(self, book_id):
self.idx_widget.setChecked(False)
+ self.main_widget.set_separator(None)
+ self.main_widget.update_items_cache(self.all_values)
for c in self.all_values:
self.main_widget.addItem(c)
self.main_widget.setEditText('')
@@ -795,7 +801,8 @@ class RemoveTags(QWidget):
layout.setSpacing(5)
layout.setContentsMargins(0, 0, 0, 0)
- self.tags_box = CompleteLineEdit(parent, values)
+ self.tags_box = MultiCompleteLineEdit(parent)
+ self.tags_box.update_items_cache(values)
layout.addWidget(self.tags_box, stretch=3)
self.checkbox = QCheckBox(_('Remove all tags'), parent)
layout.addWidget(self.checkbox)
@@ -816,7 +823,7 @@ class BulkText(BulkBase):
values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key)
if self.col_metadata['is_multiple']:
- self.make_widgets(parent, CompleteLineEdit,
+ self.make_widgets(parent, MultiCompleteLineEdit,
extra_label_text=_('tags to add'))
self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.adding_widget = self.main_widget
@@ -829,16 +836,16 @@ class BulkText(BulkBase):
w.tags_box.textChanged.connect(self.a_c_checkbox_changed)
w.checkbox.stateChanged.connect(self.a_c_checkbox_changed)
else:
- self.make_widgets(parent, EnComboBox)
+ self.make_widgets(parent, MultiCompleteComboBox)
+ self.main_widget.set_separator(None)
self.main_widget.setSizeAdjustPolicy(
self.main_widget.AdjustToMinimumContentsLengthWithIcon)
self.main_widget.setMinimumContentsLength(25)
self.ignore_change_signals = False
def initialize(self, book_ids):
- if self.col_metadata['is_multiple']:
- self.main_widget.update_items_cache(self.all_values)
- else:
+ self.main_widget.update_items_cache(self.all_values)
+ if not self.col_metadata['is_multiple']:
val = self.get_initial_value(book_ids)
self.initial_val = val = self.normalize_db_val(val)
idx = None
diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py
index a5066a99ef..8efa7f154c 100644
--- a/src/calibre/gui2/device.py
+++ b/src/calibre/gui2/device.py
@@ -838,9 +838,10 @@ class DeviceMixin(object): # {{{
format_count[f] = 1
for f in self.device_manager.device.settings().format_map:
if f in format_count.keys():
- formats.append((f, _('%i of %i Books') % (format_count[f], len(rows))), True if f in aval_out_formats else False)
+ formats.append((f, _('%i of %i Books') % (format_count[f],
+ len(rows)), True if f in aval_out_formats else False))
elif f in aval_out_formats:
- formats.append((f, _('0 of %i Books') % len(rows)), True)
+ formats.append((f, _('0 of %i Books') % len(rows), True))
d = ChooseFormatDeviceDialog(self, _('Choose format to send to device'), formats)
if d.exec_() != QDialog.Accepted:
return
@@ -871,6 +872,16 @@ class DeviceMixin(object): # {{{
self.send_by_mail(to, fmts, delete)
def cover_to_thumbnail(self, data):
+ if self.device_manager.device and \
+ hasattr(self.device_manager.device, 'THUMBNAIL_WIDTH'):
+ try:
+ return thumbnail(data,
+ self.device_manager.device.THUMBNAIL_WIDTH,
+ self.device_manager.device.THUMBNAIL_HEIGHT,
+ preserve_aspect_ratio=False)
+ except:
+ pass
+ return
ht = self.device_manager.device.THUMBNAIL_HEIGHT \
if self.device_manager else DevicePlugin.THUMBNAIL_HEIGHT
try:
@@ -1272,6 +1283,8 @@ class DeviceMixin(object): # {{{
x = x.lower() if x else ''
return string_pat.sub('', x)
+ update_metadata = prefs['manage_device_metadata'] == 'on_connect'
+
# Force a reset if the caches are not initialized
if reset or not hasattr(self, 'db_book_title_cache'):
# Build a cache (map) of the library, so the search isn't On**2
@@ -1284,8 +1297,13 @@ class DeviceMixin(object): # {{{
except:
return False
+ get_covers = False
+ if update_metadata and self.device_manager.is_device_connected:
+ if self.device_manager.device.WANTS_UPDATED_THUMBNAILS:
+ get_covers = True
+
for id in db.data.iterallids():
- mi = db.get_metadata(id, index_is_id=True)
+ mi = db.get_metadata(id, index_is_id=True, get_cover=get_covers)
title = clean_string(mi.title)
if title not in db_book_title_cache:
db_book_title_cache[title] = \
@@ -1311,7 +1329,6 @@ class DeviceMixin(object): # {{{
# the application_id to the db_id of the matching book. This value
# will be used by books_on_device to indicate matches.
- update_metadata = prefs['manage_device_metadata'] == 'on_connect'
for booklist in booklists:
for book in booklist:
book.in_library = None
@@ -1382,6 +1399,12 @@ class DeviceMixin(object): # {{{
if update_metadata:
if self.device_manager.is_device_connected:
+ if self.device_manager.device.WANTS_UPDATED_THUMBNAILS:
+ for blist in booklists:
+ for book in blist:
+ if book.cover and os.access(book.cover, os.R_OK):
+ book.thumbnail = \
+ self.cover_to_thumbnail(open(book.cover, 'rb').read())
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
self.device_manager.sync_booklists(
Dispatcher(self.metadata_synced), booklists,
diff --git a/src/calibre/gui2/dialogs/add_empty_book.py b/src/calibre/gui2/dialogs/add_empty_book.py
index b8339f95f5..9e5fb07308 100644
--- a/src/calibre/gui2/dialogs/add_empty_book.py
+++ b/src/calibre/gui2/dialogs/add_empty_book.py
@@ -7,8 +7,8 @@ __license__ = 'GPL v3'
from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \
QApplication, QSpinBox, QToolButton, QIcon
from calibre.ebooks.metadata import authors_to_string, string_to_authors
-from calibre.gui2.widgets import CompleteComboBox
from calibre.utils.icu import sort_key
+from calibre.gui2.complete import MultiCompleteComboBox
class AddEmptyBookDialog(QDialog):
@@ -32,7 +32,7 @@ class AddEmptyBookDialog(QDialog):
self.author_label = QLabel(_('Set the author of the new books to:'))
self._layout.addWidget(self.author_label, 2, 0, 1, 2)
- self.authors_combo = CompleteComboBox(self)
+ self.authors_combo = MultiCompleteComboBox(self)
self.authors_combo.setSizeAdjustPolicy(
self.authors_combo.AdjustToMinimumContentsLengthWithIcon)
self.authors_combo.setEditable(True)
diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py
index 533a344de5..12f49baaca 100644
--- a/src/calibre/gui2/dialogs/metadata_bulk.py
+++ b/src/calibre/gui2/dialogs/metadata_bulk.py
@@ -764,6 +764,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
def initialize_series(self):
all_series = self.db.all_series()
all_series.sort(key=lambda x : sort_key(x[1]))
+ self.series.set_separator(None)
+ self.series.update_items_cache([x[1] for x in all_series])
for i in all_series:
id, name = i
@@ -773,6 +775,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
def initialize_publisher(self):
all_publishers = self.db.all_publishers()
all_publishers.sort(key=lambda x : sort_key(x[1]))
+ self.publisher.set_separator(None)
+ self.publisher.update_items_cache([x[1] for x in all_publishers])
for i in all_publishers:
id, name = i
diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui
index b0f2c144fc..ecdb396662 100644
--- a/src/calibre/gui2/dialogs/metadata_bulk.ui
+++ b/src/calibre/gui2/dialogs/metadata_bulk.ui
@@ -76,7 +76,7 @@
-
-
+
true
@@ -175,7 +175,7 @@
-
-
+
true
@@ -195,7 +195,7 @@
-
-
+
Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.
@@ -229,7 +229,7 @@
-
-
+
Comma separated list of tags to remove from the books.
@@ -262,7 +262,7 @@
-
-
+
0
@@ -1072,19 +1072,14 @@ not multiple and the destination field is multiple
- EnComboBox
+ MultiCompleteComboBox
QComboBox
-
+
- CompleteComboBox
- QComboBox
-
-
-
- CompleteLineEdit
+ MultiCompleteLineEdit
QLineEdit
-
+
HistoryLineEdit
diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py
index 7a8e4ea8d0..f36fd3019d 100644
--- a/src/calibre/gui2/dialogs/metadata_single.py
+++ b/src/calibre/gui2/dialogs/metadata_single.py
@@ -429,10 +429,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
old_extensions.add(ext)
for ext in new_extensions:
self.db.add_format(self.row, ext, open(paths[ext], 'rb'), notify=False)
- db_extensions = set([f.lower() for f in self.db.formats(self.row).split(',')])
+ dbfmts = self.db.formats(self.row)
+ db_extensions = set([f.lower() for f in (dbfmts.split(',') if dbfmts
+ else [])])
extensions = new_extensions.union(old_extensions)
for ext in db_extensions:
- if ext not in extensions:
+ if ext not in extensions and ext in self.original_formats:
self.db.remove_format(self.row, ext, notify=False)
def show_format(self, item, *args):
@@ -576,6 +578,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.orig_date = qt_to_dt(self.date.date())
exts = self.db.formats(row)
+ self.original_formats = []
if exts:
exts = exts.split(',')
for ext in exts:
@@ -586,6 +589,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if size is None:
continue
Format(self.formats, ext, size, timestamp=timestamp)
+ self.original_formats.append(ext.lower())
self.initialize_combos()
@@ -735,6 +739,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.series.setSizeAdjustPolicy(self.series.AdjustToContentsOnFirstShow)
all_series = self.db.all_series()
all_series.sort(key=lambda x : sort_key(x[1]))
+ self.series.set_separator(None)
+ self.series.update_items_cache([x[1] for x in all_series])
series_id = self.db.series_id(self.row)
idx, c = None, 0
for i in all_series:
@@ -752,6 +758,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
def initialize_publisher(self):
all_publishers = self.db.all_publishers()
all_publishers.sort(key=lambda x : sort_key(x[1]))
+ self.publisher.set_separator(None)
+ self.publisher.update_items_cache([x[1] for x in all_publishers])
publisher_id = self.db.publisher_id(self.row)
idx, c = None, 0
for i in all_publishers:
diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui
index 23efc45399..5bcf268aaa 100644
--- a/src/calibre/gui2/dialogs/metadata_single.ui
+++ b/src/calibre/gui2/dialogs/metadata_single.ui
@@ -240,7 +240,7 @@ Using this button to create author sort will change author sort from red to gree
-
-
+
true
@@ -313,7 +313,7 @@ If the box is colored green, then text matches the individual author's sort stri
-
-
+
true
@@ -335,7 +335,7 @@ If the box is colored green, then text matches the individual author's sort stri
-
-
-
+
Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.
@@ -379,7 +379,7 @@ If the box is colored green, then text matches the individual author's sort stri
5
-
-
+
List of known series. You can add new series.
@@ -837,19 +837,14 @@ If the box is colored green, then text matches the individual author's sort stri
- EnComboBox
- QComboBox
-
-
-
- CompleteLineEdit
+ MultiCompleteLineEdit
QLineEdit
-
+
- CompleteComboBox
+ MultiCompleteComboBox
QComboBox
-
+
FormatList
diff --git a/src/calibre/gui2/dialogs/search.py b/src/calibre/gui2/dialogs/search.py
index ab3fd3ec4e..9c91446f3c 100644
--- a/src/calibre/gui2/dialogs/search.py
+++ b/src/calibre/gui2/dialogs/search.py
@@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal '
import re, copy
-from PyQt4.Qt import QDialog, QDialogButtonBox, QCompleter, Qt
+from PyQt4.Qt import QDialog, QDialogButtonBox
from calibre.gui2.dialogs.search_ui import Ui_Dialog
from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH
@@ -29,20 +29,18 @@ class SearchDialog(QDialog, Ui_Dialog):
name = name.strip().replace('|', ',')
self.authors_box.addItem(name)
self.authors_box.setEditText('')
- self.authors_box.completer().setCompletionMode(QCompleter.PopupCompletion)
- self.authors_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive)
self.authors_box.set_separator('&')
self.authors_box.set_space_before_sep(True)
self.authors_box.update_items_cache(db.all_author_names())
all_series = db.all_series()
all_series.sort(key=lambda x : sort_key(x[1]))
+ self.series_box.set_separator(None)
+ self.series_box.update_items_cache([x[1] for x in all_series])
for i in all_series:
id, name = i
self.series_box.addItem(name)
self.series_box.setEditText('')
- self.series_box.completer().setCompletionMode(QCompleter.PopupCompletion)
- self.series_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive)
all_tags = db.all_tags()
self.tags_box.update_items_cache(all_tags)
diff --git a/src/calibre/gui2/dialogs/search.ui b/src/calibre/gui2/dialogs/search.ui
index 1d013a1e9f..eb6fffdb60 100644
--- a/src/calibre/gui2/dialogs/search.ui
+++ b/src/calibre/gui2/dialogs/search.ui
@@ -265,21 +265,21 @@
-
-
+
Enter an author's name. Only one author can be used.
-
-
+
Enter a series name, without an index. Only one series name can be used.
-
-
+
Enter tags separated by spaces
@@ -355,19 +355,14 @@
- EnComboBox
- QComboBox
-
-
-
- CompleteLineEdit
+ MultiCompleteLineEdit
QLineEdit
-
+
- CompleteComboBox
+ MultiCompleteComboBox
QComboBox
-
+
diff --git a/src/calibre/gui2/email.py b/src/calibre/gui2/email.py
index 6b2ed81413..426747e044 100644
--- a/src/calibre/gui2/email.py
+++ b/src/calibre/gui2/email.py
@@ -264,8 +264,9 @@ class EmailMixin(object): # {{{
if _auto_ids != []:
for id in _auto_ids:
if specific_format == None:
- formats = [f.lower() for f in self.library_view.model().db.formats(id, index_is_id=True).split(',')]
- formats = formats if formats != None else []
+ dbfmts = self.library_view.model().db.formats(id, index_is_id=True)
+ formats = [f.lower() for f in (dbfmts.split(',') if fmts else
+ [])]
if list(set(formats).intersection(available_input_formats())) != [] and list(set(fmts).intersection(available_output_formats())) != []:
auto.append(id)
else:
diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py
index ae9d6e2f71..fed2e42470 100644
--- a/src/calibre/gui2/library/delegates.py
+++ b/src/calibre/gui2/library/delegates.py
@@ -12,11 +12,11 @@ from PyQt4.Qt import QColor, Qt, QModelIndex, QSize, \
QPainterPath, QLinearGradient, QBrush, \
QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
QIcon, QDoubleSpinBox, QVariant, QSpinBox, \
- QStyledItemDelegate, QCompleter, \
- QComboBox, QTextDocument
+ QStyledItemDelegate, QComboBox, QTextDocument
from calibre.gui2 import UNDEFINED_QDATE, error_dialog
-from calibre.gui2.widgets import EnLineEdit, CompleteLineEdit
+from calibre.gui2.widgets import EnLineEdit
+from calibre.gui2.complete import MultiCompleteLineEdit
from calibre.utils.date import now, format_date
from calibre.utils.config import tweaks
from calibre.utils.formatter import validation_formatter
@@ -151,38 +151,15 @@ class TextDelegate(QStyledItemDelegate): # {{{
self.auto_complete_function = f
def createEditor(self, parent, option, index):
- editor = EnLineEdit(parent)
if self.auto_complete_function:
+ editor = MultiCompleteLineEdit(parent)
+ editor.set_separator(None)
complete_items = [i[1] for i in self.auto_complete_function()]
- completer = QCompleter(complete_items, self)
- completer.setCaseSensitivity(Qt.CaseInsensitive)
- completer.setCompletionMode(QCompleter.PopupCompletion)
- editor.setCompleter(completer)
- return editor
-#}}}
-
-class TagsDelegate(QStyledItemDelegate): # {{{
- def __init__(self, parent):
- QStyledItemDelegate.__init__(self, parent)
- self.db = None
-
- def set_database(self, db):
- self.db = db
-
- def createEditor(self, parent, option, index):
- if self.db:
- col = index.model().column_map[index.column()]
- if not index.model().is_custom_column(col):
- editor = CompleteLineEdit(parent, self.db.all_tags())
- else:
- editor = CompleteLineEdit(parent,
- sorted(list(self.db.all_custom(label=self.db.field_metadata.key_to_label(col))),
- key=sort_key))
- return editor
+ editor.update_items_cache(complete_items)
else:
editor = EnLineEdit(parent)
return editor
-# }}}
+#}}}
class CompleteDelegate(QStyledItemDelegate): # {{{
def __init__(self, parent, sep, items_func_name, space_before_sep=False):
@@ -197,13 +174,15 @@ class CompleteDelegate(QStyledItemDelegate): # {{{
def createEditor(self, parent, option, index):
if self.db and hasattr(self.db, self.items_func_name):
col = index.model().column_map[index.column()]
+ editor = MultiCompleteLineEdit(parent)
+ editor.set_separator(self.sep)
+ editor.set_space_before_sep(self.space_before_sep)
if not index.model().is_custom_column(col):
- editor = CompleteLineEdit(parent, getattr(self.db, self.items_func_name)(),
- self.sep, self.space_before_sep)
+ all_items = getattr(self.db, self.items_func_name)()
else:
- editor = CompleteLineEdit(parent,
- sorted(list(self.db.all_custom(label=self.db.field_metadata.key_to_label(col))),
- key=sort_key), self.sep, self.space_before_sep)
+ all_items = list(self.db.all_custom(
+ label=self.db.field_metadata.key_to_label(col)))
+ editor.update_items_cache(all_items)
else:
editor = EnLineEdit(parent)
return editor
@@ -273,13 +252,11 @@ class CcTextDelegate(QStyledItemDelegate): # {{{
editor.setRange(-100., float(sys.maxint))
editor.setDecimals(2)
else:
- editor = EnLineEdit(parent)
+ editor = MultiCompleteLineEdit(parent)
+ editor.set_separator(None)
complete_items = sorted(list(m.db.all_custom(label=m.db.field_metadata.key_to_label(col))),
key=sort_key)
- completer = QCompleter(complete_items, self)
- completer.setCaseSensitivity(Qt.CaseInsensitive)
- completer.setCompletionMode(QCompleter.PopupCompletion)
- editor.setCompleter(completer)
+ editor.update_items_cache(complete_items)
return editor
# }}}
diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py
index 590a8be3bb..6b89e306e6 100644
--- a/src/calibre/gui2/metadata/basic_widgets.py
+++ b/src/calibre/gui2/metadata/basic_widgets.py
@@ -12,8 +12,8 @@ from PyQt4.Qt import Qt, QDateEdit, QDate, \
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \
QPushButton, QSpinBox, QLineEdit
-from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
- EnComboBox, FormatList, ImageView, CompleteLineEdit
+from calibre.gui2.widgets import EnLineEdit, FormatList, ImageView
+from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks, prefs
from calibre.ebooks.metadata import title_sort, authors_to_string, \
@@ -149,14 +149,14 @@ class TitleSortEdit(TitleEdit):
# }}}
# Authors {{{
-class AuthorsEdit(CompleteComboBox):
+class AuthorsEdit(MultiCompleteComboBox):
TOOLTIP = ''
LABEL = _('&Author(s):')
def __init__(self, parent):
self.dialog = parent
- CompleteComboBox.__init__(self, parent)
+ MultiCompleteComboBox.__init__(self, parent)
self.setToolTip(self.TOOLTIP)
self.setWhatsThis(self.TOOLTIP)
self.setEditable(True)
@@ -283,13 +283,14 @@ class AuthorSortEdit(EnLineEdit):
# }}}
# Series {{{
-class SeriesEdit(EnComboBox):
+class SeriesEdit(MultiCompleteComboBox):
TOOLTIP = _('List of known series. You can add new series.')
LABEL = _('&Series:')
def __init__(self, parent):
- EnComboBox.__init__(self, parent)
+ MultiCompleteComboBox.__init__(self, parent)
+ self.set_separator(None)
self.dialog = parent
self.setSizeAdjustPolicy(
self.AdjustToMinimumContentsLengthWithIcon)
@@ -314,6 +315,7 @@ class SeriesEdit(EnComboBox):
def initialize(self, db, id_):
all_series = db.all_series()
all_series.sort(key=lambda x : sort_key(x[1]))
+ self.update_items_cache([x[1] for x in all_series])
series_id = db.series_id(id_, index_is_id=True)
idx, c = None, 0
for i in all_series:
@@ -472,6 +474,7 @@ class FormatsManager(QWidget): # {{{
def initialize(self, db, id_):
self.changed = False
exts = db.formats(id_, index_is_id=True)
+ self.original_val = set([])
if exts:
exts = exts.split(',')
for ext in exts:
@@ -482,6 +485,7 @@ class FormatsManager(QWidget): # {{{
if size is None:
continue
Format(self.formats, ext, size, timestamp=timestamp)
+ self.original_val.add(ext.lower())
def commit(self, db, id_):
if not self.changed:
@@ -500,11 +504,12 @@ class FormatsManager(QWidget): # {{{
for ext in new_extensions:
db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False,
index_is_id=True)
- db_extensions = set([f.lower() for f in db.formats(id_,
- index_is_id=True).split(',')])
+ dbfmts = db.formats(id_, index_is_id=True)
+ db_extensions = set([f.lower() for f in (dbfmts.split(',') if dbfmts
+ else [])])
extensions = new_extensions.union(old_extensions)
for ext in db_extensions:
- if ext not in extensions:
+ if ext not in extensions and ext in self.original_val:
db.remove_format(id_, ext, notify=False, index_is_id=True)
self.changed = False
@@ -811,14 +816,14 @@ class RatingEdit(QSpinBox): # {{{
# }}}
-class TagsEdit(CompleteLineEdit): # {{{
+class TagsEdit(MultiCompleteLineEdit): # {{{
LABEL = _('Ta&gs:')
TOOLTIP = '
'+_('Tags categorize the book. This is particularly '
'useful while searching.
They can be any words'
'or phrases, separated by commas.')
def __init__(self, parent):
- CompleteLineEdit.__init__(self, parent)
+ MultiCompleteLineEdit.__init__(self, parent)
self.setToolTip(self.TOOLTIP)
self.setWhatsThis(self.TOOLTIP)
@@ -836,7 +841,7 @@ class TagsEdit(CompleteLineEdit): # {{{
tags = db.tags(id_, index_is_id=True)
tags = tags.split(',') if tags else []
self.current_val = tags
- self.update_items_cache(db.all_tags())
+ self.all_items = db.all_tags()
self.original_val = self.current_val
@property
@@ -857,7 +862,7 @@ class TagsEdit(CompleteLineEdit): # {{{
d = TagEditor(self, db, id_)
if d.exec_() == TagEditor.Accepted:
self.current_val = d.tags
- self.update_items_cache(db.all_tags())
+ self.all_items = db.all_tags()
def commit(self, db, id_):
@@ -907,11 +912,12 @@ class ISBNEdit(QLineEdit): # {{{
# }}}
-class PublisherEdit(EnComboBox): # {{{
+class PublisherEdit(MultiCompleteComboBox): # {{{
LABEL = _('&Publisher:')
def __init__(self, parent):
- EnComboBox.__init__(self, parent)
+ MultiCompleteComboBox.__init__(self, parent)
+ self.set_separator(None)
self.setSizeAdjustPolicy(
self.AdjustToMinimumContentsLengthWithIcon)
@@ -932,6 +938,7 @@ class PublisherEdit(EnComboBox): # {{{
def initialize(self, db, id_):
all_publishers = db.all_publishers()
all_publishers.sort(key=lambda x : sort_key(x[1]))
+ self.update_items_cache([x[1] for x in all_publishers])
publisher_id = db.publisher_id(id_, index_is_id=True)
idx, c = None, 0
for i in all_publishers:
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 3fc16e99b4..bfe54df36e 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -430,8 +430,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
authors = self.authors(id, index_is_id=True)
if not authors:
authors = _('Unknown')
- author = ascii_filename(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding, 'ignore')
- title = ascii_filename(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding, 'ignore')
+ author = ascii_filename(authors.split(',')[0])[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
+ title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
path = author + '/' + title + ' (%d)'%id
return path
@@ -442,8 +442,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
authors = self.authors(id, index_is_id=True)
if not authors:
authors = _('Unknown')
- author = ascii_filename(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace')
- title = ascii_filename(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding, 'replace')
+ author = ascii_filename(authors.split(',')[0])[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
+ title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
name = title + ' - ' + author
while name.endswith('.'):
name = name[:-1]
diff --git a/src/calibre/manual/conversion.rst b/src/calibre/manual/conversion.rst
index 7f3ff21fe0..60f8a10fc6 100644
--- a/src/calibre/manual/conversion.rst
+++ b/src/calibre/manual/conversion.rst
@@ -311,10 +311,25 @@ remove all non-breaking-space entities, or may include false positive matches re
:guilabel:`Ensure scene breaks are consistently formatted`
With this option |app| will attempt to detect common scene-break markers and ensure that they are center aligned.
- It also attempts to detect scene breaks defined by white space and replace them with a horizontal rule 15% of the
- page width. Some readers may find this desirable as these 'soft' scene breaks often become page breaks on readers, and
- thus become difficult to distinguish.
+ 'Soft' scene break markers, i.e. scene breaks only defined by extra white space, are styled to ensure that they
+ will not be displayed in conjunction with page breaks.
+:guilabel:`Replace scene breaks`
+ If this option is configured then |app| will replace scene break markers it finds with the replacement text specified by the
+ user. Please note that some ornamental characters may not be supported across all reading devices.
+
+ In general you should avoid using html tags, |app| will discard any tags and use pre-defined markup.
+ tags, i.e. horizontal rules, and
tags are exceptions. Horizontal rules can optionally be specified with styles, if you
+ choose to add your own style be sure to include the 'width' setting, otherwise the style information will be discarded. Image
+ tags can used, but |app| does not provide the ability to add the image during conversion, this must be done after the fact using
+ the 'Tweak Epub' feature, or Sigil.
+
+ Example image tag (place the image within an 'Images' folder inside the epub after conversion):
+
+
+ Example horizontal rule with styles:
+
+
:guilabel:`Remove unnecessary hyphens`
|app| will analyze all hyphenated content in the document when this option is enabled. The document itself is used
as a dictionary for analysis. This allows |app| to accurately remove hyphens for any words in the document in any language,
@@ -628,7 +643,7 @@ between 0 and 1. The default is 0.45, just under the median line length. Lower t
text in the unwrapping. Increase to include less. You can adjust this value in the conversion settings under :guilabel:`PDF Input`.
Also, they often have headers and footers as part of the document that will become included with the text.
-Use the options to remove headers and footers to mitigate this issue. If the headers and footers are not
+Use the Search and Replace panel to remove headers and footers to mitigate this issue. If the headers and footers are not
removed from the text it can throw off the paragraph unwrapping. To learn how to use the header and footer removal options, read
:ref:`regexptutorial`.
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index 849ded82c9..18c53ade5d 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -391,6 +391,8 @@ Take your pick:
* A tribute to the SONY Librie which was the first e-ink based e-book reader
* My wife chose it ;-)
+|app| is pronounced as cal-i-ber *not* ca-li-bre. If you're wondering, |app| is the British/commonwealth spelling for caliber. Being Indian, that's the natural spelling for me.
+
Why does |app| show only some of my fonts on OS X?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|app| embeds fonts in ebook files it creates. E-book files support embedding only TrueType (.ttf) fonts. Most fonts on OS X systems are in .dfont format, thus they cannot be embedded. |app| shows only TrueType fonts found on your system. You can obtain many TrueType fonts on the web. Simply download the .ttf files and add them to the Library/Fonts directory in your home directory.
diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py
index 2e5852df89..518f2ed140 100644
--- a/src/calibre/utils/formatter_functions.py
+++ b/src/calibre/utils/formatter_functions.py
@@ -186,7 +186,7 @@ class BuiltinTemplate(BuiltinFormatterFunction):
def evaluate(self, formatter, kwargs, mi, locals, template):
template = template.replace('[[', '{').replace(']]', '}')
- return formatter.safe_format(template, kwargs, 'TEMPLATE', mi)
+ return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)
class BuiltinEval(BuiltinFormatterFunction):
name = 'eval'
diff --git a/src/calibre/utils/magick/draw.py b/src/calibre/utils/magick/draw.py
index ad4b681b43..04cce5efe3 100644
--- a/src/calibre/utils/magick/draw.py
+++ b/src/calibre/utils/magick/draw.py
@@ -72,11 +72,17 @@ def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None,
f.write(data)
return ret
-def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg'):
+def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg',
+ preserve_aspect_ratio=True):
img = Image()
img.load(data)
owidth, oheight = img.size
- scaled, nwidth, nheight = fit_image(owidth, oheight, width, height)
+ if not preserve_aspect_ratio:
+ scaled = owidth > width or oheight > height
+ nwidth = width
+ nheight = height
+ else:
+ scaled, nwidth, nheight = fit_image(owidth, oheight, width, height)
if scaled:
img.size = (nwidth, nheight)
canvas = create_canvas(img.size[0], img.size[1], bgcolor)