This commit is contained in:
Sengian 2011-02-02 23:20:56 +01:00
commit 9c91892d04
46 changed files with 1286 additions and 243 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

View File

@ -585,7 +585,6 @@ application/vnd.osa.netdeploy
application/vnd.osgi.bundle application/vnd.osgi.bundle
application/vnd.osgi.dp dp application/vnd.osgi.dp dp
application/vnd.otps.ct-kip+xml application/vnd.otps.ct-kip+xml
application/vnd.palm oprc pdb pqa
application/vnd.paos.xml application/vnd.paos.xml
application/vnd.pg.format str application/vnd.pg.format str
application/vnd.pg.osasli ei6 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-ascii ent prt
chemical/x-ncbi-asn1-binary aso val chemical/x-ncbi-asn1-binary aso val
chemical/x-ncbi-asn1-spec asn chemical/x-ncbi-asn1-spec asn
chemical/x-pdb ent pdb
chemical/x-rosdal ros chemical/x-rosdal ros
chemical/x-swissprot sw chemical/x-swissprot sw
chemical/x-vamas-iso14976 vms chemical/x-vamas-iso14976 vms
@ -1379,3 +1377,5 @@ application/x-cbr cbr
application/x-cb7 cb7 application/x-cb7 cb7
application/x-koboreader-ebook kobo application/x-koboreader-ebook kobo
image/wmf wmf image/wmf wmf
application/ereader pdb

View File

@ -1,25 +1,25 @@
# -*- coding: utf-8
__license__ = 'GPL v3' __license__ = 'GPL v3'
__author__ = 'Luis Hernandez' __author__ = 'Luis Hernandez'
__copyright__ = 'Luis Hernandez<tolyluis@gmail.com>' __copyright__ = 'Luis Hernandez<tolyluis@gmail.com>'
description = 'Periódico gratuito en español - v0.8 - 27 Jan 2011' __version__ = 'v0.85'
__date__ = '31 January 2011'
''' '''
www.20minutos.es www.20minutos.es
''' '''
import re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1294946868(BasicNewsRecipe): class AdvancedUserRecipe1294946868(BasicNewsRecipe):
title = u'20 Minutos' title = u'20 Minutos new'
publisher = u'Grupo 20 Minutos' publisher = u'Grupo 20 Minutos'
__author__ = 'Luis Hernández' __author__ = 'Luis Hernandez'
description = 'Periódico gratuito en español' description = 'Free spanish newspaper'
cover_url = 'http://estaticos.20minutos.es/mmedia/especiales/corporativo/css/img/logotipos_grupo20minutos.gif' 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 max_articles_per_feed = 100
remove_javascript = True remove_javascript = True
@ -29,6 +29,7 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
encoding = 'ISO-8859-1' encoding = 'ISO-8859-1'
language = 'es' language = 'es'
timefmt = '[%a, %d %b, %Y]' timefmt = '[%a, %d %b, %Y]'
remove_empty_feeds = True
keep_only_tags = [ keep_only_tags = [
dict(name='div', attrs={'id':['content','vinetas',]}) dict(name='div', attrs={'id':['content','vinetas',]})
@ -43,13 +44,21 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
remove_tags = [ remove_tags = [
dict(name='ol', attrs={'class':['navigation',]}) dict(name='ol', attrs={'class':['navigation',]})
,dict(name='span', attrs={'class':['action']}) ,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='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={'class':['article-user-actions','stripped-list']})
,dict(name='ul', attrs={'id':['site-links']}) ,dict(name='ul', attrs={'id':['site-links']})
,dict(name='li', attrs={'class':['puntuacion','enviar','compartir']}) ,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'<a href="http://estaticos.*?[0-999]px;" target="_blank">', re.DOTALL), lambda m: '')]
feeds = [ feeds = [
(u'Portada' , u'http://www.20minutos.es/rss/') (u'Portada' , u'http://www.20minutos.es/rss/')
,(u'Nacional' , u'http://www.20minutos.es/rss/nacional/') ,(u'Nacional' , u'http://www.20minutos.es/rss/nacional/')

View File

@ -0,0 +1,71 @@
__license__ = 'GPL v3'
__author__ = 'Luis Hernandez'
__copyright__ = 'Luis Hernandez<tolyluis@gmail.com>'
__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')
]

View File

@ -15,12 +15,26 @@ class LeTemps(BasicNewsRecipe):
oldest_article = 7 oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 100
__author__ = 'Sujata Raman' __author__ = 'Sujata Raman'
description = 'French news. Needs a subscription from http://www.letemps.ch'
no_stylesheets = True no_stylesheets = True
remove_javascript = True remove_javascript = True
recursions = 1 recursions = 1
encoding = 'UTF-8' encoding = 'UTF-8'
match_regexps = [r'http://www.letemps.ch/Page/Uuid/[-0-9a-f]+\|[1-9]'] match_regexps = [r'http://www.letemps.ch/Page/Uuid/[-0-9a-f]+\|[1-9]']
language = 'fr' 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'}), keep_only_tags = [dict(name='div', attrs={'id':'content'}),
dict(name='div', attrs={'class':'story'}) dict(name='div', attrs={'class':'story'})

View File

@ -35,7 +35,7 @@ class WallStreetJournal(BasicNewsRecipe):
remove_tags_before = dict(name='h1') remove_tags_before = dict(name='h1')
remove_tags = [ 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"]}, {'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'), dict(rel='shortcut icon'),
] ]
@ -101,7 +101,7 @@ class WallStreetJournal(BasicNewsRecipe):
title = 'Front Section' title = 'Front Section'
url = 'http://online.wsj.com' + a['href'] url = 'http://online.wsj.com' + a['href']
feeds = self.wsj_add_feed(feeds,title,url) feeds = self.wsj_add_feed(feeds,title,url)
title = 'What''s News' title = "What's News"
url = url.replace('pageone','whatsnews') url = url.replace('pageone','whatsnews')
feeds = self.wsj_add_feed(feeds,title,url) feeds = self.wsj_add_feed(feeds,title,url)
else: else:

View File

@ -10,7 +10,10 @@ class WallStreetJournal(BasicNewsRecipe):
title = 'Wall Street Journal (free)' title = 'Wall Street Journal (free)'
__author__ = 'Kovid Goyal, Sujata Raman, Joshua Oster-Morris, Starson17' __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' language = 'en'
cover_url = 'http://dealbreaker.com/images/thumbs/Wall%20Street%20Journal%20A1.JPG' cover_url = 'http://dealbreaker.com/images/thumbs/Wall%20Street%20Journal%20A1.JPG'
max_articles_per_feed = 1000 max_articles_per_feed = 1000
@ -151,6 +154,4 @@ class WallStreetJournal(BasicNewsRecipe):
return articles return articles
def cleanup(self):
self.browser.open('http://online.wsj.com/logout?url=http://online.wsj.com')

View File

@ -22,13 +22,15 @@ Run an embedded python interpreter.
parser.add_option('-d', '--debug-device-driver', default=False, action='store_true', parser.add_option('-d', '--debug-device-driver', default=False, action='store_true',
help='Debug the specified device driver.') help='Debug the specified device driver.')
parser.add_option('-g', '--gui', default=False, action='store_true', 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, parser.add_option('--gui-debug', default=None,
help='Run the GUI with a debug console, logging to the' 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, 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', parser.add_option('-w', '--viewer', default=False, action='store_true',
help='Run the ebook viewer',) help='Run the ebook viewer',)
parser.add_option('--paths', default=False, action='store_true', parser.add_option('--paths', default=False, action='store_true',

View File

@ -35,6 +35,16 @@ class DevicePlugin(Plugin):
#: Height for thumbnails on the device #: Height for thumbnails on the device
THUMBNAIL_HEIGHT = 68 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. #: Whether the metadata on books can be set via the GUI.
CAN_SET_METADATA = ['title', 'authors', 'collections'] CAN_SET_METADATA = ['title', 'authors', 'collections']

View File

@ -8,5 +8,5 @@ CACHE_XML = 'Sony Reader/database/cache.xml'
CACHE_EXT = 'Sony Reader/database/cacheExt.xml' CACHE_EXT = 'Sony Reader/database/cacheExt.xml'
MEDIA_THUMBNAIL = 'database/thumbnail' MEDIA_THUMBNAIL = 'database/thumbnail'
CACHE_THUMBNAIL = 'Sony Reader/database/thumbnail' CACHE_THUMBNAIL = 'Sony Reader/thumbnail'

View File

@ -81,12 +81,19 @@ class PRS505(USBMS):
_('Set this option to have separate book covers uploaded ' _('Set this option to have separate book covers uploaded '
'every time you connect your device. Unset this option if ' 'every time you connect your device. Unset this option if '
'you have so many books on the reader that performance is ' '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 = [ EXTRA_CUSTOMIZATION_DEFAULT = [
', '.join(['series', 'tags']), ', '.join(['series', 'tags']),
False, False,
False False,
True
] ]
OPT_COLLECTIONS = 0 OPT_COLLECTIONS = 0
@ -96,7 +103,7 @@ class PRS505(USBMS):
plugboard = None plugboard = None
plugboard_func = None plugboard_func = None
THUMBNAIL_HEIGHT = 200 THUMBNAIL_HEIGHT = 217
MAX_PATH_LEN = 201 # 250 - (max(len(CACHE_THUMBNAIL), len(MEDIA_THUMBNAIL)) + MAX_PATH_LEN = 201 # 250 - (max(len(CACHE_THUMBNAIL), len(MEDIA_THUMBNAIL)) +
# len('main_thumbnail.jpg') + 1) # len('main_thumbnail.jpg') + 1)
@ -138,6 +145,13 @@ class PRS505(USBMS):
if not write_cache(self._card_b_prefix): if not write_cache(self._card_b_prefix):
self._card_b_prefix = None self._card_b_prefix = None
self.booklist_class.rebuild_collections = self.rebuild_collections 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): def get_device_information(self, end_session=True):
return (self.gui_name, '', '', '') return (self.gui_name, '', '', '')

View File

@ -139,6 +139,13 @@ class CHMReader(CHMFile):
if self.hhc_path not in files and files: if self.hhc_path not in files and files:
self.hhc_path = files[0] 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): def _reformat(self, data, htmlpath):
try: try:
data = xml_to_unicode(data, strip_encoding_pats=True)[0] data = xml_to_unicode(data, strip_encoding_pats=True)[0]

View File

@ -46,7 +46,8 @@ HEURISTIC_OPTIONS = ['markup_chapter_headings',
'italicize_common_cases', 'fix_indents', 'italicize_common_cases', 'fix_indents',
'html_unwrap_factor', 'unwrap_lines', 'html_unwrap_factor', 'unwrap_lines',
'delete_blank_paragraphs', 'format_scene_breaks', 'delete_blank_paragraphs', 'format_scene_breaks',
'dehyphenate', 'renumber_headings'] 'dehyphenate', 'renumber_headings',
'replace_scene_breaks']
def print_help(parser, log): def print_help(parser, log):
help = parser.format_help().encode(preferred_encoding, 'replace') help = parser.format_help().encode(preferred_encoding, 'replace')

View File

@ -531,6 +531,11 @@ OptionRecommendation(name='format_scene_breaks',
'Replace soft scene breaks that use multiple blank lines with' 'Replace soft scene breaks that use multiple blank lines with'
'horizontal rules.')), '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', OptionRecommendation(name='dehyphenate',
recommended_value=True, level=OptionRecommendation.LOW, recommended_value=True, level=OptionRecommendation.LOW,
help=_('Analyze hyphenated words throughout the document. The ' help=_('Analyze hyphenated words throughout the document. The '

View File

@ -26,9 +26,14 @@ class HeuristicProcessor(object):
self.blanks_deleted = False self.blanks_deleted = False
self.blanks_between_paragraphs = False self.blanks_between_paragraphs = False
self.linereg = re.compile('(?<=<p).*?(?=</p>)', re.IGNORECASE|re.DOTALL) self.linereg = re.compile('(?<=<p).*?(?=</p>)', re.IGNORECASE|re.DOTALL)
self.blankreg = re.compile(r'\s*(?P<openline><p(?!\sclass=\"(softbreak|spacer)\")[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE) self.blankreg = re.compile(r'\s*(?P<openline><p(?!\sclass=\"(softbreak|whitespace)\")[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE)
self.anyblank = re.compile(r'\s*(?P<openline><p[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE) self.anyblank = re.compile(r'\s*(?P<openline><p[^>]*>)\s*(?P<closeline></p>)', re.IGNORECASE)
self.multi_blank = re.compile(r'(\s*<p[^>]*>\s*</p>){2,}(?!\s*<h\d)', re.IGNORECASE) self.multi_blank = re.compile(r'(\s*<p[^>]*>\s*</p>){2,}(?!\s*<h\d)', re.IGNORECASE)
self.any_multi_blank = re.compile(r'(\s*<p[^>]*>\s*</p>){2,}', re.IGNORECASE)
self.line_open = "<(?P<outer>p|div)[^>]*>\s*(<(?P<inner1>font|span|[ibu])[^>]*>)?\s*(<(?P<inner2>font|span|[ibu])[^>]*>)?\s*(<(?P<inner3>font|span|[ibu])[^>]*>)?\s*"
self.line_close = "(</(?P=inner3)>)?\s*(</(?P=inner2)>)?\s*(</(?P=inner1)>)?\s*</(?P=outer)>"
self.single_blank = re.compile(r'(\s*<p[^>]*>\s*</p>)', re.IGNORECASE)
self.scene_break_open = '<p class="scenebreak" style="text-align:center; text-indent:0%; margin-top:1em; margin-bottom:1em; page-break-before:avoid">'
def is_pdftohtml(self, src): def is_pdftohtml(self, src):
return '<!-- created by calibre\'s pdftohtml -->' in src[:1000] return '<!-- created by calibre\'s pdftohtml -->' in src[:1000]
@ -187,19 +192,17 @@ class HeuristicProcessor(object):
# Build the Regular Expressions in pieces # Build the Regular Expressions in pieces
init_lookahead = "(?=<(p|div))" init_lookahead = "(?=<(p|div))"
chapter_line_open = "<(?P<outer>p|div)[^>]*>\s*(<(?P<inner1>font|span|[ibu])[^>]*>)?\s*(<(?P<inner2>font|span|[ibu])[^>]*>)?\s*(<(?P<inner3>font|span|[ibu])[^>]*>)?\s*" chapter_line_open = self.line_open
title_line_open = "<(?P<outer2>p|div)[^>]*>\s*(<(?P<inner4>font|span|[ibu])[^>]*>)?\s*(<(?P<inner5>font|span|[ibu])[^>]*>)?\s*(<(?P<inner6>font|span|[ibu])[^>]*>)?\s*" title_line_open = "<(?P<outer2>p|div)[^>]*>\s*(<(?P<inner4>font|span|[ibu])[^>]*>)?\s*(<(?P<inner5>font|span|[ibu])[^>]*>)?\s*(<(?P<inner6>font|span|[ibu])[^>]*>)?\s*"
chapter_header_open = r"(?P<chap>" chapter_header_open = r"(?P<chap>"
title_header_open = r"(?P<title>" title_header_open = r"(?P<title>"
chapter_header_close = ")\s*" chapter_header_close = ")\s*"
title_header_close = ")" 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)>" title_line_close = "(</(?P=inner6)>)?\s*(</(?P=inner5)>)?\s*(</(?P=inner4)>)?\s*</(?P=outer2)>"
is_pdftohtml = self.is_pdftohtml(html) is_pdftohtml = self.is_pdftohtml(html)
if is_pdftohtml: if is_pdftohtml:
chapter_line_open = "<(?P<outer>p)[^>]*>(\s*<[ibu][^>]*>)?\s*"
chapter_line_close = "\s*(</[ibu][^>]*>\s*)?</(?P=outer)>"
title_line_open = "<(?P<outer2>p)[^>]*>\s*" title_line_open = "<(?P<outer2>p)[^>]*>\s*"
title_line_close = "\s*</(?P=outer2)>" title_line_close = "\s*</(?P=outer2)>"
@ -374,13 +377,15 @@ class HeuristicProcessor(object):
html = re.sub(ur'\s*<o:p>\s*</o:p>', ' ', html) html = re.sub(ur'\s*<o:p>\s*</o:p>', ' ', html)
# Delete microsoft 'smart' tags # Delete microsoft 'smart' tags
html = re.sub('(?i)</?st1:\w+>', '', html) html = re.sub('(?i)</?st1:\w+>', '', html)
# Delete self closing paragraph tags # Re-open self closing paragraph tags
html = re.sub('<p\s?/>', '', html) html = re.sub('<p[^>/]*/>', '<p> </p>', html)
# Get rid of empty span, bold, font, em, & italics tags # Get rid of empty span, bold, font, em, & italics tags
html = re.sub(r"\s*<span[^>]*>\s*(<span[^>]*>\s*</span>){0,2}\s*</span>\s*", " ", html) html = re.sub(r"\s*<span[^>]*>\s*(<span[^>]*>\s*</span>){0,2}\s*</span>\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*<(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*<span[^>]*>\s*(<span[^>]>\s*</span>){0,2}\s*</span>\s*", " ", html) html = re.sub(r"\s*<span[^>]*>\s*(<span[^>]>\s*</span>){0,2}\s*</span>\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*<(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)<h\d+>\s*</h\d+>', '', html)
self.deleted_nbsps = True self.deleted_nbsps = True
return html return html
@ -419,32 +424,99 @@ class HeuristicProcessor(object):
return True return True
return False return False
def detect_blank_formatting(self, html): def merge_blanks(self, html, blanks_count=None):
blanks_before_headings = re.compile(r'(\s*<p[^>]*>\s*</p>){1,}(?=\s*<h\d)', re.IGNORECASE) base_em = .5 # Baseline is 1.5em per blank line, 1st line is .5 em css and 1em for the nbsp
blanks_after_headings = re.compile(r'(?<=</h\d>)(\s*<p[^>]*>\s*</p>){1,}', re.IGNORECASE) em_per_line = 1.5 # Add another 1.5 em for each additional blank
def markup_spacers(match): 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<p class="whitespace'+str(int(em * 10))+'" style="text-align:center; margin-top:'+str(em)+'em"> </p>', match.group(0))
else:
newline = self.any_multi_blank.sub('\n<p class="softbreak'+str(int(em * 10))+'" style="text-align:center; margin-top:'+str(em)+'em"> </p>', 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<initparas>(<p[^>]*>\s*</p>\s*){1,}\s*)?(?P<heading><h(?P<hnum>\d+)[^>]*>.*?</h(?P=hnum)>)(?P<endparas>\s*(<p[^>]*>\s*</p>\s*){1,})?', re.IGNORECASE)
blanks_n_nopunct = re.compile(r'(?P<initparas>(<p[^>]*>\s*</p>\s*){1,}\s*)?<p[^>]*>\s*(<(span|[ibu]|em|strong|font)[^>]*>\s*)*.{1,100}?[^\W](</(span|[ibu]|em|strong|font)>\s*)*</p>(?P<endparas>\s*(<p[^>]*>\s*</p>\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)<h(?P<hnum>\d+)[^>]*>', '\n\n<h'+'\g<hnum>'+' 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 = match.group(0)
blanks = self.blankreg.sub('\n<p class="spacer"> </p>', blanks) blanks = self.blankreg.sub('\n<p class="whitespace" style="text-align:center; margin-top:0em; margin-bottom:0em"> </p>', blanks)
return blanks return blanks
html = blanks_before_headings.sub(markup_spacers, html)
html = blanks_after_headings.sub(markup_spacers, html) html = blanks_n_nopunct.sub(markup_whitespaces, html)
if self.html_preprocess_sections > self.min_chapters: if self.html_preprocess_sections > self.min_chapters:
html = re.sub('(?si)^.*?(?=<h\d)', markup_spacers, html) html = re.sub('(?si)^.*?(?=<h\d)', markup_whitespaces, html)
return html return html
def detect_soft_breaks(self, html): def detect_soft_breaks(self, html):
if not self.blanks_deleted and self.blanks_between_paragraphs: if not self.blanks_deleted and self.blanks_between_paragraphs:
html = self.multi_blank.sub('\n<p class="softbreak" style="margin-top:1.25em; margin-bottom:1.25em; page-break-before:avoid"> </p>', html) html = self.multi_blank.sub('\n<p class="softbreak" style="margin-top:1em; page-break-before:avoid; text-align:center"> </p>', html)
else: else:
html = self.blankreg.sub('\n<p class="softbreak" style="margin-top:1.25em; margin-bottom:1.25em; page-break-before:avoid"> </p>', html) html = self.blankreg.sub('\n<p class="softbreak" style="margin-top:.5em; page-break-before:avoid; text-align:center"> </p>', html)
return 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. <hr> and <img> tags are allowed. If the user specifies
a style with width attributes in the <hr> 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 = '<div id="scenebreak" style="margin-left: 45%; margin-right: 45%; margin-top:1.5em; margin-bottom:1.5em; page-break-before:avoid">'
if re.findall('(<|>)', replacement_break):
if re.match('^<hr', replacement_break):
if replacement_break.find('width') != -1:
width = int(re.sub('.*?width(:|=)(?P<wnum>\d+).*', '\g<wnum>', 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+'</div>'
else:
scene_break = hr_open+'<hr style="height: 3px; background:#505050" /></div>'
elif re.match('^<img', replacement_break):
scene_break = self.scene_break_open+replacement_break+'</p>'
else:
from calibre.utils.html2text import html2text
replacement_break = html2text(replacement_break)
replacement_break = re.sub('\s', '&nbsp;', replacement_break)
scene_break = self.scene_break_open+replacement_break+'</p>'
else:
replacement_break = re.sub('\s', '&nbsp;', replacement_break)
scene_break = self.scene_break_open+replacement_break+'</p>'
return scene_break
def __call__(self, html): def __call__(self, html):
self.log.debug("********* Heuristic processing HTML *********") self.log.debug("********* Heuristic processing HTML *********")
# Count the words in the document to estimate how many chapters to look for and whether # Count the words in the document to estimate how many chapters to look for and whether
# other types of processing are attempted # other types of processing are attempted
try: try:
@ -458,7 +530,7 @@ class HeuristicProcessor(object):
# Arrange line feeds and </p> tags so the line_length and no_markup functions work correctly # Arrange line feeds and </p> tags so the line_length and no_markup functions work correctly
html = self.arrange_htm_line_endings(html) html = self.arrange_htm_line_endings(html)
#self.dump(html, 'after_arrange_line_endings')
if self.cleanup_required(): if self.cleanup_required():
###### Check Markup ###### ###### Check Markup ######
# #
@ -478,6 +550,11 @@ class HeuristicProcessor(object):
# fix indents must run before this step, as it removes non-breaking spaces # fix indents must run before this step, as it removes non-breaking spaces
html = self.cleanup_markup(html) html = self.cleanup_markup(html)
is_pdftohtml = self.is_pdftohtml(html)
if is_pdftohtml:
self.line_open = "<(?P<outer>p)[^>]*>(\s*<[ibu][^>]*>)?\s*"
self.line_close = "\s*(</[ibu][^>]*>\s*)?</(?P=outer)>"
# ADE doesn't render <br />, change to empty paragraphs # ADE doesn't render <br />, change to empty paragraphs
#html = re.sub('<br[^>]*>', u'<p>\u00a0</p>', html) #html = re.sub('<br[^>]*>', u'<p>\u00a0</p>', html)
@ -489,6 +566,7 @@ class HeuristicProcessor(object):
if getattr(self.extra_opts, 'markup_chapter_headings', False): if getattr(self.extra_opts, 'markup_chapter_headings', False):
html = self.markup_chapters(html, self.totalwords, self.blanks_between_paragraphs) 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): if getattr(self.extra_opts, 'italicize_common_cases', False):
html = self.markup_italicis(html) 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): if self.blanks_between_paragraphs and getattr(self.extra_opts, 'delete_blank_paragraphs', False):
self.log.debug("deleting blank lines") self.log.debug("deleting blank lines")
self.blanks_deleted = True self.blanks_deleted = True
html = self.multi_blank.sub('\n<p class="softbreak" style="margin-top:1.25em; margin-bottom:1.25em; page-break-before:avoid"> </p>', html) html = self.multi_blank.sub('\n<p class="softbreak" style="margin-top:.5em; page-break-before:avoid; text-align:center"> </p>', html)
html = self.blankreg.sub('', html) html = self.blankreg.sub('', html)
# Determine line ending type # 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): 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," self.log.debug("Looking for more split points based on punctuation,"
" currently have " + unicode(self.html_preprocess_sections)) " currently have " + unicode(self.html_preprocess_sections))
chapdetect3 = re.compile(r'<(?P<styles>(p|div)[^>]*)>\s*(?P<section>(<span[^>]*>)?\s*(?!([*#•]+\s*)+)(<[ibu][^>]*>){0,2}\s*(<span[^>]*>)?\s*(<[ibu][^>]*>){0,2}\s*(<span[^>]*>)?\s*.?(?=[a-z#\-*\s]+<)([a-z#-*]+\s*){1,5}\s*\s*(</span>)?(</[ibu]>){0,2}\s*(</span>)?\s*(</[ibu]>){0,2}\s*(</span>)?\s*</(p|div)>)', re.IGNORECASE) chapdetect3 = re.compile(r'<(?P<styles>(p|div)[^>]*)>\s*(?P<section>(<span[^>]*>)?\s*(?!([\W]+\s*)+)(<[ibu][^>]*>){0,2}\s*(<span[^>]*>)?\s*(<[ibu][^>]*>){0,2}\s*(<span[^>]*>)?\s*.?(?=[a-z#\-*\s]+<)([a-z#-*]+\s*){1,5}\s*\s*(</span>)?(</[ibu]>){0,2}\s*(</span>)?\s*(</[ibu]>){0,2}\s*(</span>)?\s*</(p|div)>)', re.IGNORECASE)
html = chapdetect3.sub(self.chapter_break, html) html = chapdetect3.sub(self.chapter_break, html)
if getattr(self.extra_opts, 'renumber_headings', False): if getattr(self.extra_opts, 'renumber_headings', False):
@ -549,14 +627,32 @@ class HeuristicProcessor(object):
doubleheading = re.compile(r'(?P<firsthead><h(1|2)[^>]*>.+?</h(1|2)>\s*(<(?!h\d)[^>]*>\s*)*)<h(1|2)(?P<secondhead>[^>]*>.+?)</h(1|2)>', re.IGNORECASE) doubleheading = re.compile(r'(?P<firsthead><h(1|2)[^>]*>.+?</h(1|2)>\s*(<(?!h\d)[^>]*>\s*)*)<h(1|2)(?P<secondhead>[^>]*>.+?)</h(1|2)>', re.IGNORECASE)
html = doubleheading.sub('\g<firsthead>'+'\n<h3'+'\g<secondhead>'+'</h3>', html) html = doubleheading.sub('\g<firsthead>'+'\n<h3'+'\g<secondhead>'+'</h3>', 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): 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) html = self.detect_soft_breaks(html)
# Center separator lines blanks_count = len(self.any_multi_blank.findall(html))
html = re.sub(u'<(?P<outer>p|div)[^>]*>\s*(<(?P<inner1>font|span|[ibu])[^>]*>)?\s*(<(?P<inner2>font|span|[ibu])[^>]*>)?\s*(<(?P<inner3>font|span|[ibu])[^>]*>)?\s*(?P<break>([*#•=✦]+\s*)+)\s*(</(?P=inner3)>)?\s*(</(?P=inner2)>)?\s*(</(?P=inner1)>)?\s*</(?P=outer)>', '<p style="text-align:center; margin-top:1.25em; margin-bottom:1.25em; page-break-before:avoid">' + '\g<break>' + '</p>', html) if blanks_count >= 1:
#html = re.sub('<p\s+class="softbreak"[^>]*>\s*</p>', '<div id="softbreak" style="margin-left: 45%; margin-right: 45%; margin-top:1.5em; margin-bottom:1.5em"><hr style="height: 3px; background:#505050" /></div>', html) html = self.merge_blanks(html, blanks_count)
scene_break_regex = self.line_open+'(?![\w\'\"])(?P<break>((?P<break_char>((?!\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('<p\s+class="softbreak"[^>]*>\s*</p>', replacement_break, html)
else:
html = scene_break.sub(self.scene_break_open+'\g<break>'+'</p>', html)
if self.deleted_nbsps: 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<openline>'+u'\u00a0'+r'\g<closeline>', html) html = self.anyblank.sub('\n'+r'\g<openline>'+u'\u00a0'+r'\g<closeline>', html)
return html return html

View File

@ -175,6 +175,19 @@ class EPUBInput(InputFormatPlugin):
raise ValueError( raise ValueError(
'EPUB files with DTBook markup are not supported') '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: with open('content.opf', 'wb') as nopf:
nopf.write(opf.render()) nopf.write(opf.render())

View File

@ -422,6 +422,33 @@ class MetadataField(object):
elem = obj.create_metadata_element(self.name, is_dc=self.is_dc) elem = obj.create_metadata_element(self.name, is_dc=self.is_dc)
obj.set_text(elem, self.renderer(val)) 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)): def serialize_user_metadata(metadata_elem, all_user_metadata, tail='\n'+(' '*8)):
from calibre.utils.config import to_json from calibre.utils.config import to_json
@ -490,6 +517,7 @@ class OPF(object): # {{{
rights = MetadataField('rights') rights = MetadataField('rights')
series = MetadataField('series', is_dc=False) series = MetadataField('series', is_dc=False)
series_index = MetadataField('series_index', is_dc=False, formatter=float, none_is=1) 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) rating = MetadataField('rating', is_dc=False, formatter=int)
pubdate = MetadataField('date', formatter=parse_date, pubdate = MetadataField('date', formatter=parse_date,
renderer=isoformat) renderer=isoformat)
@ -776,30 +804,6 @@ class OPF(object): # {{{
return property(fget=fget, fset=fset) 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 @dynamic_property
def tags(self): def tags(self):
@ -1129,8 +1133,6 @@ class OPFCreator(Metadata):
metadata = M.metadata() metadata = M.metadata()
a = metadata.append a = metadata.append
role = {} role = {}
if self.title_sort:
role = {'file-as':self.title_sort}
a(DC_ELEM('title', self.title if self.title else _('Unknown'), a(DC_ELEM('title', self.title if self.title else _('Unknown'),
opf_attrs=role)) opf_attrs=role))
for i, author in enumerate(self.authors): for i, author in enumerate(self.authors):
@ -1165,6 +1167,8 @@ class OPFCreator(Metadata):
a(CAL_ELEM('calibre:series', self.series)) a(CAL_ELEM('calibre:series', self.series))
if self.series_index is not None: if self.series_index is not None:
a(CAL_ELEM('calibre:series_index', self.format_series_index())) 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: if self.rating is not None:
a(CAL_ELEM('calibre:rating', str(self.rating))) a(CAL_ELEM('calibre:rating', str(self.rating)))
if self.timestamp is not None: if self.timestamp is not None:
@ -1320,7 +1324,6 @@ def test_m2o():
mi.author_sort = 'author sort' mi.author_sort = 'author sort'
mi.pubdate = nowf() mi.pubdate = nowf()
mi.language = 'en' mi.language = 'en'
mi.category = 'test'
mi.comments = 'what a fun book\n\n' mi.comments = 'what a fun book\n\n'
mi.publisher = 'publisher' mi.publisher = 'publisher'
mi.isbn = 'boooo' mi.isbn = 'boooo'
@ -1335,11 +1338,11 @@ def test_m2o():
opf = metadata_to_opf(mi) opf = metadata_to_opf(mi)
print opf print opf
newmi = MetaInformation(OPF(StringIO(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', 'publisher', 'series', 'series_index', 'rating',
'isbn', 'tags', 'cover_data', 'application_id', 'isbn', 'tags', 'cover_data', 'application_id',
'language', 'cover', 'language', 'cover',
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'book_producer', 'timestamp',
'pubdate', 'rights', 'publication_type'): 'pubdate', 'rights', 'publication_type'):
o, n = getattr(mi, attr), getattr(newmi, attr) o, n = getattr(mi, attr), getattr(newmi, attr)
if o != n and o.strip() != n.strip(): if o != n and o.strip() != n.strip():
@ -1441,4 +1444,6 @@ def test_user_metadata():
print opf.render() print opf.render()
if __name__ == '__main__': if __name__ == '__main__':
test_user_metadata() #test_user_metadata()
#test_m2o()
test()

View File

@ -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 <kovid@kovidgoyal.net>'
__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

View File

@ -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 <kovid@kovidgoyal.net>'
__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

View File

@ -103,6 +103,8 @@ class EXTHHeader(object):
pass pass
elif id == 108: elif id == 108:
pass # Producer pass # Producer
elif id == 113:
pass # ASIN or UUID
#else: #else:
# print 'unhandled metadata record', id, repr(content) # print 'unhandled metadata record', id, repr(content)

View File

@ -1547,6 +1547,31 @@ class MobiWriter(object):
rights = 'Unknown' rights = 'Unknown'
exth.write(pack('>II', EXTH_CODES['rights'], len(rights) + 8)) exth.write(pack('>II', EXTH_CODES['rights'], len(rights) + 8))
exth.write(rights) 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 # Add a publication date entry
if oeb.metadata['date'] != [] : if oeb.metadata['date'] != [] :

View File

@ -74,23 +74,29 @@ class ShareConnMenu(QMenu): # {{{
opts = email_config().parse() opts = email_config().parse()
if opts.accounts: if opts.accounts:
self.email_to_menu = QMenu(_('Email to')+'...', self) 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()) keys = sorted(opts.accounts.keys())
for account in keys: for account in keys:
formats, auto, default = opts.accounts[account] formats, auto, default = opts.accounts[account]
dest = 'mail:'+account+';'+formats dest = 'mail:'+account+';'+formats
action1 = DeviceAction(dest, False, False, I('mail.png'), action1 = DeviceAction(dest, False, False, I('mail.png'),
_('Email to')+' '+account) account)
action2 = DeviceAction(dest, True, False, I('mail.png'), action2 = DeviceAction(dest, True, False, I('mail.png'),
_('Email to')+' '+account+ _(' and delete from library')) account + ' ' + _('(delete from library)'))
map(self.email_to_menu.addAction, (action1, action2)) self.email_to_menu.addAction(action1)
self.email_to_and_delete_menu.addAction(action2)
map(self.memory.append, (action1, action2)) map(self.memory.append, (action1, action2))
if default: if default:
map(self.addAction, (action1, action2)) ac = DeviceAction(dest, False, False,
map(self.email_actions.append, (action1, action2)) I('mail.png'), _('Email to') + ' ' +account)
self.email_to_menu.addSeparator() self.addAction(ac)
self.email_actions.append(ac)
action1.a_s.connect(sync_menu.action_triggered) action1.a_s.connect(sync_menu.action_triggered)
action2.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) self.email_actions.append(ac)
else: else:
ac = self.addAction(_('Setup email based sharing of books')) ac = self.addAction(_('Setup email based sharing of books'))

View File

@ -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 <kovid@kovidgoyal.net>'
__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_()

View File

@ -6,6 +6,7 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import Qt from PyQt4.Qt import Qt
from calibre.gui2 import gprefs
from calibre.gui2.convert.heuristics_ui import Ui_Form from calibre.gui2.convert.heuristics_ui import Ui_Form
from calibre.gui2.convert import Widget from calibre.gui2.convert import Widget
@ -21,17 +22,38 @@ class HeuristicsWidget(Widget, Ui_Form):
['enable_heuristics', 'markup_chapter_headings', ['enable_heuristics', 'markup_chapter_headings',
'italicize_common_cases', 'fix_indents', 'italicize_common_cases', 'fix_indents',
'html_unwrap_factor', 'unwrap_lines', 'html_unwrap_factor', 'unwrap_lines',
'delete_blank_paragraphs', 'format_scene_breaks', 'delete_blank_paragraphs',
'format_scene_breaks', 'replace_scene_breaks',
'dehyphenate', 'renumber_headings'] 'dehyphenate', 'renumber_headings']
) )
self.db, self.book_id = db, book_id self.db, self.book_id = db, book_id
self.rssb_defaults = [u'', u'<hr />', u' ', u'• • •', u'♦ ♦ ♦',
u'† †', u'‡ ‡ ‡', u'∞ ∞ ∞', u'¤ ¤ ¤', u'§']
self.initialize_options(get_option, get_help, db, book_id) self.initialize_options(get_option, get_help, db, book_id)
self.load_histories()
self.opt_enable_heuristics.stateChanged.connect(self.enable_heuristics) self.opt_enable_heuristics.stateChanged.connect(self.enable_heuristics)
self.opt_unwrap_lines.stateChanged.connect(self.enable_unwrap) self.opt_unwrap_lines.stateChanged.connect(self.enable_unwrap)
self.enable_heuristics(self.opt_enable_heuristics.checkState()) 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): def break_cycles(self):
Widget.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: if val is None and g is self.opt_html_unwrap_factor:
g.setValue(0.0) g.setValue(0.0)
return True 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): def enable_heuristics(self, state):
state = state == Qt.Checked state = state == Qt.Checked

View File

@ -150,6 +150,45 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Replace soft scene &amp;breaks:</string>
</property>
<property name="buddy">
<cstring>opt_replace_scene_breaks</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="opt_replace_scene_breaks">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>true</bool>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertAtTop</enum>
</property>
</widget>
</item>
</layout>
</item>
<item> <item>
<widget class="QCheckBox" name="opt_dehyphenate"> <widget class="QCheckBox" name="opt_dehyphenate">
<property name="text"> <property name="text">

View File

@ -70,9 +70,6 @@ class MetadataWidget(Widget, Ui_Form):
def initialize_metadata_options(self): def initialize_metadata_options(self):
self.initialize_combos() self.initialize_combos()
self.author.editTextChanged.connect(self.deduce_author_sort) 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) mi = self.db.get_metadata(self.book_id, index_is_id=True)
self.title.setText(mi.title) self.title.setText(mi.title)
@ -109,6 +106,9 @@ class MetadataWidget(Widget, Ui_Form):
def initalize_authors(self): def initalize_authors(self):
all_authors = self.db.all_authors() all_authors = self.db.all_authors()
all_authors.sort(key=lambda x : sort_key(x[1])) 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: for i in all_authors:
id, name = i id, name = i
@ -124,6 +124,8 @@ class MetadataWidget(Widget, Ui_Form):
def initialize_series(self): def initialize_series(self):
all_series = self.db.all_series() all_series = self.db.all_series()
all_series.sort(key=lambda x : sort_key(x[1])) 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: for i in all_series:
id, name = i id, name = i
@ -133,6 +135,8 @@ class MetadataWidget(Widget, Ui_Form):
def initialize_publisher(self): def initialize_publisher(self):
all_publishers = self.db.all_publishers() all_publishers = self.db.all_publishers()
all_publishers.sort(key=lambda x : sort_key(x[1])) 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: for i in all_publishers:
id, name = i id, name = i

View File

@ -190,7 +190,7 @@
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="4" column="1">
<widget class="CompleteLineEdit" name="tags"> <widget class="MultiCompleteLineEdit" name="tags">
<property name="toolTip"> <property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string> <string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property> </property>
@ -213,7 +213,7 @@
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="5" column="1">
<widget class="EnComboBox" name="series"> <widget class="MultiCompleteComboBox" name="series">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed"> <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>10</horstretch> <horstretch>10</horstretch>
@ -248,14 +248,14 @@
</widget> </widget>
</item> </item>
<item row="3" column="1"> <item row="3" column="1">
<widget class="EnComboBox" name="publisher"> <widget class="MultiCompleteComboBox" name="publisher">
<property name="editable"> <property name="editable">
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="CompleteComboBox" name="author"> <widget class="MultiCompleteComboBox" name="author">
<property name="editable"> <property name="editable">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -277,19 +277,14 @@
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>EnComboBox</class> <class>MultiCompleteComboBox</class>
<extends>QComboBox</extends> <extends>QComboBox</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>CompleteComboBox</class> <class>MultiCompleteLineEdit</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>CompleteLineEdit</class>
<extends>QLineEdit</extends> <extends>QLineEdit</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>ImageView</class> <class>ImageView</class>

View File

@ -48,10 +48,10 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0" colspan="3"> <item row="6" column="0" colspan="3">
<widget class="XPathEdit" name="opt_page_breaks_before" native="true"/> <widget class="XPathEdit" name="opt_page_breaks_before" native="true"/>
</item> </item>
<item row="6" column="0" colspan="3"> <item row="7" column="0" colspan="3">
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -77,6 +77,16 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item row="4" column="0" colspan="3">
<widget class="QLabel" name="label_2">
<property name="text">
<string>The header and footer removal options have been replaced by the Search &amp; Replace options. Click the Search &amp; 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.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<customwidgets> <customwidgets>

View File

@ -14,7 +14,7 @@ from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
QPushButton QPushButton
from calibre.utils.date import qt_to_dt, now 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.comments_editor import Editor as CommentsEditor
from calibre.gui2 import UNDEFINED_QDATE, error_dialog from calibre.gui2 import UNDEFINED_QDATE, error_dialog
from calibre.utils.config import tweaks 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 = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key) values.sort(key=sort_key)
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
w = CompleteLineEdit(parent, values) w = MultiCompleteLineEdit(parent)
w.update_items_cache(values)
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
else: else:
w = EnComboBox(parent) w = MultiCompleteComboBox(parent)
w.set_separator(None)
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon) w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
w.setMinimumContentsLength(25) w.setMinimumContentsLength(25)
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w] 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) val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
self.initial_val = val self.initial_val = val
val = self.normalize_db_val(val) val = self.normalize_db_val(val)
self.widgets[1].update_items_cache(self.all_values)
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
self.setter(val) self.setter(val)
self.widgets[1].update_items_cache(self.all_values)
else: else:
idx = None idx = None
for i, c in enumerate(self.all_values): for i, c in enumerate(self.all_values):
@ -276,7 +279,7 @@ class Series(Base):
def setup_ui(self, parent): def setup_ui(self, parent):
values = self.all_values = list(self.db.all_custom(num=self.col_id)) values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key) values.sort(key=sort_key)
w = EnComboBox(parent) w = MultiCompleteComboBox(parent)
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon) w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
w.setMinimumContentsLength(25) w.setMinimumContentsLength(25)
self.name_widget = w self.name_widget = w
@ -305,6 +308,7 @@ class Series(Base):
if c == val: if c == val:
idx = i idx = i
self.name_widget.addItem(c) self.name_widget.addItem(c)
self.name_widget.update_items_cache(self.all_values)
self.name_widget.setEditText('') self.name_widget.setEditText('')
if idx is not None: if idx is not None:
self.widgets[1].setCurrentIndex(idx) self.widgets[1].setCurrentIndex(idx)
@ -670,7 +674,7 @@ class BulkDateTime(BulkBase):
class BulkSeries(BulkBase): class BulkSeries(BulkBase):
def setup_ui(self, parent): 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 = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key) values.sort(key=sort_key)
self.main_widget.setSizeAdjustPolicy(self.main_widget.AdjustToMinimumContentsLengthWithIcon) self.main_widget.setSizeAdjustPolicy(self.main_widget.AdjustToMinimumContentsLengthWithIcon)
@ -705,6 +709,8 @@ class BulkSeries(BulkBase):
def initialize(self, book_id): def initialize(self, book_id):
self.idx_widget.setChecked(False) 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: for c in self.all_values:
self.main_widget.addItem(c) self.main_widget.addItem(c)
self.main_widget.setEditText('') self.main_widget.setEditText('')
@ -795,7 +801,8 @@ class RemoveTags(QWidget):
layout.setSpacing(5) layout.setSpacing(5)
layout.setContentsMargins(0, 0, 0, 0) 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) layout.addWidget(self.tags_box, stretch=3)
self.checkbox = QCheckBox(_('Remove all tags'), parent) self.checkbox = QCheckBox(_('Remove all tags'), parent)
layout.addWidget(self.checkbox) 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 = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key) values.sort(key=sort_key)
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
self.make_widgets(parent, CompleteLineEdit, self.make_widgets(parent, MultiCompleteLineEdit,
extra_label_text=_('tags to add')) extra_label_text=_('tags to add'))
self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.adding_widget = self.main_widget self.adding_widget = self.main_widget
@ -829,16 +836,16 @@ class BulkText(BulkBase):
w.tags_box.textChanged.connect(self.a_c_checkbox_changed) w.tags_box.textChanged.connect(self.a_c_checkbox_changed)
w.checkbox.stateChanged.connect(self.a_c_checkbox_changed) w.checkbox.stateChanged.connect(self.a_c_checkbox_changed)
else: else:
self.make_widgets(parent, EnComboBox) self.make_widgets(parent, MultiCompleteComboBox)
self.main_widget.set_separator(None)
self.main_widget.setSizeAdjustPolicy( self.main_widget.setSizeAdjustPolicy(
self.main_widget.AdjustToMinimumContentsLengthWithIcon) self.main_widget.AdjustToMinimumContentsLengthWithIcon)
self.main_widget.setMinimumContentsLength(25) self.main_widget.setMinimumContentsLength(25)
self.ignore_change_signals = False self.ignore_change_signals = False
def initialize(self, book_ids): def initialize(self, book_ids):
if self.col_metadata['is_multiple']:
self.main_widget.update_items_cache(self.all_values) self.main_widget.update_items_cache(self.all_values)
else: if not self.col_metadata['is_multiple']:
val = self.get_initial_value(book_ids) val = self.get_initial_value(book_ids)
self.initial_val = val = self.normalize_db_val(val) self.initial_val = val = self.normalize_db_val(val)
idx = None idx = None

View File

@ -838,9 +838,10 @@ class DeviceMixin(object): # {{{
format_count[f] = 1 format_count[f] = 1
for f in self.device_manager.device.settings().format_map: for f in self.device_manager.device.settings().format_map:
if f in format_count.keys(): 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: 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) d = ChooseFormatDeviceDialog(self, _('Choose format to send to device'), formats)
if d.exec_() != QDialog.Accepted: if d.exec_() != QDialog.Accepted:
return return
@ -871,6 +872,16 @@ class DeviceMixin(object): # {{{
self.send_by_mail(to, fmts, delete) self.send_by_mail(to, fmts, delete)
def cover_to_thumbnail(self, data): 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 \ ht = self.device_manager.device.THUMBNAIL_HEIGHT \
if self.device_manager else DevicePlugin.THUMBNAIL_HEIGHT if self.device_manager else DevicePlugin.THUMBNAIL_HEIGHT
try: try:
@ -1272,6 +1283,8 @@ class DeviceMixin(object): # {{{
x = x.lower() if x else '' x = x.lower() if x else ''
return string_pat.sub('', x) return string_pat.sub('', x)
update_metadata = prefs['manage_device_metadata'] == 'on_connect'
# Force a reset if the caches are not initialized # Force a reset if the caches are not initialized
if reset or not hasattr(self, 'db_book_title_cache'): if reset or not hasattr(self, 'db_book_title_cache'):
# Build a cache (map) of the library, so the search isn't On**2 # Build a cache (map) of the library, so the search isn't On**2
@ -1284,8 +1297,13 @@ class DeviceMixin(object): # {{{
except: except:
return False 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(): 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) title = clean_string(mi.title)
if title not in db_book_title_cache: if title not in db_book_title_cache:
db_book_title_cache[title] = \ 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 # the application_id to the db_id of the matching book. This value
# will be used by books_on_device to indicate matches. # will be used by books_on_device to indicate matches.
update_metadata = prefs['manage_device_metadata'] == 'on_connect'
for booklist in booklists: for booklist in booklists:
for book in booklist: for book in booklist:
book.in_library = None book.in_library = None
@ -1382,6 +1399,12 @@ class DeviceMixin(object): # {{{
if update_metadata: if update_metadata:
if self.device_manager.is_device_connected: 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', {}) plugboards = self.library_view.model().db.prefs.get('plugboards', {})
self.device_manager.sync_booklists( self.device_manager.sync_booklists(
Dispatcher(self.metadata_synced), booklists, Dispatcher(self.metadata_synced), booklists,

View File

@ -7,8 +7,8 @@ __license__ = 'GPL v3'
from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \ from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \
QApplication, QSpinBox, QToolButton, QIcon QApplication, QSpinBox, QToolButton, QIcon
from calibre.ebooks.metadata import authors_to_string, string_to_authors 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.utils.icu import sort_key
from calibre.gui2.complete import MultiCompleteComboBox
class AddEmptyBookDialog(QDialog): class AddEmptyBookDialog(QDialog):
@ -32,7 +32,7 @@ class AddEmptyBookDialog(QDialog):
self.author_label = QLabel(_('Set the author of the new books to:')) self.author_label = QLabel(_('Set the author of the new books to:'))
self._layout.addWidget(self.author_label, 2, 0, 1, 2) 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.setSizeAdjustPolicy(
self.authors_combo.AdjustToMinimumContentsLengthWithIcon) self.authors_combo.AdjustToMinimumContentsLengthWithIcon)
self.authors_combo.setEditable(True) self.authors_combo.setEditable(True)

View File

@ -764,6 +764,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
def initialize_series(self): def initialize_series(self):
all_series = self.db.all_series() all_series = self.db.all_series()
all_series.sort(key=lambda x : sort_key(x[1])) 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: for i in all_series:
id, name = i id, name = i
@ -773,6 +775,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
def initialize_publisher(self): def initialize_publisher(self):
all_publishers = self.db.all_publishers() all_publishers = self.db.all_publishers()
all_publishers.sort(key=lambda x : sort_key(x[1])) 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: for i in all_publishers:
id, name = i id, name = i

View File

@ -76,7 +76,7 @@
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="CompleteComboBox" name="authors"> <widget class="MultiCompleteComboBox" name="authors">
<property name="editable"> <property name="editable">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -175,7 +175,7 @@
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="4" column="1">
<widget class="EnComboBox" name="publisher"> <widget class="MultiCompleteComboBox" name="publisher">
<property name="editable"> <property name="editable">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -195,7 +195,7 @@
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="5" column="1">
<widget class="CompleteLineEdit" name="tags"> <widget class="MultiCompleteLineEdit" name="tags">
<property name="toolTip"> <property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string> <string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property> </property>
@ -229,7 +229,7 @@
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="6" column="1">
<widget class="CompleteLineEdit" name="remove_tags"> <widget class="MultiCompleteLineEdit" name="remove_tags">
<property name="toolTip"> <property name="toolTip">
<string>Comma separated list of tags to remove from the books. </string> <string>Comma separated list of tags to remove from the books. </string>
</property> </property>
@ -262,7 +262,7 @@
</widget> </widget>
</item> </item>
<item row="7" column="1"> <item row="7" column="1">
<widget class="EnComboBox" name="series"> <widget class="MultiCompleteComboBox" name="series">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed"> <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch> <horstretch>0</horstretch>
@ -1072,19 +1072,14 @@ not multiple and the destination field is multiple</string>
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>EnComboBox</class> <class>MultiCompleteComboBox</class>
<extends>QComboBox</extends> <extends>QComboBox</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>CompleteComboBox</class> <class>MultiCompleteLineEdit</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>CompleteLineEdit</class>
<extends>QLineEdit</extends> <extends>QLineEdit</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>HistoryLineEdit</class> <class>HistoryLineEdit</class>

View File

@ -429,10 +429,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
old_extensions.add(ext) old_extensions.add(ext)
for ext in new_extensions: for ext in new_extensions:
self.db.add_format(self.row, ext, open(paths[ext], 'rb'), notify=False) 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) extensions = new_extensions.union(old_extensions)
for ext in db_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) self.db.remove_format(self.row, ext, notify=False)
def show_format(self, item, *args): def show_format(self, item, *args):
@ -576,6 +578,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.orig_date = qt_to_dt(self.date.date()) self.orig_date = qt_to_dt(self.date.date())
exts = self.db.formats(row) exts = self.db.formats(row)
self.original_formats = []
if exts: if exts:
exts = exts.split(',') exts = exts.split(',')
for ext in exts: for ext in exts:
@ -586,6 +589,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if size is None: if size is None:
continue continue
Format(self.formats, ext, size, timestamp=timestamp) Format(self.formats, ext, size, timestamp=timestamp)
self.original_formats.append(ext.lower())
self.initialize_combos() self.initialize_combos()
@ -735,6 +739,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.series.setSizeAdjustPolicy(self.series.AdjustToContentsOnFirstShow) self.series.setSizeAdjustPolicy(self.series.AdjustToContentsOnFirstShow)
all_series = self.db.all_series() all_series = self.db.all_series()
all_series.sort(key=lambda x : sort_key(x[1])) 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) series_id = self.db.series_id(self.row)
idx, c = None, 0 idx, c = None, 0
for i in all_series: for i in all_series:
@ -752,6 +758,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
def initialize_publisher(self): def initialize_publisher(self):
all_publishers = self.db.all_publishers() all_publishers = self.db.all_publishers()
all_publishers.sort(key=lambda x : sort_key(x[1])) 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) publisher_id = self.db.publisher_id(self.row)
idx, c = None, 0 idx, c = None, 0
for i in all_publishers: for i in all_publishers:

View File

@ -240,7 +240,7 @@ Using this button to create author sort will change author sort from red to gree
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="CompleteComboBox" name="authors"> <widget class="MultiCompleteComboBox" name="authors">
<property name="editable"> <property name="editable">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -313,7 +313,7 @@ If the box is colored green, then text matches the individual author's sort stri
</widget> </widget>
</item> </item>
<item row="5" column="1" colspan="2"> <item row="5" column="1" colspan="2">
<widget class="EnComboBox" name="publisher"> <widget class="MultiCompleteComboBox" name="publisher">
<property name="editable"> <property name="editable">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -335,7 +335,7 @@ If the box is colored green, then text matches the individual author's sort stri
<item row="6" column="1"> <item row="6" column="1">
<layout class="QHBoxLayout" name="_2"> <layout class="QHBoxLayout" name="_2">
<item> <item>
<widget class="CompleteLineEdit" name="tags"> <widget class="MultiCompleteLineEdit" name="tags">
<property name="toolTip"> <property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string> <string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property> </property>
@ -379,7 +379,7 @@ If the box is colored green, then text matches the individual author's sort stri
<number>5</number> <number>5</number>
</property> </property>
<item> <item>
<widget class="EnComboBox" name="series"> <widget class="MultiCompleteComboBox" name="series">
<property name="toolTip"> <property name="toolTip">
<string>List of known series. You can add new series.</string> <string>List of known series. You can add new series.</string>
</property> </property>
@ -837,19 +837,14 @@ If the box is colored green, then text matches the individual author's sort stri
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>EnComboBox</class> <class>MultiCompleteLineEdit</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>CompleteLineEdit</class>
<extends>QLineEdit</extends> <extends>QLineEdit</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>CompleteComboBox</class> <class>MultiCompleteComboBox</class>
<extends>QComboBox</extends> <extends>QComboBox</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>FormatList</class> <class>FormatList</class>

View File

@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import re, copy 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.gui2.dialogs.search_ui import Ui_Dialog
from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH
@ -29,20 +29,18 @@ class SearchDialog(QDialog, Ui_Dialog):
name = name.strip().replace('|', ',') name = name.strip().replace('|', ',')
self.authors_box.addItem(name) self.authors_box.addItem(name)
self.authors_box.setEditText('') 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_separator('&')
self.authors_box.set_space_before_sep(True) self.authors_box.set_space_before_sep(True)
self.authors_box.update_items_cache(db.all_author_names()) self.authors_box.update_items_cache(db.all_author_names())
all_series = db.all_series() all_series = db.all_series()
all_series.sort(key=lambda x : sort_key(x[1])) 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: for i in all_series:
id, name = i id, name = i
self.series_box.addItem(name) self.series_box.addItem(name)
self.series_box.setEditText('') self.series_box.setEditText('')
self.series_box.completer().setCompletionMode(QCompleter.PopupCompletion)
self.series_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive)
all_tags = db.all_tags() all_tags = db.all_tags()
self.tags_box.update_items_cache(all_tags) self.tags_box.update_items_cache(all_tags)

View File

@ -265,21 +265,21 @@
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="CompleteComboBox" name="authors_box"> <widget class="MultiCompleteComboBox" name="authors_box">
<property name="toolTip"> <property name="toolTip">
<string>Enter an author's name. Only one author can be used.</string> <string>Enter an author's name. Only one author can be used.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1"> <item row="3" column="1">
<widget class="EnComboBox" name="series_box"> <widget class="MultiCompleteComboBox" name="series_box">
<property name="toolTip"> <property name="toolTip">
<string>Enter a series name, without an index. Only one series name can be used.</string> <string>Enter a series name, without an index. Only one series name can be used.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="4" column="1">
<widget class="CompleteLineEdit" name="tags_box"> <widget class="MultiCompleteLineEdit" name="tags_box">
<property name="toolTip"> <property name="toolTip">
<string>Enter tags separated by spaces</string> <string>Enter tags separated by spaces</string>
</property> </property>
@ -355,19 +355,14 @@
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>EnComboBox</class> <class>MultiCompleteLineEdit</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>CompleteLineEdit</class>
<extends>QLineEdit</extends> <extends>QLineEdit</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>CompleteComboBox</class> <class>MultiCompleteComboBox</class>
<extends>QComboBox</extends> <extends>QComboBox</extends>
<header>widgets.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>

View File

@ -264,8 +264,9 @@ class EmailMixin(object): # {{{
if _auto_ids != []: if _auto_ids != []:
for id in _auto_ids: for id in _auto_ids:
if specific_format == None: if specific_format == None:
formats = [f.lower() for f in self.library_view.model().db.formats(id, index_is_id=True).split(',')] dbfmts = self.library_view.model().db.formats(id, index_is_id=True)
formats = formats if formats != None else [] 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())) != []: if list(set(formats).intersection(available_input_formats())) != [] and list(set(fmts).intersection(available_output_formats())) != []:
auto.append(id) auto.append(id)
else: else:

View File

@ -12,11 +12,11 @@ from PyQt4.Qt import QColor, Qt, QModelIndex, QSize, \
QPainterPath, QLinearGradient, QBrush, \ QPainterPath, QLinearGradient, QBrush, \
QPen, QStyle, QPainter, QStyleOptionViewItemV4, \ QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
QIcon, QDoubleSpinBox, QVariant, QSpinBox, \ QIcon, QDoubleSpinBox, QVariant, QSpinBox, \
QStyledItemDelegate, QCompleter, \ QStyledItemDelegate, QComboBox, QTextDocument
QComboBox, QTextDocument
from calibre.gui2 import UNDEFINED_QDATE, error_dialog 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.date import now, format_date
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
from calibre.utils.formatter import validation_formatter from calibre.utils.formatter import validation_formatter
@ -151,34 +151,11 @@ class TextDelegate(QStyledItemDelegate): # {{{
self.auto_complete_function = f self.auto_complete_function = f
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
editor = EnLineEdit(parent)
if self.auto_complete_function: if self.auto_complete_function:
editor = MultiCompleteLineEdit(parent)
editor.set_separator(None)
complete_items = [i[1] for i in self.auto_complete_function()] complete_items = [i[1] for i in self.auto_complete_function()]
completer = QCompleter(complete_items, self) editor.update_items_cache(complete_items)
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
else: else:
editor = EnLineEdit(parent) editor = EnLineEdit(parent)
return editor return editor
@ -197,13 +174,15 @@ class CompleteDelegate(QStyledItemDelegate): # {{{
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
if self.db and hasattr(self.db, self.items_func_name): if self.db and hasattr(self.db, self.items_func_name):
col = index.model().column_map[index.column()] 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): if not index.model().is_custom_column(col):
editor = CompleteLineEdit(parent, getattr(self.db, self.items_func_name)(), all_items = getattr(self.db, self.items_func_name)()
self.sep, self.space_before_sep)
else: else:
editor = CompleteLineEdit(parent, all_items = list(self.db.all_custom(
sorted(list(self.db.all_custom(label=self.db.field_metadata.key_to_label(col))), label=self.db.field_metadata.key_to_label(col)))
key=sort_key), self.sep, self.space_before_sep) editor.update_items_cache(all_items)
else: else:
editor = EnLineEdit(parent) editor = EnLineEdit(parent)
return editor return editor
@ -273,13 +252,11 @@ class CcTextDelegate(QStyledItemDelegate): # {{{
editor.setRange(-100., float(sys.maxint)) editor.setRange(-100., float(sys.maxint))
editor.setDecimals(2) editor.setDecimals(2)
else: 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))), complete_items = sorted(list(m.db.all_custom(label=m.db.field_metadata.key_to_label(col))),
key=sort_key) key=sort_key)
completer = QCompleter(complete_items, self) editor.update_items_cache(complete_items)
completer.setCaseSensitivity(Qt.CaseInsensitive)
completer.setCompletionMode(QCompleter.PopupCompletion)
editor.setCompleter(completer)
return editor return editor
# }}} # }}}

View File

@ -12,8 +12,8 @@ from PyQt4.Qt import Qt, QDateEdit, QDate, \
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \ QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \
QPushButton, QSpinBox, QLineEdit QPushButton, QSpinBox, QLineEdit
from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ from calibre.gui2.widgets import EnLineEdit, FormatList, ImageView
EnComboBox, FormatList, ImageView, CompleteLineEdit from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks, prefs from calibre.utils.config import tweaks, prefs
from calibre.ebooks.metadata import title_sort, authors_to_string, \ from calibre.ebooks.metadata import title_sort, authors_to_string, \
@ -149,14 +149,14 @@ class TitleSortEdit(TitleEdit):
# }}} # }}}
# Authors {{{ # Authors {{{
class AuthorsEdit(CompleteComboBox): class AuthorsEdit(MultiCompleteComboBox):
TOOLTIP = '' TOOLTIP = ''
LABEL = _('&Author(s):') LABEL = _('&Author(s):')
def __init__(self, parent): def __init__(self, parent):
self.dialog = parent self.dialog = parent
CompleteComboBox.__init__(self, parent) MultiCompleteComboBox.__init__(self, parent)
self.setToolTip(self.TOOLTIP) self.setToolTip(self.TOOLTIP)
self.setWhatsThis(self.TOOLTIP) self.setWhatsThis(self.TOOLTIP)
self.setEditable(True) self.setEditable(True)
@ -283,13 +283,14 @@ class AuthorSortEdit(EnLineEdit):
# }}} # }}}
# Series {{{ # Series {{{
class SeriesEdit(EnComboBox): class SeriesEdit(MultiCompleteComboBox):
TOOLTIP = _('List of known series. You can add new series.') TOOLTIP = _('List of known series. You can add new series.')
LABEL = _('&Series:') LABEL = _('&Series:')
def __init__(self, parent): def __init__(self, parent):
EnComboBox.__init__(self, parent) MultiCompleteComboBox.__init__(self, parent)
self.set_separator(None)
self.dialog = parent self.dialog = parent
self.setSizeAdjustPolicy( self.setSizeAdjustPolicy(
self.AdjustToMinimumContentsLengthWithIcon) self.AdjustToMinimumContentsLengthWithIcon)
@ -314,6 +315,7 @@ class SeriesEdit(EnComboBox):
def initialize(self, db, id_): def initialize(self, db, id_):
all_series = db.all_series() all_series = db.all_series()
all_series.sort(key=lambda x : sort_key(x[1])) 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) series_id = db.series_id(id_, index_is_id=True)
idx, c = None, 0 idx, c = None, 0
for i in all_series: for i in all_series:
@ -472,6 +474,7 @@ class FormatsManager(QWidget): # {{{
def initialize(self, db, id_): def initialize(self, db, id_):
self.changed = False self.changed = False
exts = db.formats(id_, index_is_id=True) exts = db.formats(id_, index_is_id=True)
self.original_val = set([])
if exts: if exts:
exts = exts.split(',') exts = exts.split(',')
for ext in exts: for ext in exts:
@ -482,6 +485,7 @@ class FormatsManager(QWidget): # {{{
if size is None: if size is None:
continue continue
Format(self.formats, ext, size, timestamp=timestamp) Format(self.formats, ext, size, timestamp=timestamp)
self.original_val.add(ext.lower())
def commit(self, db, id_): def commit(self, db, id_):
if not self.changed: if not self.changed:
@ -500,11 +504,12 @@ class FormatsManager(QWidget): # {{{
for ext in new_extensions: for ext in new_extensions:
db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False, db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False,
index_is_id=True) index_is_id=True)
db_extensions = set([f.lower() for f in db.formats(id_, dbfmts = db.formats(id_, index_is_id=True)
index_is_id=True).split(',')]) db_extensions = set([f.lower() for f in (dbfmts.split(',') if dbfmts
else [])])
extensions = new_extensions.union(old_extensions) extensions = new_extensions.union(old_extensions)
for ext in db_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) db.remove_format(id_, ext, notify=False, index_is_id=True)
self.changed = False self.changed = False
@ -811,14 +816,14 @@ class RatingEdit(QSpinBox): # {{{
# }}} # }}}
class TagsEdit(CompleteLineEdit): # {{{ class TagsEdit(MultiCompleteLineEdit): # {{{
LABEL = _('Ta&gs:') LABEL = _('Ta&gs:')
TOOLTIP = '<p>'+_('Tags categorize the book. This is particularly ' TOOLTIP = '<p>'+_('Tags categorize the book. This is particularly '
'useful while searching. <br><br>They can be any words' 'useful while searching. <br><br>They can be any words'
'or phrases, separated by commas.') 'or phrases, separated by commas.')
def __init__(self, parent): def __init__(self, parent):
CompleteLineEdit.__init__(self, parent) MultiCompleteLineEdit.__init__(self, parent)
self.setToolTip(self.TOOLTIP) self.setToolTip(self.TOOLTIP)
self.setWhatsThis(self.TOOLTIP) self.setWhatsThis(self.TOOLTIP)
@ -836,7 +841,7 @@ class TagsEdit(CompleteLineEdit): # {{{
tags = db.tags(id_, index_is_id=True) tags = db.tags(id_, index_is_id=True)
tags = tags.split(',') if tags else [] tags = tags.split(',') if tags else []
self.current_val = tags self.current_val = tags
self.update_items_cache(db.all_tags()) self.all_items = db.all_tags()
self.original_val = self.current_val self.original_val = self.current_val
@property @property
@ -857,7 +862,7 @@ class TagsEdit(CompleteLineEdit): # {{{
d = TagEditor(self, db, id_) d = TagEditor(self, db, id_)
if d.exec_() == TagEditor.Accepted: if d.exec_() == TagEditor.Accepted:
self.current_val = d.tags self.current_val = d.tags
self.update_items_cache(db.all_tags()) self.all_items = db.all_tags()
def commit(self, db, id_): def commit(self, db, id_):
@ -907,11 +912,12 @@ class ISBNEdit(QLineEdit): # {{{
# }}} # }}}
class PublisherEdit(EnComboBox): # {{{ class PublisherEdit(MultiCompleteComboBox): # {{{
LABEL = _('&Publisher:') LABEL = _('&Publisher:')
def __init__(self, parent): def __init__(self, parent):
EnComboBox.__init__(self, parent) MultiCompleteComboBox.__init__(self, parent)
self.set_separator(None)
self.setSizeAdjustPolicy( self.setSizeAdjustPolicy(
self.AdjustToMinimumContentsLengthWithIcon) self.AdjustToMinimumContentsLengthWithIcon)
@ -932,6 +938,7 @@ class PublisherEdit(EnComboBox): # {{{
def initialize(self, db, id_): def initialize(self, db, id_):
all_publishers = db.all_publishers() all_publishers = db.all_publishers()
all_publishers.sort(key=lambda x : sort_key(x[1])) 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) publisher_id = db.publisher_id(id_, index_is_id=True)
idx, c = None, 0 idx, c = None, 0
for i in all_publishers: for i in all_publishers:

View File

@ -430,8 +430,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
authors = self.authors(id, index_is_id=True) authors = self.authors(id, index_is_id=True)
if not authors: if not authors:
authors = _('Unknown') authors = _('Unknown')
author = ascii_filename(authors.split(',')[0][: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, 'ignore') title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
path = author + '/' + title + ' (%d)'%id path = author + '/' + title + ' (%d)'%id
return path return path
@ -442,8 +442,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
authors = self.authors(id, index_is_id=True) authors = self.authors(id, index_is_id=True)
if not authors: if not authors:
authors = _('Unknown') authors = _('Unknown')
author = ascii_filename(authors.split(',')[0][: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') title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
name = title + ' - ' + author name = title + ' - ' + author
while name.endswith('.'): while name.endswith('.'):
name = name[:-1] name = name[:-1]

View File

@ -311,9 +311,24 @@ remove all non-breaking-space entities, or may include false positive matches re
:guilabel:`Ensure scene breaks are consistently formatted` :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. 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 'Soft' scene break markers, i.e. scene breaks only defined by extra white space, are styled to ensure that they
page width. Some readers may find this desirable as these 'soft' scene breaks often become page breaks on readers, and will not be displayed in conjunction with page breaks.
thus become difficult to distinguish.
: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. <hr />
tags, i.e. horizontal rules, and <img> 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):
<img style="width:10%" src="../Images/scenebreak.png" />
Example horizontal rule with styles:
<hr style="width:20%;padding-top: 1px;border-top: 2px ridge black;border-bottom: 2px groove black;"/>
:guilabel:`Remove unnecessary hyphens` :guilabel:`Remove unnecessary hyphens`
|app| will analyze all hyphenated content in the document when this option is enabled. The document itself is used |app| will analyze all hyphenated content in the document when this option is enabled. The document itself is used
@ -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`. 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. 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 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`. :ref:`regexptutorial`.

View File

@ -391,6 +391,8 @@ Take your pick:
* A tribute to the SONY Librie which was the first e-ink based e-book reader * A tribute to the SONY Librie which was the first e-ink based e-book reader
* My wife chose it ;-) * 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? 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. |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.

View File

@ -186,7 +186,7 @@ class BuiltinTemplate(BuiltinFormatterFunction):
def evaluate(self, formatter, kwargs, mi, locals, template): def evaluate(self, formatter, kwargs, mi, locals, template):
template = template.replace('[[', '{').replace(']]', '}') template = template.replace('[[', '{').replace(']]', '}')
return formatter.safe_format(template, kwargs, 'TEMPLATE', mi) return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)
class BuiltinEval(BuiltinFormatterFunction): class BuiltinEval(BuiltinFormatterFunction):
name = 'eval' name = 'eval'

View File

@ -72,10 +72,16 @@ def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None,
f.write(data) f.write(data)
return ret 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 = Image()
img.load(data) img.load(data)
owidth, oheight = img.size owidth, oheight = img.size
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) scaled, nwidth, nheight = fit_image(owidth, oheight, width, height)
if scaled: if scaled:
img.size = (nwidth, nheight) img.size = (nwidth, nheight)