Sync to trunk.

This commit is contained in:
John Schember 2011-01-27 17:50:01 -05:00
commit 8f8c5eaa97
38 changed files with 1325 additions and 649 deletions

View File

@ -2,7 +2,7 @@
__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.5 - 25 Jan 2011' description = 'Periódico gratuito en español - v0.8 - 27 Jan 2011'
''' '''
www.20minutos.es www.20minutos.es
@ -15,8 +15,8 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
title = u'20 Minutos' title = u'20 Minutos'
publisher = u'Grupo 20 Minutos' publisher = u'Grupo 20 Minutos'
__author__ = u'Luis Hernández' __author__ = 'Luis Hernández'
description = u'Periódico gratuito en español' description = 'Periódico gratuito en español'
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 = 5
@ -30,8 +30,9 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
language = 'es' language = 'es'
timefmt = '[%a, %d %b, %Y]' timefmt = '[%a, %d %b, %Y]'
keep_only_tags = [dict(name='div', attrs={'id':['content']}) keep_only_tags = [
,dict(name='div', attrs={'class':['boxed','description','lead','article-content']}) dict(name='div', attrs={'id':['content','vinetas',]})
,dict(name='div', attrs={'class':['boxed','description','lead','article-content','cuerpo estirar']})
,dict(name='span', attrs={'class':['photo-bar']}) ,dict(name='span', attrs={'class':['photo-bar']})
,dict(name='ul', attrs={'class':['article-author']}) ,dict(name='ul', attrs={'class':['article-author']})
] ]
@ -42,10 +43,12 @@ 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']}) ,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={'id':['twitter-destacados']}) ,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='li', attrs={'class':['puntuacion','enviar','compartir']})
]
feeds = [ feeds = [
(u'Portada' , u'http://www.20minutos.es/rss/') (u'Portada' , u'http://www.20minutos.es/rss/')
@ -62,6 +65,6 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
,(u'Empleo' , u'http://www.20minutos.es/rss/empleo/') ,(u'Empleo' , u'http://www.20minutos.es/rss/empleo/')
,(u'Cine' , u'http://www.20minutos.es/rss/cine/') ,(u'Cine' , u'http://www.20minutos.es/rss/cine/')
,(u'Musica' , u'http://www.20minutos.es/rss/musica/') ,(u'Musica' , u'http://www.20minutos.es/rss/musica/')
,(u'Vinetas' , u'http://www.20minutos.es/rss/vinetas/')
,(u'Comunidad20' , u'http://www.20minutos.es/rss/zona20/') ,(u'Comunidad20' , u'http://www.20minutos.es/rss/zona20/')
] ]

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8
__license__ = 'GPL v3'
__author__ = 'Luis Hernandez'
__copyright__ = 'Luis Hernandez<tolyluis@gmail.com>'
'''
http://www.filmica.com/david_bravo/
'''
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1294946868(BasicNewsRecipe):
title = u'Blog de David Bravo'
publisher = u'Filmica'
__author__ = 'Luis Hernández'
description = 'blog sobre leyes, p2p y copyright'
cover_url = 'http://www.elpais.es/edigitales/image.php?foto=par/portada/1551.jpg'
oldest_article = 365
max_articles_per_feed = 100
remove_javascript = True
no_stylesheets = True
use_embedded_content = False
encoding = 'ISO-8859-1'
language = 'es'
timefmt = '[%a, %d %b, %Y]'
keep_only_tags = [
dict(name='div', attrs={'class':['blog','date','blogbody','comments-head','comments-body']})
,dict(name='span', attrs={'class':['comments-post']})
]
remove_tags_before = dict(name='div' , attrs={'id':['bitacoras']})
remove_tags_after = dict(name='div' , attrs={'id':['comments-body']})
extra_css = ' p{text-align: justify; font-size: 100%} body{ text-align: left; font-family: serif; font-size: 100% } h2{ font-family: sans-serif; font-size:75%; font-weight: 800; text-align: justify } h3{ font-family: sans-serif; font-size:150%; font-weight: 600; text-align: left } img{margin-bottom: 0.4em} '
feeds = [(u'Blog', u'http://www.filmica.com/david_bravo/index.rdf')]

View File

@ -22,8 +22,11 @@ class Economist(BasicNewsRecipe):
oldest_article = 7.0 oldest_article = 7.0
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg' cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']), remove_tags = [
dict(attrs={'class':['dblClkTrk', 'ec-article-info']})] dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
dict(attrs={'class':['dblClkTrk', 'ec-article-info']}),
{'class': lambda x: x and 'share-links-header' in x},
]
keep_only_tags = [dict(id='ec-article-body')] keep_only_tags = [dict(id='ec-article-body')]
needs_subscription = False needs_subscription = False
no_stylesheets = True no_stylesheets = True

View File

@ -16,8 +16,11 @@ class Economist(BasicNewsRecipe):
oldest_article = 7.0 oldest_article = 7.0
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg' cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']), remove_tags = [
dict(attrs={'class':['dblClkTrk', 'ec-article-info']})] dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
dict(attrs={'class':['dblClkTrk', 'ec-article-info']}),
{'class': lambda x: x and 'share-links-header' in x},
]
keep_only_tags = [dict(id='ec-article-body')] keep_only_tags = [dict(id='ec-article-body')]
no_stylesheets = True no_stylesheets = True
preprocess_regexps = [(re.compile('</html>.*', re.DOTALL), preprocess_regexps = [(re.compile('</html>.*', re.DOTALL),

View File

@ -1,17 +1,18 @@
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1293122276(BasicNewsRecipe): class AdvancedUserRecipe1293122276(BasicNewsRecipe):
title = u'Smarter Planet | Tumblr for eReaders' title = u'Smarter Planet | Tumblr'
__author__ = 'Jack Mason' __author__ = 'Jack Mason'
author = 'IBM Global Business Services' author = 'IBM Global Business Services'
publisher = 'IBM' publisher = 'IBM'
language = 'en' language = 'en'
category = 'news, technology, IT, internet of things, analytics' category = 'news, technology, IT, internet of things, analytics'
oldest_article = 7 oldest_article = 14
max_articles_per_feed = 30 max_articles_per_feed = 30
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
masthead_url = 'http://30.media.tumblr.com/tumblr_l70dow9UmU1qzs4rbo1_r3_250.jpg' masthead_url = 'http://www.hellercd.com/wp-content/uploads/2010/09/hero.jpg'
remove_tags_before = dict(id='item') remove_tags_before = dict(id='item')
remove_tags_after = dict(id='item') remove_tags_after = dict(id='item')
remove_tags = [dict(attrs={'class':['sidebar', 'about', 'footer', 'description,' 'disqus', 'nav', 'notes', 'disqus_thread']}), remove_tags = [dict(attrs={'class':['sidebar', 'about', 'footer', 'description,' 'disqus', 'nav', 'notes', 'disqus_thread']}),
@ -21,4 +22,3 @@ class AdvancedUserRecipe1293122276(BasicNewsRecipe):
feeds = [(u'Smarter Planet Tumblr', u'http://smarterplanet.tumblr.com/mobile/rss')] feeds = [(u'Smarter Planet Tumblr', u'http://smarterplanet.tumblr.com/mobile/rss')]

View File

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
__author__ = 'Luis Hernandez'
__copyright__ = 'Luis Hernandez<tolyluis@gmail.com>'
description = 'Diario independiente de Asturias - v1.0 - 27 Jan 2011'
'''
www.lne.es
'''
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1294946868(BasicNewsRecipe):
title = u'La Nueva España'
publisher = u'Editorial Prensa Iberica'
__author__ = 'Luis Hernandez'
description = 'Diario independiente de Asturias'
cover_url = 'http://estaticos00.lne.es//elementosWeb/mediaweb/images/iconos/logo2.jpg'
oldest_article = 3
max_articles_per_feed = 100
remove_javascript = True
no_stylesheets = True
use_embedded_content = False
encoding = 'ISO-8859-1'
language = 'es'
timefmt = '[%a, %d %b, %Y]'
keep_only_tags = [
dict(name='div', attrs={'class':['noticia_titular','subtitulo','noticiadd2','noticia_texto']})
,dict(name='div', attrs={'id':['noticia_texto']})
]
extra_css = ' p{text-align: justify; font-size: 100%} body{ text-align: left; font-family: serif; font-size: 100% } h1{ font-family: sans-serif; font-size:150%; font-weight: 600; text-align: justify; } h2{ font-family: sans-serif; font-size:120%; font-weight: 500; text-align: justify } '
remove_tags_before = dict(name='div' , attrs={'class':['contenedor']})
remove_tags_after = dict(name='div' , attrs={'class':['fin_noticia']})
remove_tags = [
dict(name='div', attrs={'class':['epigrafe','antetitulo','bloqueclear','bloqueclear_video','cuadro_multimedia','cintillo2','editor_documentos','noticiadd','noticiadd3','noticiainterior','fin_noticia']})
,dict(name='div', attrs={'id':['evotos']})
]
feeds = [
(u'Al minuto' , u'http://www.lne.es/elementosInt/rss/AlMinuto')
,(u'General' , u'http://www.lne.es/elementosInt/rss/55')
,(u'Nacional' , u'http://www.lne.es/elementosInt/rss/43')
,(u'Internacional' , u'http://www.lne.es/elementosInt/rss/44')
,(u'Economia' , u'http://www.lne.es/elementosInt/rss/45')
,(u'Deportes' , u'http://www.lne.es/elementosInt/rss/47')
,(u'Campeones' , u'http://www.lne.es/elementosInt/rss/65')
,(u'Sociedad' , u'http://www.lne.es/elementosInt/rss/46')
,(u'Sucesos' , u'http://www.lne.es/elementosInt/rss/48')
,(u'Galeria' , u'http://www.lne.es/elementosInt/rss/51')
,(u'Cultura' , u'http://www.lne.es/elementosInt/rss/66')
,(u'Motor' , u'http://www.lne.es/elementosInt/rss/62')
,(u'Opinion' , u'http://www.lne.es/elementosInt/rss/52')
,(u'Asturias' , u'http://www.lne.es/elementosInt/rss/42')
,(u'Oviedo' , u'http://www.lne.es/elementosInt/rss/31')
,(u'Gijon' , u'http://www.lne.es/elementosInt/rss/35')
,(u'Aviles' , u'http://www.lne.es/elementosInt/rss/36')
,(u'Nalon' , u'http://www.lne.es/elementosInt/rss/37')
,(u'Cuencas' , u'http://www.lne.es/elementosInt/rss/38')
,(u'Caudal' , u'http://www.lne.es/elementosInt/rss/39')
,(u'Oriente' , u'http://www.lne.es/elementosInt/rss/40')
,(u'Occidente' , u'http://www.lne.es/elementosInt/rss/41')
,(u'Mar y Campo' , u'http://www.lne.es/elementosInt/rss/63')
,(u'Ultima' , u'http://www.lne.es/elementosInt/rss/50')
]

View File

@ -1,9 +1,22 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
__author__ = 'Luis Hernandez'
__copyright__ = 'Luis Hernandez<tolyluis@gmail.com>'
description = 'Diario local de Talavera de la Reina - v1.2 - 27 Jan 2011'
'''
http://www.latribunadetalavera.es/
'''
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1294946868(BasicNewsRecipe): class AdvancedUserRecipe1294946868(BasicNewsRecipe):
title = u'La Tribuna de Talavera' title = u'La Tribuna de Talavera'
publisher = u'Grupo PROMECAL'
__author__ = 'Luis Hernández' __author__ = 'Luis Hernández'
description = 'Diario de Talavera de la Reina' description = 'Diario local de Talavera de la Reina'
cover_url = 'http://www.latribunadetalavera.es/entorno/mancheta.gif' cover_url = 'http://www.latribunadetalavera.es/entorno/mancheta.gif'
oldest_article = 5 oldest_article = 5
@ -17,7 +30,8 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
language = 'es' language = 'es'
timefmt = '[%a, %d %b, %Y]' timefmt = '[%a, %d %b, %Y]'
keep_only_tags = [dict(name='div', attrs={'id':['articulo']}) keep_only_tags = [
dict(name='div', attrs={'id':['articulo']})
,dict(name='div', attrs={'class':['foto']}) ,dict(name='div', attrs={'class':['foto']})
,dict(name='p', attrs={'id':['texto']}) ,dict(name='p', attrs={'id':['texto']})
] ]
@ -25,5 +39,13 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
remove_tags_before = dict(name='div' , attrs={'class':['comparte']}) remove_tags_before = dict(name='div' , attrs={'class':['comparte']})
remove_tags_after = dict(name='div' , attrs={'id':['relacionadas']}) remove_tags_after = dict(name='div' , attrs={'id':['relacionadas']})
extra_css = ' p{text-align: justify; font-size: 100%} body{ text-align: left; font-family: serif; font-size: 100% } h1{ font-family: sans-serif; font-size:150%; font-weight: 700; text-align: justify; } h2{ font-family: sans-serif; font-size:120%; font-weight: 600; text-align: justify } h3{ font-family: sans-serif; font-size:60%; font-weight: 600; text-align: left } h4{ font-family: sans-serif; font-size:80%; font-weight: 600; text-align: left } h5{ font-family: sans-serif; font-size:70%; font-weight: 600; text-align: left }img{margin-bottom: 0.4em} '
def preprocess_html(self, soup):
for alink in soup.findAll('a'):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
return soup
feeds = [(u'Portada', u'http://www.latribunadetalavera.es/rss.html')] feeds = [(u'Portada', u'http://www.latribunadetalavera.es/rss.html')]

View File

@ -0,0 +1,40 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1292550626(BasicNewsRecipe):
title = 'Leduc - Wetaskiwin Pipestone Flyer'
__author__ = 'Brian Hahn'
description = 'News from Alberta, Canada'
oldest_article = 56
max_articles_per_feed = 100
no_stylesheets = True
#delay = 1
use_embedded_content = False
publisher = 'Pipestone Publishing'
category = 'News, Alberta, Canada'
language = 'en_CA'
encoding = 'iso-8859-1'
cover_url = 'http://www.pipestoneflyer.ca/images/calibre-cover.jpg'
remove_tags_before = dict(id='ContentPanel')
remove_tags_after = dict(id='ContentPanel')
remove_tags = [dict(name='div', attrs={'id':'StoryNav'}),dict(name='div', attrs={'id':'BottomAds'}),dict(name='div', attrs={'id':'MoreStoryLinks'})]
extra_css = 'img { margin:5px }'
feeds = [
('Feature', 'http://www.pipestoneflyer.ca/Feature.rss'),
('Editors Desk', 'http://www.pipestoneflyer.ca/Editor%27s%20Desk.rss'),
('Letters', 'http://www.pipestoneflyer.ca/Letters.rss'),
('A Loco Viewpoint', 'http://www.pipestoneflyer.ca/A%20Loco%20Viewpoint.rss'),
('Lifes Doorway', 'http://www.pipestoneflyer.ca/Life%27s%20Doorway.rss'),
('From the Otherside', 'http://www.pipestoneflyer.ca/From%20the%20Otherside.rss'),
('Opinion', 'http://www.pipestoneflyer.ca/Opinion.rss'),
('Community', 'http://www.pipestoneflyer.ca/Community.rss'),
('Sports', 'http://www.pipestoneflyer.ca/Sports.rss'),
('Chambers', 'http://www.pipestoneflyer.ca/Chambers.rss'),
('Government', 'http://www.pipestoneflyer.ca/Government.rss'),
('Environment', 'http://www.pipestoneflyer.ca/Environment.rss'),
('Health', 'http://www.pipestoneflyer.ca/Health.rss'),
('Funnies', 'http://www.pipestoneflyer.ca/Funnies.rss'),
('Faith', 'http://www.pipestoneflyer.ca/Faith.rss'),
('News and Views', 'http://www.pipestoneflyer.ca/News%20and%20Views.rss'),
('Obituaries', 'http://www.pipestoneflyer.ca/Obituaries.rss'),
('Police Blotter', 'http://www.pipestoneflyer.ca/Police%20Blotter.rss'),
]

View File

@ -495,6 +495,22 @@ class SonyReader900Output(SonyReaderOutput):
screen_size = (600, 999) screen_size = (600, 999)
comic_screen_size = screen_size comic_screen_size = screen_size
class GenericEink(SonyReaderOutput):
name = 'Generic e-ink'
short_name = 'generic_eink'
description = _('Suitable for use with any e-ink device')
epub_periodical_format = None
class GenericEinkLarge(GenericEink):
name = 'Generic e-ink large'
short_name = 'generic_eink_large'
description = _('Suitable for use with any large screen e-ink device')
screen_size = (600, 999)
comic_screen_size = screen_size
class JetBook5Output(OutputProfile): class JetBook5Output(OutputProfile):
name = 'JetBook 5-inch' name = 'JetBook 5-inch'
@ -719,6 +735,6 @@ output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output,
iPadOutput, KoboReaderOutput, TabletOutput, SamsungGalaxy, iPadOutput, KoboReaderOutput, TabletOutput, SamsungGalaxy,
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput, SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput,
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput, IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,
BambookOutput, NookColorOutput] BambookOutput, NookColorOutput, GenericEink, GenericEinkLarge]
output_profiles.sort(cmp=lambda x,y:cmp(x.name.lower(), y.name.lower())) output_profiles.sort(cmp=lambda x,y:cmp(x.name.lower(), y.name.lower()))

View File

@ -24,7 +24,7 @@ class N516(USBMS):
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
# Ordered list of supported formats # Ordered list of supported formats
FORMATS = ['epub', 'prc', 'html', 'pdf', 'txt'] FORMATS = ['epub', 'prc', 'mobi', 'html', 'pdf', 'txt']
VENDOR_ID = [0x0525] VENDOR_ID = [0x0525]
PRODUCT_ID = [0xa4a5] PRODUCT_ID = [0xa4a5]

View File

@ -576,10 +576,12 @@ OptionRecommendation(name='sr3_replace',
if not input_fmt: if not input_fmt:
raise ValueError('Input file must have an extension') raise ValueError('Input file must have an extension')
input_fmt = input_fmt[1:].lower() input_fmt = input_fmt[1:].lower()
self.archive_input_tdir = None
if input_fmt in ('zip', 'rar', 'oebzip'): if input_fmt in ('zip', 'rar', 'oebzip'):
self.log('Processing archive...') self.log('Processing archive...')
tdir = PersistentTemporaryDirectory('_plumber') tdir = PersistentTemporaryDirectory('_plumber_archive')
self.input, input_fmt = self.unarchive(self.input, tdir) self.input, input_fmt = self.unarchive(self.input, tdir)
self.archive_input_tdir = tdir
if os.access(self.input, os.R_OK): if os.access(self.input, os.R_OK):
nfp = run_plugins_on_preprocess(self.input, input_fmt) nfp = run_plugins_on_preprocess(self.input, input_fmt)
if nfp != self.input: if nfp != self.input:

View File

@ -36,9 +36,10 @@ def author_to_author_sort(author):
return author return author
author = _bracket_pat.sub('', author).strip() author = _bracket_pat.sub('', author).strip()
tokens = author.split() tokens = author.split()
tokens = tokens[-1:] + tokens[:-1] if tokens and tokens[-1] not in ('Inc.', 'Inc'):
if len(tokens) > 1 and method != 'nocomma': tokens = tokens[-1:] + tokens[:-1]
tokens[0] += ',' if len(tokens) > 1 and method != 'nocomma':
tokens[0] += ','
return ' '.join(tokens) return ' '.join(tokens)
def authors_to_sort_string(authors): def authors_to_sort_string(authors):

View File

@ -121,6 +121,7 @@ class LibraryThingCovers(CoverDownload): # {{{
LIBRARYTHING = 'http://www.librarything.com/isbn/' LIBRARYTHING = 'http://www.librarything.com/isbn/'
def get_cover_url(self, isbn, br, timeout=5.): def get_cover_url(self, isbn, br, timeout=5.):
try: try:
src = br.open_novisit('http://www.librarything.com/isbn/'+isbn, src = br.open_novisit('http://www.librarything.com/isbn/'+isbn,
timeout=timeout).read().decode('utf-8', 'replace') timeout=timeout).read().decode('utf-8', 'replace')
@ -129,6 +130,8 @@ class LibraryThingCovers(CoverDownload): # {{{
err = Exception(_('LibraryThing.com timed out. Try again later.')) err = Exception(_('LibraryThing.com timed out. Try again later.'))
raise err raise err
else: else:
if '/wiki/index.php/HelpThing:Verify' in src:
raise Exception('LibraryThing is blocking calibre.')
s = BeautifulSoup(src) s = BeautifulSoup(src)
url = s.find('td', attrs={'class':'left'}) url = s.find('td', attrs={'class':'left'})
if url is None: if url is None:
@ -142,9 +145,12 @@ class LibraryThingCovers(CoverDownload): # {{{
return url return url
def has_cover(self, mi, ans, timeout=5.): def has_cover(self, mi, ans, timeout=5.):
if not mi.isbn: if not mi.isbn or not self.site_customization:
return False return False
br = browser() from calibre.ebooks.metadata.library_thing import get_browser, login
br = get_browser()
un, _, pw = self.site_customization.partition(':')
login(br, un, pw)
try: try:
self.get_cover_url(mi.isbn, br, timeout=timeout) self.get_cover_url(mi.isbn, br, timeout=timeout)
self.debug('cover for', mi.isbn, 'found') self.debug('cover for', mi.isbn, 'found')
@ -153,9 +159,12 @@ class LibraryThingCovers(CoverDownload): # {{{
self.debug(e) self.debug(e)
def get_covers(self, mi, result_queue, abort, timeout=5.): def get_covers(self, mi, result_queue, abort, timeout=5.):
if not mi.isbn: if not mi.isbn or not self.site_customization:
return return
br = browser() from calibre.ebooks.metadata.library_thing import get_browser, login
br = get_browser()
un, _, pw = self.site_customization.partition(':')
login(br, un, pw)
try: try:
url = self.get_cover_url(mi.isbn, br, timeout=timeout) url = self.get_cover_url(mi.isbn, br, timeout=timeout)
cover_data = br.open_novisit(url).read() cover_data = br.open_novisit(url).read()
@ -164,6 +173,11 @@ class LibraryThingCovers(CoverDownload): # {{{
result_queue.put((False, self.exception_to_string(e), result_queue.put((False, self.exception_to_string(e),
traceback.format_exc(), self.name)) traceback.format_exc(), self.name))
def customization_help(self, gui=False):
ans = _('To use librarything.com you must sign up for a %sfree account%s '
'and enter your username and password separated by a : below.')
return '<p>'+ans%('<a href="http://www.librarything.com">', '</a>')
# }}} # }}}
def check_for_cover(mi, timeout=5.): # {{{ def check_for_cover(mi, timeout=5.): # {{{

View File

@ -251,19 +251,26 @@ class LibraryThing(MetadataSource): # {{{
name = 'LibraryThing' name = 'LibraryThing'
metadata_type = 'social' metadata_type = 'social'
description = _('Downloads series/tags/rating information from librarything.com') description = _('Downloads series/covers/rating information from librarything.com')
def fetch(self): def fetch(self):
if not self.isbn: if not self.isbn or not self.site_customization:
return return
from calibre.ebooks.metadata.library_thing import get_social_metadata from calibre.ebooks.metadata.library_thing import get_social_metadata
un, _, pw = self.site_customization.partition(':')
try: try:
self.results = get_social_metadata(self.title, self.book_author, self.results = get_social_metadata(self.title, self.book_author,
self.publisher, self.isbn) self.publisher, self.isbn, username=un, password=pw)
except Exception, e: except Exception, e:
self.exception = e self.exception = e
self.tb = traceback.format_exc() self.tb = traceback.format_exc()
@property
def string_customization_help(self):
ans = _('To use librarything.com you must sign up for a %sfree account%s '
'and enter your username and password separated by a : below.')
return '<p>'+ans%('<a href="http://www.librarything.com">', '</a>')
# }}} # }}}

View File

@ -4,14 +4,13 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
Fetch cover from LibraryThing.com based on ISBN number. Fetch cover from LibraryThing.com based on ISBN number.
''' '''
import sys, socket, os, re, random import sys, re, random
from lxml import html from lxml import html
import mechanize import mechanize
from calibre import browser, prints from calibre import browser, prints
from calibre.utils.config import OptionParser from calibre.utils.config import OptionParser
from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.ebooks.chardet import strip_encoding_declarations from calibre.ebooks.chardet import strip_encoding_declarations
OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false' OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false'
@ -28,6 +27,12 @@ def get_ua():
] ]
return choices[random.randint(0, len(choices)-1)] return choices[random.randint(0, len(choices)-1)]
_lt_br = None
def get_browser():
global _lt_br
if _lt_br is None:
_lt_br = browser(user_agent=get_ua())
return _lt_br.clone_browser()
class HeadRequest(mechanize.Request): class HeadRequest(mechanize.Request):
@ -35,7 +40,7 @@ class HeadRequest(mechanize.Request):
return 'HEAD' return 'HEAD'
def check_for_cover(isbn, timeout=5.): def check_for_cover(isbn, timeout=5.):
br = browser(user_agent=get_ua()) br = get_browser()
br.set_handle_redirect(False) br.set_handle_redirect(False)
try: try:
br.open_novisit(HeadRequest(OPENLIBRARY%isbn), timeout=timeout) br.open_novisit(HeadRequest(OPENLIBRARY%isbn), timeout=timeout)
@ -54,46 +59,16 @@ class ISBNNotFound(LibraryThingError):
class ServerBusy(LibraryThingError): class ServerBusy(LibraryThingError):
pass pass
def login(br, username, password, force=True): def login(br, username, password):
br.open('http://www.librarything.com') raw = br.open('http://www.librarything.com').read()
if '>Sign out' in raw:
return
br.select_form('signup') br.select_form('signup')
br['formusername'] = username br['formusername'] = username
br['formpassword'] = password br['formpassword'] = password
br.submit() raw = br.submit().read()
if '>Sign out' not in raw:
raise ValueError('Failed to login as %r:%r'%(username, password))
def cover_from_isbn(isbn, timeout=5., username=None, password=None):
src = None
br = browser(user_agent=get_ua())
try:
return br.open(OPENLIBRARY%isbn, timeout=timeout).read(), 'jpg'
except:
pass # Cover not found
if username and password:
try:
login(br, username, password, force=False)
except:
pass
try:
src = br.open_novisit('http://www.librarything.com/isbn/'+isbn,
timeout=timeout).read().decode('utf-8', 'replace')
except Exception, err:
if isinstance(getattr(err, 'args', [None])[0], socket.timeout):
err = LibraryThingError(_('LibraryThing.com timed out. Try again later.'))
raise err
else:
s = BeautifulSoup(src)
url = s.find('td', attrs={'class':'left'})
if url is None:
if s.find('div', attrs={'class':'highloadwarning'}) is not None:
raise ServerBusy(_('Could not fetch cover as server is experiencing high load. Please try again later.'))
raise ISBNNotFound('ISBN: '+isbn+_(' not found.'))
url = url.find('img')
if url is None:
raise LibraryThingError(_('LibraryThing.com server error. Try again later.'))
url = re.sub(r'_S[XY]\d+', '', url['src'])
cover_data = br.open_novisit(url).read()
return cover_data, url.rpartition('.')[-1]
def option_parser(): def option_parser():
parser = OptionParser(usage=\ parser = OptionParser(usage=\
@ -113,15 +88,16 @@ def get_social_metadata(title, authors, publisher, isbn, username=None,
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation(title, authors) mi = MetaInformation(title, authors)
if isbn: if isbn:
br = browser(user_agent=get_ua()) br = get_browser()
if username and password: try:
try: login(br, username, password)
login(br, username, password, force=False)
except:
pass
raw = br.open_novisit('http://www.librarything.com/isbn/' raw = br.open_novisit('http://www.librarything.com/isbn/'
+isbn).read() +isbn).read()
except:
return mi
if '/wiki/index.php/HelpThing:Verify' in raw:
raise Exception('LibraryThing is blocking calibre.')
if not raw: if not raw:
return mi return mi
raw = raw.decode('utf-8', 'replace') raw = raw.decode('utf-8', 'replace')
@ -172,15 +148,46 @@ def main(args=sys.argv):
parser.print_help() parser.print_help()
return 1 return 1
isbn = args[1] isbn = args[1]
mi = get_social_metadata('', [], '', isbn) from calibre.customize.ui import metadata_sources, cover_sources
lt = None
for x in metadata_sources('social'):
if x.name == 'LibraryThing':
lt = x
break
lt('', '', '', isbn, True)
lt.join()
if lt.exception:
print lt.tb
return 1
mi = lt.results
prints(mi) prints(mi)
cover_data, ext = cover_from_isbn(isbn, username=opts.username, mi.isbn = isbn
password=opts.password)
if not ext: lt = None
ext = 'jpg' for x in cover_sources():
oname = os.path.abspath(isbn+'.'+ext) if x.name == 'librarything.com covers':
open(oname, 'w').write(cover_data) lt = x
print 'Cover saved to file', oname break
from threading import Event
from Queue import Queue
ev = Event()
lt.has_cover(mi, ev)
hc = ev.is_set()
print 'Has cover:', hc
if hc:
abort = Event()
temp = Queue()
lt.get_covers(mi, temp, abort)
cover = temp.get_nowait()
if cover[0]:
open(isbn + '.jpg', 'wb').write(cover[1])
print 'Cover saved to:', isbn+'.jpg'
else:
print 'Cover download failed'
print cover[2]
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -8,12 +8,12 @@ from urllib import unquote
from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \ from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \
QByteArray, QTranslator, QCoreApplication, QThread, \ QByteArray, QTranslator, QCoreApplication, QThread, \
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \ QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \ QFileDialog, QFileIconProvider, \
QIcon, QApplication, QDialog, QPushButton, QUrl, QFont QIcon, QApplication, QDialog, QUrl, QFont
ORG_NAME = 'KovidsBrain' ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500' APP_UID = 'libprs500'
from calibre.constants import islinux, iswindows, isosx, isfreebsd, isfrozen from calibre.constants import islinux, iswindows, isfreebsd, isfrozen
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
from calibre.utils.localization import set_qt_translator from calibre.utils.localization import set_qt_translator
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
@ -178,104 +178,40 @@ def is_widescreen():
def extension(path): def extension(path):
return os.path.splitext(path)[1][1:].lower() return os.path.splitext(path)[1][1:].lower()
class CopyButton(QPushButton):
ACTION_KEYS = [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Space]
def copied(self):
self.emit(SIGNAL('copy()'))
self.setDisabled(True)
self.setText(_('Copied'))
def keyPressEvent(self, ev):
try:
if ev.key() in self.ACTION_KEYS:
self.copied()
return
except:
pass
QPushButton.keyPressEvent(self, ev)
def keyReleaseEvent(self, ev):
try:
if ev.key() in self.ACTION_KEYS:
return
except:
pass
QPushButton.keyReleaseEvent(self, ev)
def mouseReleaseEvent(self, ev):
ev.accept()
self.copied()
class MessageBox(QMessageBox):
def __init__(self, type_, title, msg, buttons, parent, det_msg=''):
QMessageBox.__init__(self, type_, title, msg, buttons, parent)
self.title = title
self.msg = msg
self.det_msg = det_msg
self.setDetailedText(det_msg)
# Cannot set keyboard shortcut as the event is not easy to filter
self.cb = CopyButton(_('Copy') if isosx else _('Copy to Clipboard'))
self.connect(self.cb, SIGNAL('copy()'), self.copy_to_clipboard)
self.addButton(self.cb, QMessageBox.ActionRole)
default_button = self.button(self.Ok)
if default_button is None:
default_button = self.button(self.Yes)
if default_button is not None:
self.setDefaultButton(default_button)
def copy_to_clipboard(self):
QApplication.clipboard().setText('%s: %s\n\n%s' %
(self.title, self.msg, self.det_msg))
def warning_dialog(parent, title, msg, det_msg='', show=False, def warning_dialog(parent, title, msg, det_msg='', show=False,
show_copy_button=True): show_copy_button=True):
d = MessageBox(QMessageBox.Warning, 'WARNING: '+title, msg, QMessageBox.Ok, from calibre.gui2.dialogs.message_box import MessageBox
parent, det_msg) d = MessageBox(MessageBox.WARNING, 'WARNING: '+title, msg, det_msg, parent=parent,
d.setEscapeButton(QMessageBox.Ok) show_copy_button=show_copy_button)
d.setIconPixmap(QPixmap(I('dialog_warning.png')))
if not show_copy_button:
d.cb.setVisible(False)
if show: if show:
return d.exec_() return d.exec_()
return d return d
def error_dialog(parent, title, msg, det_msg='', show=False, def error_dialog(parent, title, msg, det_msg='', show=False,
show_copy_button=True): show_copy_button=True):
d = MessageBox(QMessageBox.Critical, 'ERROR: '+title, msg, QMessageBox.Ok, from calibre.gui2.dialogs.message_box import MessageBox
parent, det_msg) d = MessageBox(MessageBox.ERROR, 'ERROR: '+title, msg, det_msg, parent=parent,
d.setIconPixmap(QPixmap(I('dialog_error.png'))) show_copy_button=show_copy_button)
d.setEscapeButton(QMessageBox.Ok)
if not show_copy_button:
d.cb.setVisible(False)
if show: if show:
return d.exec_() return d.exec_()
return d return d
def question_dialog(parent, title, msg, det_msg='', show_copy_button=True, def question_dialog(parent, title, msg, det_msg='', show_copy_button=False,
buttons=QMessageBox.Yes|QMessageBox.No, yes_button=QMessageBox.Yes): buttons=None, yes_button=None):
d = MessageBox(QMessageBox.Question, title, msg, buttons, from calibre.gui2.dialogs.message_box import MessageBox
parent, det_msg) d = MessageBox(MessageBox.QUESTION, title, msg, det_msg, parent=parent,
d.setIconPixmap(QPixmap(I('dialog_question.png'))) show_copy_button=show_copy_button)
d.setEscapeButton(QMessageBox.No) if buttons is not None:
if not show_copy_button: d.bb.setStandardButtons(buttons)
d.cb.setVisible(False)
return d.exec_() == yes_button return d.exec_() == d.Accepted
def info_dialog(parent, title, msg, det_msg='', show=False, def info_dialog(parent, title, msg, det_msg='', show=False,
show_copy_button=True): show_copy_button=True):
d = MessageBox(QMessageBox.Information, title, msg, QMessageBox.Ok, from calibre.gui2.dialogs.message_box import MessageBox
parent, det_msg) d = MessageBox(MessageBox.INFO, title, msg, det_msg, parent=parent,
d.setIconPixmap(QPixmap(I('dialog_information.png'))) show_copy_button=show_copy_button)
if not show_copy_button:
d.cb.setVisible(False)
if show: if show:
return d.exec_() return d.exec_()

View File

@ -100,6 +100,9 @@ class AddAction(InterfaceAction):
mi = MetaInformation(_('Unknown'), dlg.selected_authors) mi = MetaInformation(_('Unknown'), dlg.selected_authors)
self.gui.library_view.model().db.import_book(mi, []) self.gui.library_view.model().db.import_book(mi, [])
self.gui.library_view.model().books_added(num) self.gui.library_view.model().books_added(num)
if hasattr(self.gui, 'db_images'):
self.gui.db_images.reset()
self.gui.tags_view.recount()
def add_isbns(self, books, add_tags=[]): def add_isbns(self, books, add_tags=[]):
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation

View File

@ -17,7 +17,7 @@ from calibre.gui2.actions import InterfaceAction
class GenerateCatalogAction(InterfaceAction): class GenerateCatalogAction(InterfaceAction):
name = 'Generate Catalog' name = 'Generate Catalog'
action_spec = (_('Create catalog of books in your calibre library'), None, None, None) action_spec = (_('Create a catalog of the books in your calibre library'), None, None, None)
dont_add_to = frozenset(['toolbar-device', 'context-menu-device']) dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
def generate_catalog(self): def generate_catalog(self):

View File

@ -343,7 +343,7 @@ class ChooseLibraryAction(InterfaceAction):
db.dirtied(list(db.data.iterallids())) db.dirtied(list(db.data.iterallids()))
info_dialog(self.gui, _('Backup metadata'), info_dialog(self.gui, _('Backup metadata'),
_('Metadata will be backed up while calibre is running, at the ' _('Metadata will be backed up while calibre is running, at the '
'rate of approximately 1 book per second.'), show=True) 'rate of approximately 1 book every three seconds.'), show=True)
def check_library(self): def check_library(self):
db = self.gui.library_view.model().db db = self.gui.library_view.model().db

View File

@ -31,7 +31,7 @@ class ConvertAction(InterfaceAction):
partial(self.convert_ebook, False, bulk=True)) partial(self.convert_ebook, False, bulk=True))
cm.addSeparator() cm.addSeparator()
ac = cm.addAction( ac = cm.addAction(
_('Create catalog of books in your calibre library')) _('Create a catalog of the books in your calibre library'))
ac.triggered.connect(self.gui.iactions['Generate Catalog'].generate_catalog) ac.triggered.connect(self.gui.iactions['Generate Catalog'].generate_catalog)
self.qaction.setMenu(cm) self.qaction.setMenu(cm)
self.qaction.triggered.connect(self.convert_ebook) self.qaction.triggered.connect(self.convert_ebook)

View File

@ -4,6 +4,8 @@ __license__ = 'GPL 3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>' __copyright__ = '2009, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import shutil
from PyQt4.Qt import QString, SIGNAL from PyQt4.Qt import QString, SIGNAL
from calibre.gui2.convert.single import Config, sort_formats_by_preference, \ from calibre.gui2.convert.single import Config, sort_formats_by_preference, \
@ -108,6 +110,11 @@ class BulkConfig(Config):
idx = oidx if -1 < oidx < self._groups_model.rowCount() else 0 idx = oidx if -1 < oidx < self._groups_model.rowCount() else 0
self.groups.setCurrentIndex(self._groups_model.index(idx)) self.groups.setCurrentIndex(self._groups_model.index(idx))
self.stack.setCurrentIndex(idx) self.stack.setCurrentIndex(idx)
try:
shutil.rmtree(self.plumber.archive_input_tdir, ignore_errors=True)
except:
pass
def setup_output_formats(self, db, preferred_output_format): def setup_output_formats(self, db, preferred_output_format):
if preferred_output_format: if preferred_output_format:

View File

@ -6,7 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import sys, cPickle import sys, cPickle, shutil
from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont
@ -224,6 +224,10 @@ class Config(ResizableDialog, Ui_Dialog):
idx = oidx if -1 < oidx < self._groups_model.rowCount() else 0 idx = oidx if -1 < oidx < self._groups_model.rowCount() else 0
self.groups.setCurrentIndex(self._groups_model.index(idx)) self.groups.setCurrentIndex(self._groups_model.index(idx))
self.stack.setCurrentIndex(idx) self.stack.setCurrentIndex(idx)
try:
shutil.rmtree(self.plumber.archive_input_tdir, ignore_errors=True)
except:
pass
def setup_input_output_formats(self, db, book_id, preferred_input_format, def setup_input_output_formats(self, db, book_id, preferred_input_format,

View File

@ -151,12 +151,27 @@ class DateEdit(QDateEdit):
def set_to_today(self): def set_to_today(self):
self.setDate(now()) self.setDate(now())
def set_to_clear(self):
self.setDate(UNDEFINED_QDATE)
class DateTime(Base): class DateTime(Base):
def setup_ui(self, parent): def setup_ui(self, parent):
cm = self.col_metadata cm = self.col_metadata
self.widgets = [QLabel('&'+cm['name']+':', parent), DateEdit(parent), self.widgets = [QLabel('&'+cm['name']+':', parent), DateEdit(parent)]
QLabel(''), QPushButton(_('Set \'%s\' to today')%cm['name'], parent)] self.widgets.append(QLabel(''))
w = QWidget(parent)
self.widgets.append(w)
l = QHBoxLayout()
l.setContentsMargins(0, 0, 0, 0)
w.setLayout(l)
l.addStretch(1)
self.today_button = QPushButton(_('Set \'%s\' to today')%cm['name'], parent)
l.addWidget(self.today_button)
self.clear_button = QPushButton(_('Clear \'%s\'')%cm['name'], parent)
l.addWidget(self.clear_button)
l.addStretch(2)
w = self.widgets[1] w = self.widgets[1]
format = cm['display'].get('date_format','') format = cm['display'].get('date_format','')
if not format: if not format:
@ -165,7 +180,8 @@ class DateTime(Base):
w.setCalendarPopup(True) w.setCalendarPopup(True)
w.setMinimumDate(UNDEFINED_QDATE) w.setMinimumDate(UNDEFINED_QDATE)
w.setSpecialValueText(_('Undefined')) w.setSpecialValueText(_('Undefined'))
self.widgets[3].clicked.connect(w.set_to_today) self.today_button.clicked.connect(w.set_to_today)
self.clear_button.clicked.connect(w.set_to_clear)
def setter(self, val): def setter(self, val):
if val is None: if val is None:
@ -470,11 +486,48 @@ class BulkBase(Base):
self.setter(val) self.setter(val)
def commit(self, book_ids, notify=False): def commit(self, book_ids, notify=False):
if not self.a_c_checkbox.isChecked():
return
val = self.gui_val val = self.gui_val
val = self.normalize_ui_val(val) val = self.normalize_ui_val(val)
if val != self.initial_val: if val != self.initial_val:
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
def make_widgets(self, parent, main_widget_class, extra_label_text=''):
w = QWidget(parent)
self.widgets = [QLabel('&'+self.col_metadata['name']+':', w), w]
l = QHBoxLayout()
l.setContentsMargins(0, 0, 0, 0)
w.setLayout(l)
self.main_widget = main_widget_class(w)
l.addWidget(self.main_widget)
l.setStretchFactor(self.main_widget, 10)
self.a_c_checkbox = QCheckBox( _('Apply changes'), w)
l.addWidget(self.a_c_checkbox)
self.ignore_change_signals = True
# connect to the various changed signals so we can auto-update the
# apply changes checkbox
if hasattr(self.main_widget, 'editTextChanged'):
# editable combobox widgets
self.main_widget.editTextChanged.connect(self.a_c_checkbox_changed)
if hasattr(self.main_widget, 'textChanged'):
# lineEdit widgets
self.main_widget.textChanged.connect(self.a_c_checkbox_changed)
if hasattr(self.main_widget, 'currentIndexChanged'):
# combobox widgets
self.main_widget.currentIndexChanged[int].connect(self.a_c_checkbox_changed)
if hasattr(self.main_widget, 'valueChanged'):
# spinbox widgets
self.main_widget.valueChanged.connect(self.a_c_checkbox_changed)
if hasattr(self.main_widget, 'dateChanged'):
# dateEdit widgets
self.main_widget.dateChanged.connect(self.a_c_checkbox_changed)
def a_c_checkbox_changed(self):
if not self.ignore_change_signals:
self.a_c_checkbox.setChecked(True)
class BulkBool(BulkBase, Bool): class BulkBool(BulkBase, Bool):
def get_initial_value(self, book_ids): def get_initial_value(self, book_ids):
@ -484,58 +537,144 @@ class BulkBool(BulkBase, Bool):
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None: if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
val = False val = False
if value is not None and value != val: if value is not None and value != val:
return 'nochange' return None
value = val value = val
return value return value
def setup_ui(self, parent): def setup_ui(self, parent):
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), self.make_widgets(parent, QComboBox)
QComboBox(parent)] items = [_('Yes'), _('No'), _('Undefined')]
w = self.widgets[1] icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
items = [_('Yes'), _('No'), _('Undefined'), _('Do not change')] self.main_widget.blockSignals(True)
icons = [I('ok.png'), I('list_remove.png'), I('blank.png'), I('blank.png')]
for icon, text in zip(icons, items): for icon, text in zip(icons, items):
w.addItem(QIcon(icon), text) self.main_widget.addItem(QIcon(icon), text)
self.main_widget.blockSignals(False)
def getter(self): def getter(self):
val = self.widgets[1].currentIndex() val = self.main_widget.currentIndex()
return {3: 'nochange', 2: None, 1: False, 0: True}[val] return {2: None, 1: False, 0: True}[val]
def setter(self, val): def setter(self, val):
val = {'nochange': 3, None: 2, False: 1, True: 0}[val] val = {None: 2, False: 1, True: 0}[val]
self.widgets[1].setCurrentIndex(val) self.main_widget.setCurrentIndex(val)
self.ignore_change_signals = False
def commit(self, book_ids, notify=False): def commit(self, book_ids, notify=False):
if not self.a_c_checkbox.isChecked():
return
val = self.gui_val val = self.gui_val
val = self.normalize_ui_val(val) val = self.normalize_ui_val(val)
if val != self.initial_val and val != 'nochange': if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None: val = False
val = False self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
class BulkInt(BulkBase, Int): class BulkInt(BulkBase):
pass
class BulkFloat(BulkBase, Float): def setup_ui(self, parent):
pass self.make_widgets(parent, QSpinBox)
self.main_widget.setRange(-100, sys.maxint)
self.main_widget.setSpecialValueText(_('Undefined'))
self.main_widget.setSingleStep(1)
class BulkRating(BulkBase, Rating): def setter(self, val):
pass if val is None:
val = self.main_widget.minimum()
else:
val = int(val)
self.main_widget.setValue(val)
self.ignore_change_signals = False
class BulkDateTime(BulkBase, DateTime): def getter(self):
pass val = self.main_widget.value()
if val == self.main_widget.minimum():
val = None
return val
class BulkFloat(BulkInt):
def setup_ui(self, parent):
self.make_widgets(parent, QDoubleSpinBox)
self.main_widget.setRange(-100., float(sys.maxint))
self.main_widget.setDecimals(2)
self.main_widget.setSpecialValueText(_('Undefined'))
self.main_widget.setSingleStep(1)
class BulkRating(BulkBase):
def setup_ui(self, parent):
self.make_widgets(parent, QSpinBox)
self.main_widget.setRange(0, 5)
self.main_widget.setSuffix(' '+_('star(s)'))
self.main_widget.setSpecialValueText(_('Unrated'))
self.main_widget.setSingleStep(1)
def setter(self, val):
if val is None:
val = 0
self.main_widget.setValue(int(round(val/2.)))
self.ignore_change_signals = False
def getter(self):
val = self.main_widget.value()
if val == 0:
val = None
else:
val *= 2
return val
class BulkDateTime(BulkBase):
def setup_ui(self, parent):
cm = self.col_metadata
self.make_widgets(parent, DateEdit)
self.widgets.append(QLabel(''))
w = QWidget(parent)
self.widgets.append(w)
l = QHBoxLayout()
l.setContentsMargins(0, 0, 0, 0)
w.setLayout(l)
l.addStretch(1)
self.today_button = QPushButton(_('Set \'%s\' to today')%cm['name'], parent)
l.addWidget(self.today_button)
self.clear_button = QPushButton(_('Clear \'%s\'')%cm['name'], parent)
l.addWidget(self.clear_button)
l.addStretch(2)
w = self.main_widget
format = cm['display'].get('date_format','')
if not format:
format = 'dd MMM yyyy'
w.setDisplayFormat(format)
w.setCalendarPopup(True)
w.setMinimumDate(UNDEFINED_QDATE)
w.setSpecialValueText(_('Undefined'))
self.today_button.clicked.connect(w.set_to_today)
self.clear_button.clicked.connect(w.set_to_clear)
def setter(self, val):
if val is None:
val = self.main_widget.minimumDate()
else:
val = QDate(val.year, val.month, val.day)
self.main_widget.setDate(val)
self.ignore_change_signals = False
def getter(self):
val = self.main_widget.date()
if val == UNDEFINED_QDATE:
val = None
else:
val = qt_to_dt(val)
return val
class BulkSeries(BulkBase): class BulkSeries(BulkBase):
def setup_ui(self, parent): def setup_ui(self, parent):
self.make_widgets(parent, EnComboBox)
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) self.main_widget.setSizeAdjustPolicy(self.main_widget.AdjustToMinimumContentsLengthWithIcon)
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon) self.main_widget.setMinimumContentsLength(25)
w.setMinimumContentsLength(25)
self.name_widget = w
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
self.widgets.append(QLabel('', parent)) self.widgets.append(QLabel('', parent))
w = QWidget(parent) w = QWidget(parent)
layout = QHBoxLayout(w) layout = QHBoxLayout(w)
@ -555,15 +694,24 @@ class BulkSeries(BulkBase):
layout.addWidget(self.series_start_number) layout.addWidget(self.series_start_number)
layout.addItem(QSpacerItem(20, 10, QSizePolicy.Expanding, QSizePolicy.Minimum)) layout.addItem(QSpacerItem(20, 10, QSizePolicy.Expanding, QSizePolicy.Minimum))
self.widgets.append(w) self.widgets.append(w)
self.idx_widget.stateChanged.connect(self.check_changed_checkbox)
self.force_number.stateChanged.connect(self.check_changed_checkbox)
self.series_start_number.valueChanged.connect(self.check_changed_checkbox)
self.remove_series.stateChanged.connect(self.check_changed_checkbox)
self.ignore_change_signals = False
def check_changed_checkbox(self):
self.a_c_checkbox.setChecked(True)
def initialize(self, book_id): def initialize(self, book_id):
self.idx_widget.setChecked(False) self.idx_widget.setChecked(False)
for c in self.all_values: for c in self.all_values:
self.name_widget.addItem(c) self.main_widget.addItem(c)
self.name_widget.setEditText('') self.main_widget.setEditText('')
self.a_c_checkbox.setChecked(False)
def getter(self): def getter(self):
n = unicode(self.name_widget.currentText()).strip() n = unicode(self.main_widget.currentText()).strip()
i = self.idx_widget.checkState() i = self.idx_widget.checkState()
f = self.force_number.checkState() f = self.force_number.checkState()
s = self.series_start_number.value() s = self.series_start_number.value()
@ -571,6 +719,8 @@ class BulkSeries(BulkBase):
return n, i, f, s, r return n, i, f, s, r
def commit(self, book_ids, notify=False): def commit(self, book_ids, notify=False):
if not self.a_c_checkbox.isChecked():
return
val, update_indices, force_start, at_value, clear = self.gui_val val, update_indices, force_start, at_value, clear = self.gui_val
val = None if clear else self.normalize_ui_val(val) val = None if clear else self.normalize_ui_val(val)
if clear or val != '': if clear or val != '':
@ -598,9 +748,9 @@ class BulkEnumeration(BulkBase, Enumeration):
def get_initial_value(self, book_ids): def get_initial_value(self, book_ids):
value = None value = None
ret_value = None first = True
dialog_shown = False dialog_shown = False
for i,book_id in enumerate(book_ids): for book_id in book_ids:
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)
if val and val not in self.col_metadata['display']['enum_values']: if val and val not in self.col_metadata['display']['enum_values']:
if not dialog_shown: if not dialog_shown:
@ -610,44 +760,32 @@ class BulkEnumeration(BulkBase, Enumeration):
self.col_metadata['name']), self.col_metadata['name']),
show=True, show_copy_button=False) show=True, show_copy_button=False)
dialog_shown = True dialog_shown = True
ret_value = ' nochange ' if first:
elif (value is not None and value != val) or (val and i != 0): value = val
ret_value = ' nochange ' first = False
value = val elif value != val:
if ret_value is None: value = None
return value if not value:
return ret_value self.ignore_change_signals = False
return value
def setup_ui(self, parent): def setup_ui(self, parent):
self.parent = parent self.make_widgets(parent, QComboBox)
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
QComboBox(parent)]
w = self.widgets[1]
vals = self.col_metadata['display']['enum_values'] vals = self.col_metadata['display']['enum_values']
w.addItem('Do Not Change') self.main_widget.blockSignals(True)
w.addItem('') self.main_widget.addItem('')
for v in vals: self.main_widget.addItems(vals)
w.addItem(v) self.main_widget.blockSignals(False)
def getter(self): def getter(self):
if self.widgets[1].currentIndex() == 0: return unicode(self.main_widget.currentText())
return ' nochange '
return unicode(self.widgets[1].currentText())
def setter(self, val): def setter(self, val):
if val == ' nochange ': if val is None:
self.widgets[1].setCurrentIndex(0) self.main_widget.setCurrentIndex(0)
else: else:
if val is None: self.main_widget.setCurrentIndex(self.main_widget.findText(val))
self.widgets[1].setCurrentIndex(1) self.ignore_change_signals = False
else:
self.widgets[1].setCurrentIndex(self.widgets[1].findText(val))
def commit(self, book_ids, notify=False):
val = self.gui_val
val = self.normalize_ui_val(val)
if val != self.initial_val and val != ' nochange ':
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
class RemoveTags(QWidget): class RemoveTags(QWidget):
@ -658,11 +796,10 @@ class RemoveTags(QWidget):
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
self.tags_box = CompleteLineEdit(parent, values) self.tags_box = CompleteLineEdit(parent, values)
layout.addWidget(self.tags_box, stretch = 1) layout.addWidget(self.tags_box, stretch=3)
# self.tags_box.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.checkbox = QCheckBox(_('Remove all tags'), parent) self.checkbox = QCheckBox(_('Remove all tags'), parent)
layout.addWidget(self.checkbox) layout.addWidget(self.checkbox)
layout.addStretch(1)
self.setLayout(layout) self.setLayout(layout)
self.connect(self.checkbox, SIGNAL('stateChanged(int)'), self.box_touched) self.connect(self.checkbox, SIGNAL('stateChanged(int)'), self.box_touched)
@ -679,39 +816,45 @@ 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']:
w = CompleteLineEdit(parent, values) self.make_widgets(parent, CompleteLineEdit,
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) extra_label_text=_('tags to add'))
self.widgets = [QLabel('&'+self.col_metadata['name']+': ' + self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
_('tags to add'), parent), w] self.adding_widget = self.main_widget
self.adding_widget = w
w = RemoveTags(parent, values) w = RemoveTags(parent, values)
self.widgets.append(QLabel('&'+self.col_metadata['name']+': ' + self.widgets.append(QLabel('&'+self.col_metadata['name']+': ' +
_('tags to remove'), parent)) _('tags to remove'), parent))
self.widgets.append(w) self.widgets.append(w)
self.removing_widget = w self.removing_widget = w
w.tags_box.textChanged.connect(self.a_c_checkbox_changed)
w.checkbox.stateChanged.connect(self.a_c_checkbox_changed)
else: else:
w = EnComboBox(parent) self.make_widgets(parent, EnComboBox)
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon) self.main_widget.setSizeAdjustPolicy(
w.setMinimumContentsLength(25) self.main_widget.AdjustToMinimumContentsLengthWithIcon)
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w] self.main_widget.setMinimumContentsLength(25)
self.ignore_change_signals = False
def initialize(self, book_ids): def initialize(self, book_ids):
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
self.widgets[1].update_items_cache(self.all_values) self.main_widget.update_items_cache(self.all_values)
else: else:
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
self.main_widget.blockSignals(True)
for i, c in enumerate(self.all_values): for i, c in enumerate(self.all_values):
if c == val: if c == val:
idx = i idx = i
self.widgets[1].addItem(c) self.main_widget.addItem(c)
self.widgets[1].setEditText('') self.main_widget.setEditText('')
if idx is not None: if idx is not None:
self.widgets[1].setCurrentIndex(idx) self.main_widget.setCurrentIndex(idx)
self.main_widget.blockSignals(False)
def commit(self, book_ids, notify=False): def commit(self, book_ids, notify=False):
if not self.a_c_checkbox.isChecked():
return
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
remove_all, adding, rtext = self.gui_val remove_all, adding, rtext = self.gui_val
remove = set() remove = set()
@ -740,7 +883,7 @@ class BulkText(BulkBase):
unicode(self.adding_widget.text()), \ unicode(self.adding_widget.text()), \
unicode(self.removing_widget.tags_box.text()) unicode(self.removing_widget.tags_box.text())
val = unicode(self.widgets[1].currentText()).strip() val = unicode(self.main_widget.currentText()).strip()
if not val: if not val:
val = None val = None
return val return val

View File

@ -8,15 +8,12 @@ __docformat__ = 'restructuredtext en'
import os, sys import os, sys
from PyQt4 import QtGui
from PyQt4.Qt import QDialog, SIGNAL
from calibre.customize.ui import config from calibre.customize.ui import config
from calibre.gui2.dialogs.catalog_ui import Ui_Dialog from calibre.gui2.dialogs.catalog_ui import Ui_Dialog
from calibre.gui2 import dynamic from calibre.gui2 import dynamic, ResizableDialog
from calibre.customize.ui import catalog_plugins from calibre.customize.ui import catalog_plugins
class Catalog(QDialog, Ui_Dialog): class Catalog(ResizableDialog, Ui_Dialog):
''' Catalog Dialog builder''' ''' Catalog Dialog builder'''
def __init__(self, parent, dbspec, ids, db): def __init__(self, parent, dbspec, ids, db):
@ -24,10 +21,8 @@ class Catalog(QDialog, Ui_Dialog):
from calibre import prints as info from calibre import prints as info
from PyQt4.uic import compileUi from PyQt4.uic import compileUi
QDialog.__init__(self, parent) ResizableDialog.__init__(self, parent)
# Run the dialog setup generated from catalog.ui
self.setupUi(self)
self.dbspec, self.ids = dbspec, ids self.dbspec, self.ids = dbspec, ids
# Display the number of books we've been passed # Display the number of books we've been passed
@ -120,11 +115,13 @@ class Catalog(QDialog, Ui_Dialog):
self.sync.setChecked(dynamic.get('catalog_sync_to_device', True)) self.sync.setChecked(dynamic.get('catalog_sync_to_device', True))
self.format.currentIndexChanged.connect(self.show_plugin_tab) self.format.currentIndexChanged.connect(self.show_plugin_tab)
self.connect(self.buttonBox.button(QtGui.QDialogButtonBox.Apply), self.buttonBox.button(self.buttonBox.Apply).clicked.connect(self.apply)
SIGNAL("clicked()"),
self.apply)
self.show_plugin_tab(None) self.show_plugin_tab(None)
geom = dynamic.get('catalog_window_geom', None)
if geom is not None:
self.restoreGeometry(bytes(geom))
def show_plugin_tab(self, idx): def show_plugin_tab(self, idx):
cf = unicode(self.format.currentText()).lower() cf = unicode(self.format.currentText()).lower()
while self.tabs.count() > 1: while self.tabs.count() > 1:
@ -157,8 +154,9 @@ class Catalog(QDialog, Ui_Dialog):
dynamic.set('catalog_last_used_title', self.catalog_title) dynamic.set('catalog_last_used_title', self.catalog_title)
self.catalog_sync = bool(self.sync.isChecked()) self.catalog_sync = bool(self.sync.isChecked())
dynamic.set('catalog_sync_to_device', self.catalog_sync) dynamic.set('catalog_sync_to_device', self.catalog_sync)
dynamic.set('catalog_window_geom', bytearray(self.saveGeometry()))
def apply(self): def apply(self, *args):
# Store current values without building catalog # Store current values without building catalog
self.save_catalog_settings() self.save_catalog_settings()
if self.tabs.count() > 1: if self.tabs.count() > 1:
@ -166,4 +164,9 @@ class Catalog(QDialog, Ui_Dialog):
def accept(self): def accept(self):
self.save_catalog_settings() self.save_catalog_settings()
return QDialog.accept(self) return ResizableDialog.accept(self)
def reject(self):
dynamic.set('catalog_window_geom', bytearray(self.saveGeometry()))
ResizableDialog.reject(self)

View File

@ -14,7 +14,7 @@
<string>Generate catalog</string> <string>Generate catalog</string>
</property> </property>
<property name="windowIcon"> <property name="windowIcon">
<iconset> <iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/library.png</normaloff>:/images/library.png</iconset> <normaloff>:/images/library.png</normaloff>:/images/library.png</iconset>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
@ -31,81 +31,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0" colspan="2">
<widget class="QTabWidget" name="tabs">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>650</width>
<height>575</height>
</size>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Catalog options</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Catalog &amp;format:</string>
</property>
<property name="buddy">
<cstring>format</cstring>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QComboBox" name="format"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Catalog &amp;title (existing catalog with the same title will be replaced):</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>title</cstring>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLineEdit" name="title"/>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="sync">
<property name="text">
<string>&amp;Send catalog to device automatically</string>
</property>
</widget>
</item>
<item row="2" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>299</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation"> <property name="orientation">
@ -116,10 +41,110 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0" colspan="2">
<widget class="QScrollArea" name="scrollArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>666</width>
<height>599</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QTabWidget" name="tabs">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>650</width>
<height>575</height>
</size>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Catalog options</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Catalog &amp;format:</string>
</property>
<property name="buddy">
<cstring>format</cstring>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QComboBox" name="format"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Catalog &amp;title (existing catalog with the same title will be replaced):</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>title</cstring>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLineEdit" name="title"/>
</item>
<item row="2" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="sync">
<property name="text">
<string>&amp;Send catalog to device automatically</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources> <resources>
<include location="../../../work/calibre/resources/images.qrc"/> <include location="../../../../resources/images.qrc"/>
</resources> </resources>
<connections> <connections>
<connection> <connection>

View File

@ -0,0 +1,104 @@
#!/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 QDialog, QIcon, QApplication, QSize, QKeySequence, \
QAction, Qt
from calibre.constants import __version__
from calibre.gui2.dialogs.message_box_ui import Ui_Dialog
class MessageBox(QDialog, Ui_Dialog):
ERROR = 0
WARNING = 1
INFO = 2
QUESTION = 3
def __init__(self, type_, title, msg, det_msg='', show_copy_button=True,
parent=None):
QDialog.__init__(self, parent)
icon = {
self.ERROR : 'error',
self.WARNING: 'warning',
self.INFO: 'information',
self.QUESTION: 'question',
}[type_]
icon = 'dialog_%s.png'%icon
self.icon = QIcon(I(icon))
self.setupUi(self)
self.setWindowTitle(title)
self.setWindowIcon(self.icon)
self.icon_label.setPixmap(self.icon.pixmap(128, 128))
self.msg.setText(msg)
self.det_msg.setPlainText(det_msg)
self.det_msg.setVisible(False)
if det_msg:
self.show_det_msg = _('Show &details')
self.hide_det_msg = _('Hide &details')
self.det_msg_toggle = self.bb.addButton(self.show_det_msg, self.bb.ActionRole)
self.det_msg_toggle.clicked.connect(self.toggle_det_msg)
self.det_msg_toggle.setToolTip(
_('Show detailed information about this error'))
if show_copy_button:
self.ctc_button = self.bb.addButton(_('&Copy to clipboard'),
self.bb.ActionRole)
self.ctc_button.clicked.connect(self.copy_to_clipboard)
self.copy_action = QAction(self)
self.addAction(self.copy_action)
self.copy_action.setShortcuts(QKeySequence.Copy)
self.copy_action.triggered.connect(self.copy_to_clipboard)
self.is_question = type_ == self.QUESTION
if self.is_question:
self.bb.setStandardButtons(self.bb.Yes|self.bb.No)
self.bb.button(self.bb.Yes).setDefault(True)
else:
self.bb.button(self.bb.Ok).setDefault(True)
self.do_resize()
def toggle_det_msg(self, *args):
vis = self.det_msg.isVisible()
self.det_msg_toggle.setText(self.show_det_msg if vis else
self.hide_det_msg)
self.det_msg.setVisible(not vis)
self.do_resize()
def do_resize(self):
sz = self.sizeHint() + QSize(100, 0)
sz.setWidth(min(500, sz.width()))
sz.setHeight(min(500, sz.height()))
self.resize(sz)
def copy_to_clipboard(self, *args):
QApplication.clipboard().setText(
'calibre, version %s\n%s: %s\n\n%s' %
(__version__, unicode(self.windowTitle()),
unicode(self.msg.text()),
unicode(self.det_msg.toPlainText())))
self.ctc_button.setText(_('Copied'))
def showEvent(self, ev):
ret = QDialog.showEvent(self, ev)
if self.is_question:
self.bb.button(self.bb.Yes).setFocus(Qt.OtherFocusReason)
else:
self.bb.button(self.bb.Ok).setFocus(Qt.OtherFocusReason)
return ret
if __name__ == '__main__':
app = QApplication([])
from calibre.gui2 import question_dialog
print question_dialog(None, 'title', 'msg <a href="http://google.com">goog</a> ',
det_msg='det '*1000,
show_copy_button=True)

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>497</width>
<height>235</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="icon_label">
<property name="maximumSize">
<size>
<width>68</width>
<height>68</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../../../../resources/images.qrc">:/images/dialog_warning.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="msg">
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QPlainTextEdit" name="det_msg">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QDialogButtonBox" name="bb">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>bb</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>bb</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>962</width> <width>962</width>
<height>727</height> <height>645</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -45,7 +45,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>954</width> <width>954</width>
<height>666</height> <height>584</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
@ -996,8 +996,8 @@ not multiple and the destination field is multiple</string>
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>197</width> <width>938</width>
<height>60</height> <height>268</height>
</rect> </rect>
</property> </property>
<layout class="QGridLayout" name="testgrid"> <layout class="QGridLayout" name="testgrid">

View File

@ -2,14 +2,14 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.QtCore import SIGNAL, Qt from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem
from calibre.gui2.dialogs.tag_categories_ui import Ui_TagCategories from calibre.gui2.dialogs.tag_categories_ui import Ui_TagCategories
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2 import error_dialog
from calibre.constants import islinux from calibre.constants import islinux
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key, strcmp
class Item: class Item:
def __init__(self, name, label, index, icon, exists): def __init__(self, name, label, index, icon, exists):
@ -102,12 +102,13 @@ class TagCategories(QDialog, Ui_TagCategories):
self.category_filter_box.addItem(v) self.category_filter_box.addItem(v)
self.current_cat_name = None self.current_cat_name = None
self.connect(self.apply_button, SIGNAL('clicked()'), self.apply_tags) self.apply_button.clicked.connect(self.apply_button_clicked)
self.connect(self.unapply_button, SIGNAL('clicked()'), self.unapply_tags) self.unapply_button.clicked.connect(self.unapply_button_clicked)
self.connect(self.add_category_button, SIGNAL('clicked()'), self.add_category) self.add_category_button.clicked.connect(self.add_category)
self.connect(self.category_box, SIGNAL('currentIndexChanged(int)'), self.select_category) self.rename_category_button.clicked.connect(self.rename_category)
self.connect(self.category_filter_box, SIGNAL('currentIndexChanged(int)'), self.display_filtered_categories) self.category_box.currentIndexChanged[int].connect(self.select_category)
self.connect(self.delete_category_button, SIGNAL('clicked()'), self.del_category) self.category_filter_box.currentIndexChanged[int].connect(self.display_filtered_categories)
self.delete_category_button.clicked.connect(self.del_category)
if islinux: if islinux:
self.available_items_box.itemDoubleClicked.connect(self.apply_tags) self.available_items_box.itemDoubleClicked.connect(self.apply_tags)
else: else:
@ -119,6 +120,9 @@ class TagCategories(QDialog, Ui_TagCategories):
l = self.category_box.findText(on_category) l = self.category_box.findText(on_category)
if l >= 0: if l >= 0:
self.category_box.setCurrentIndex(l) self.category_box.setCurrentIndex(l)
if self.current_cat_name is None:
self.category_box.setCurrentIndex(0)
self.select_category(0)
def make_list_widget(self, item): def make_list_widget(self, item):
n = item.name if item.exists else item.name + _(' (not on any book)') n = item.name if item.exists else item.name + _(' (not on any book)')
@ -137,6 +141,9 @@ class TagCategories(QDialog, Ui_TagCategories):
for index in self.applied_items: for index in self.applied_items:
self.applied_items_box.addItem(self.make_list_widget(self.all_items[index])) self.applied_items_box.addItem(self.make_list_widget(self.all_items[index]))
def apply_button_clicked(self):
self.apply_tags(node=None)
def apply_tags(self, node=None): def apply_tags(self, node=None):
if self.current_cat_name is None: if self.current_cat_name is None:
return return
@ -148,6 +155,9 @@ class TagCategories(QDialog, Ui_TagCategories):
self.applied_items.sort(key=lambda x:sort_key(self.all_items[x].name)) self.applied_items.sort(key=lambda x:sort_key(self.all_items[x].name))
self.display_filtered_categories(None) self.display_filtered_categories(None)
def unapply_button_clicked(self):
self.unapply_tags(node=None)
def unapply_tags(self, node=None): def unapply_tags(self, node=None):
nodes = self.applied_items_box.selectedItems() if node is None else [node] nodes = self.applied_items_box.selectedItems() if node is None else [node]
for node in nodes: for node in nodes:
@ -160,15 +170,40 @@ class TagCategories(QDialog, Ui_TagCategories):
cat_name = unicode(self.input_box.text()).strip() cat_name = unicode(self.input_box.text()).strip()
if cat_name == '': if cat_name == '':
return False return False
for c in self.categories:
if strcmp(c, cat_name) == 0:
error_dialog(self, _('Name already used'),
_('That name is already used, perhaps with different case.')).exec_()
return False
if cat_name not in self.categories: if cat_name not in self.categories:
self.category_box.clear() self.category_box.clear()
self.current_cat_name = cat_name self.current_cat_name = cat_name
self.categories[cat_name] = [] self.categories[cat_name] = []
self.applied_items = [] self.applied_items = []
self.populate_category_list() self.populate_category_list()
self.category_box.setCurrentIndex(self.category_box.findText(cat_name)) self.input_box.clear()
else: self.category_box.setCurrentIndex(self.category_box.findText(cat_name))
self.select_category(self.category_box.findText(cat_name)) return True
def rename_category(self):
self.save_category()
cat_name = unicode(self.input_box.text()).strip()
if cat_name == '':
return False
if not self.current_cat_name:
return False
for c in self.categories:
if strcmp(c, cat_name) == 0:
error_dialog(self, _('Name already used'),
_('That name is already used, perhaps with different case.')).exec_()
return False
# The order below is important because of signals
self.categories[cat_name] = self.categories[self.current_cat_name]
del self.categories[self.current_cat_name]
self.current_cat_name = None
self.populate_category_list()
self.input_box.clear()
self.category_box.setCurrentIndex(self.category_box.findText(cat_name))
return True return True
def del_category(self): def del_category(self):
@ -196,7 +231,6 @@ class TagCategories(QDialog, Ui_TagCategories):
def accept(self): def accept(self):
self.save_category() self.save_category()
self.db.prefs['user_categories'] = self.categories
QDialog.accept(self) QDialog.accept(self)
def save_category(self): def save_category(self):
@ -208,5 +242,7 @@ class TagCategories(QDialog, Ui_TagCategories):
self.categories[self.current_cat_name] = l self.categories[self.current_cat_name] = l
def populate_category_list(self): def populate_category_list(self):
for n in sorted(self.categories.keys(), key=sort_key): self.category_box.blockSignals(True)
self.category_box.addItem(n) self.category_box.clear()
self.category_box.addItems(sorted(self.categories.keys(), key=sort_key))
self.category_box.blockSignals(False)

View File

@ -18,7 +18,139 @@
<normaloff>:/images/chapters.png</normaloff>:/images/chapters.png</iconset> <normaloff>:/images/chapters.png</normaloff>:/images/chapters.png</iconset>
</property> </property>
<layout class="QGridLayout"> <layout class="QGridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Category name: </string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>category_box</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="category_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>160</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>145</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Select a category to edit</string>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
<widget class="QToolButton" name="delete_category_button">
<property name="toolTip">
<string>Delete this selected tag category</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/minus.png</normaloff>:/images/minus.png</iconset>
</property>
</widget>
</item>
<item row="0" column="2">
<layout class="QHBoxLayout">
<item>
<widget class="QLineEdit" name="input_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>60</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Enter a category name, then use the add button or the rename button</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="add_category_button">
<property name="toolTip">
<string>Add a new category</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/plus.png</normaloff>:/images/plus.png
</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="3">
<widget class="QToolButton" name="rename_category_button">
<property name="toolTip">
<string>Rename the current category to the what is in the box</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/edit-undo.png</normaloff>:/images/edit-undo.png</iconset>
</property>
</widget>
</item>
<item row="1" column="0"> <item row="1" column="0">
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Category filter: </string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="category_filter_box">
<property name="toolTip">
<string>Select the content kind of the new category</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<layout class="QVBoxLayout"> <layout class="QVBoxLayout">
<item> <item>
<layout class="QHBoxLayout"> <layout class="QHBoxLayout">
@ -66,7 +198,7 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="1" column="1"> <item row="2" column="1">
<layout class="QVBoxLayout"> <layout class="QVBoxLayout">
<item> <item>
<spacer> <spacer>
@ -110,7 +242,7 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="1" column="2"> <item row="2" column="2">
<layout class="QVBoxLayout"> <layout class="QVBoxLayout">
<item> <item>
<layout class="QHBoxLayout"> <layout class="QHBoxLayout">
@ -151,7 +283,7 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="1" column="3"> <item row="2" column="3">
<layout class="QVBoxLayout"> <layout class="QVBoxLayout">
<item> <item>
<spacer> <spacer>
@ -195,7 +327,7 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="3" column="0" colspan="4"> <item row="4" column="0" colspan="4">
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
@ -208,141 +340,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0" colspan="4">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Category name: </string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>category_box</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="category_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>160</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>145</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Select a category to edit</string>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QToolButton" name="delete_category_button">
<property name="toolTip">
<string>Delete this selected tag category</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/minus.png</normaloff>:/images/minus.png</iconset>
</property>
</widget>
</item>
<item row="0" column="3">
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="4">
<widget class="QLineEdit" name="input_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>60</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Enter a new category name. Select the kind before adding it.</string>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QToolButton" name="add_category_button">
<property name="toolTip">
<string>Add the new category</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/plus.png</normaloff>:/images/plus.png</iconset>
</property>
</widget>
</item>
<item row="1" column="5">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Category filter: </string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="category_filter_box">
<property name="toolTip">
<string>Select the content kind of the new category</string>
</property>
</widget>
</item>
</layout>
</item>
</layout> </layout>
</widget> </widget>
<resources> <resources>

View File

@ -64,6 +64,8 @@ class TagDelegate(QItemDelegate): # {{{
# }}} # }}}
TAG_SEARCH_STATES = {'clear': 0, 'mark_plus': 1, 'mark_minus': 2}
class TagsView(QTreeView): # {{{ class TagsView(QTreeView): # {{{
refresh_required = pyqtSignal() refresh_required = pyqtSignal()
@ -177,9 +179,16 @@ class TagsView(QTreeView): # {{{
return joiner.join(tokens) return joiner.join(tokens)
def toggle(self, index): def toggle(self, index):
self._toggle(index, None)
def _toggle(self, index, set_to):
'''
set_to: if None, advance the state. Otherwise must be one of the values
in TAG_SEARCH_STATES
'''
modifiers = int(QApplication.keyboardModifiers()) modifiers = int(QApplication.keyboardModifiers())
exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT) exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
if self._model.toggle(index, exclusive): if self._model.toggle(index, exclusive, set_to=set_to):
self.tags_marked.emit(self.search_string) self.tags_marked.emit(self.search_string)
def conditional_clear(self, search_string): def conditional_clear(self, search_string):
@ -187,7 +196,7 @@ class TagsView(QTreeView): # {{{
self.clear() self.clear()
def context_menu_handler(self, action=None, category=None, def context_menu_handler(self, action=None, category=None,
key=None, index=None, negate=None): key=None, index=None, search_state=None):
if not action: if not action:
return return
try: try:
@ -201,11 +210,10 @@ class TagsView(QTreeView): # {{{
self.user_category_edit.emit(category) self.user_category_edit.emit(category)
return return
if action == 'search': if action == 'search':
self.tags_marked.emit(('not ' if negate else '') + self._toggle(index, set_to=search_state)
category + ':"=' + key + '"')
return return
if action == 'search_category': if action == 'search_category':
self.tags_marked.emit(category + ':' + str(not negate)) self.tags_marked.emit(key + ':' + search_state)
return return
if action == 'manage_searches': if action == 'manage_searches':
self.saved_search_edit.emit(category) self.saved_search_edit.emit(category)
@ -270,20 +278,16 @@ class TagsView(QTreeView): # {{{
partial(self.context_menu_handler, partial(self.context_menu_handler,
action='edit_author_sort', index=tag_id)) action='edit_author_sort', index=tag_id))
# Add the search for value items # Add the search for value items
n = tag_name
c = category
if self.db.field_metadata[key]['datatype'] == 'rating':
n = str(len(tag_name))
elif self.db.field_metadata[key]['kind'] in ['user', 'search']:
c = tag_item.tag.category
self.context_menu.addAction(self.search_icon, self.context_menu.addAction(self.search_icon,
_('Search for %s')%tag_name, _('Search for %s')%tag_name,
partial(self.context_menu_handler, action='search', partial(self.context_menu_handler, action='search',
category=c, key=n, negate=False)) search_state=TAG_SEARCH_STATES['mark_plus'],
index=index))
self.context_menu.addAction(self.search_icon, self.context_menu.addAction(self.search_icon,
_('Search for everything but %s')%tag_name, _('Search for everything but %s')%tag_name,
partial(self.context_menu_handler, action='search', partial(self.context_menu_handler, action='search',
category=c, key=n, negate=True)) search_state=TAG_SEARCH_STATES['mark_minus'],
index=index))
self.context_menu.addSeparator() self.context_menu.addSeparator()
# Hide/Show/Restore categories # Hide/Show/Restore categories
self.context_menu.addAction(_('Hide category %s') % category, self.context_menu.addAction(_('Hide category %s') % category,
@ -299,11 +303,11 @@ class TagsView(QTreeView): # {{{
self.context_menu.addAction(self.search_icon, self.context_menu.addAction(self.search_icon,
_('Search for books in category %s')%category, _('Search for books in category %s')%category,
partial(self.context_menu_handler, action='search_category', partial(self.context_menu_handler, action='search_category',
category=key, negate=False)) key=key, search_state='true'))
self.context_menu.addAction(self.search_icon, self.context_menu.addAction(self.search_icon,
_('Search for books not in category %s')%category, _('Search for books not in category %s')%category,
partial(self.context_menu_handler, action='search_category', partial(self.context_menu_handler, action='search_category',
category=key, negate=True)) key=key, search_state='false'))
# Offer specific editors for tags/series/publishers/saved searches # Offer specific editors for tags/series/publishers/saved searches
self.context_menu.addSeparator() self.context_menu.addSeparator()
if key in ['tags', 'publisher', 'series'] or \ if key in ['tags', 'publisher', 'series'] or \
@ -528,9 +532,15 @@ class TagTreeItem(object): # {{{
return QVariant(self.tooltip) return QVariant(self.tooltip)
return NONE return NONE
def toggle(self): def toggle(self, set_to=None):
'''
set_to: None => advance the state, otherwise a value from TAG_SEARCH_STATES
'''
if self.type == self.TAG: if self.type == self.TAG:
self.tag.state = (self.tag.state + 1)%3 if set_to is None:
self.tag.state = (self.tag.state + 1)%3
else:
self.tag.state = set_to
def child_tags(self): def child_tags(self):
res = [] res = []
@ -576,10 +586,7 @@ class TagsModel(QAbstractItemModel): # {{{
for i, r in enumerate(self.row_map): for i, r in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories: if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue continue
if self.db.field_metadata[r]['kind'] != 'user': tt = _(u'The lookup/search name is "{0}"').format(r)
tt = _('The lookup/search name is "{0}"').format(r)
else:
tt = ''
TagTreeItem(parent=self.root_item, TagTreeItem(parent=self.root_item,
data=self.categories[i], data=self.categories[i],
category_icon=self.category_icon_map[r], category_icon=self.category_icon_map[r],
@ -1017,11 +1024,15 @@ class TagsModel(QAbstractItemModel): # {{{
def clear_state(self): def clear_state(self):
self.reset_all_states() self.reset_all_states()
def toggle(self, index, exclusive): def toggle(self, index, exclusive, set_to=None):
'''
exclusive: clear all states before applying this one
set_to: None => advance the state, otherwise a value from TAG_SEARCH_STATES
'''
if not index.isValid(): return False if not index.isValid(): return False
item = index.internalPointer() item = index.internalPointer()
if item.type == TagTreeItem.TAG: if item.type == TagTreeItem.TAG:
item.toggle() item.toggle(set_to=set_to)
if exclusive: if exclusive:
self.reset_all_states(except_=item.tag) self.reset_all_states(except_=item.tag)
self.dataChanged.emit(index, index) self.dataChanged.emit(index, index)
@ -1043,8 +1054,9 @@ class TagsModel(QAbstractItemModel): # {{{
category_item = self.root_item.children[row_index] category_item = self.root_item.children[row_index]
for tag_item in category_item.child_tags(): for tag_item in category_item.child_tags():
tag = tag_item.tag tag = tag_item.tag
if tag.state > 0: if tag.state != TAG_SEARCH_STATES['clear']:
prefix = ' not ' if tag.state == 2 else '' prefix = ' not ' if tag.state == TAG_SEARCH_STATES['mark_minus'] \
else ''
category = key if key != 'news' else 'tag' category = key if key != 'news' else 'tag'
if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
ans.append('%s%s:%s'%(prefix, category, len(tag.name))) ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
@ -1187,9 +1199,14 @@ class TagBrowserMixin(object): # {{{
self.do_user_categories_edit()) self.do_user_categories_edit())
def do_user_categories_edit(self, on_category=None): def do_user_categories_edit(self, on_category=None):
d = TagCategories(self, self.library_view.model().db, on_category) db = self.library_view.model().db
d.exec_() d = TagCategories(self, db, on_category)
if d.result() == d.Accepted: if d.exec_() == d.Accepted:
db.prefs.set('user_categories', d.categories)
db.field_metadata.remove_user_categories()
for k in d.categories:
db.field_metadata.add_user_category('@' + k, k)
db.data.sqp_change_locations(db.field_metadata.get_search_terms())
self.tags_view.set_new_model() self.tags_view.set_new_model()
self.tags_view.recount() self.tags_view.recount()

View File

@ -33,10 +33,10 @@ from calibre.gui2.dialogs.progress import ProgressDialog
class Device(object): class Device(object):
output_profile = 'default' output_profile = 'generic_eink'
output_format = 'EPUB' output_format = 'EPUB'
name = 'Default' name = 'Generic e-ink device'
manufacturer = 'Default' manufacturer = 'Generic'
id = 'default' id = 'default'
supports_color = False supports_color = False
@ -63,6 +63,18 @@ class Device(object):
recs['dont_grayscale'] = True recs['dont_grayscale'] = True
save_defaults('comic_input', recs) save_defaults('comic_input', recs)
class Smartphone(Device):
id = 'smartphone'
name = 'Smartphone'
supports_color = True
class Tablet(Device):
id = 'tablet'
name = 'iPad like tablet'
output_profile = 'tablet'
supports_color = True
class Kindle(Device): class Kindle(Device):
@ -206,12 +218,21 @@ class iPhone(Device):
class Android(Device): class Android(Device):
name = 'Adroid phone + WordPlayer/Aldiko' name = 'Android phone'
output_format = 'EPUB' output_format = 'EPUB'
manufacturer = 'Android' manufacturer = 'Android'
id = 'android' id = 'android'
supports_color = True supports_color = True
class AndroidTablet(Device):
name = 'Android tablet'
output_format = 'EPUB'
manufacturer = 'Android'
id = 'android_tablet'
supports_color = True
output_profile = 'tablet'
class HanlinV3(Device): class HanlinV3(Device):
name = 'Hanlin V3' name = 'Hanlin V3'
@ -268,9 +289,9 @@ def get_manufacturers():
mans = set([]) mans = set([])
for x in get_devices(): for x in get_devices():
mans.add(x.manufacturer) mans.add(x.manufacturer)
if 'Default' in mans: if Device.manufacturer in mans:
mans.remove('Default') mans.remove(Device.manufacturer)
return ['Default'] + sorted(mans) return [Device.manufacturer] + sorted(mans)
def get_devices_of(manufacturer): def get_devices_of(manufacturer):
ans = [d for d in get_devices() if d.manufacturer == manufacturer] ans = [d for d in get_devices() if d.manufacturer == manufacturer]
@ -402,22 +423,6 @@ class StanzaPage(QWizardPage, StanzaUI):
except: except:
continue continue
class WordPlayerPage(StanzaPage):
ID = 6
def __init__(self):
StanzaPage.__init__(self)
self.label.setText('<p>'+_('If you use the WordPlayer e-book app on '
'your Android phone, you can access your calibre book collection '
'directly on the device. To do this you have to turn on the '
'content server.'))
self.instructions.setText('<p>'+_('Remember to leave calibre running '
'as the server only runs as long as calibre is running.')+'<br><br>'
+ _('You have to add the URL http://myhostname:8080 as your '
'calibre library in WordPlayer. Here myhostname should be the fully '
'qualified hostname or the IP address of the computer calibre is running on.'))
class DevicePage(QWizardPage, DeviceUI): class DevicePage(QWizardPage, DeviceUI):
@ -430,6 +435,8 @@ class DevicePage(QWizardPage, DeviceUI):
self.registerField("device", self.device_view) self.registerField("device", self.device_view)
def initializePage(self): def initializePage(self):
self.label.setText(_('Choose you e-book device. If your device is'
' not in the list, choose a "%s" device.')%Device.manufacturer)
self.man_model = ManufacturerModel() self.man_model = ManufacturerModel()
self.manufacturer_view.setModel(self.man_model) self.manufacturer_view.setModel(self.man_model)
previous = dynamic.get('welcome_wizard_device', False) previous = dynamic.get('welcome_wizard_device', False)
@ -477,8 +484,6 @@ class DevicePage(QWizardPage, DeviceUI):
return KindlePage.ID return KindlePage.ID
if dev is iPhone: if dev is iPhone:
return StanzaPage.ID return StanzaPage.ID
if dev is Android:
return WordPlayerPage.ID
return FinishPage.ID return FinishPage.ID
class MoveMonitor(QObject): class MoveMonitor(QObject):
@ -753,13 +758,11 @@ class Wizard(QWizard):
self.set_finish_text() self.set_finish_text()
self.kindle_page = KindlePage() self.kindle_page = KindlePage()
self.stanza_page = StanzaPage() self.stanza_page = StanzaPage()
self.word_player_page = WordPlayerPage()
self.setPage(self.library_page.ID, self.library_page) self.setPage(self.library_page.ID, self.library_page)
self.setPage(self.device_page.ID, self.device_page) self.setPage(self.device_page.ID, self.device_page)
self.setPage(self.finish_page.ID, self.finish_page) self.setPage(self.finish_page.ID, self.finish_page)
self.setPage(self.kindle_page.ID, self.kindle_page) self.setPage(self.kindle_page.ID, self.kindle_page)
self.setPage(self.stanza_page.ID, self.stanza_page) self.setPage(self.stanza_page.ID, self.stanza_page)
self.setPage(self.word_player_page.ID, self.word_player_page)
self.device_extra_page = None self.device_extra_page = None
nh, nw = min_available_height()-75, available_width()-30 nh, nw = min_available_height()-75, available_width()-30

View File

@ -27,7 +27,7 @@
<item row="0" column="0" colspan="2"> <item row="0" column="0" colspan="2">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string>Choose your book reader. This will set the conversion options to produce books optimized for your device.</string> <string/>
</property> </property>
<property name="wordWrap"> <property name="wordWrap">
<bool>true</bool> <bool>true</bool>

View File

@ -10,7 +10,6 @@ import re, itertools, time, traceback
from itertools import repeat from itertools import repeat
from datetime import timedelta from datetime import timedelta
from threading import Thread from threading import Thread
from Queue import Empty
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
from calibre.utils.date import parse_date, now, UNDEFINED_DATE from calibre.utils.date import parse_date, now, UNDEFINED_DATE
@ -38,7 +37,6 @@ class MetadataBackup(Thread): # {{{
self.get_metadata_for_dump = FunctionDispatcher(db.get_metadata_for_dump) self.get_metadata_for_dump = FunctionDispatcher(db.get_metadata_for_dump)
self.clear_dirtied = FunctionDispatcher(db.clear_dirtied) self.clear_dirtied = FunctionDispatcher(db.clear_dirtied)
self.set_dirtied = FunctionDispatcher(db.dirtied) self.set_dirtied = FunctionDispatcher(db.dirtied)
self.in_limbo = None
def stop(self): def stop(self):
self.keep_running = False self.keep_running = False
@ -50,34 +48,33 @@ class MetadataBackup(Thread): # {{{
def run(self): def run(self):
while self.keep_running: while self.keep_running:
self.in_limbo = None
try: try:
time.sleep(0.5) # Limit to two per second time.sleep(2) # Limit to one book per two seconds
id_ = self.db.dirtied_queue.get(True, 1.45) (id_, sequence) = self.db.get_a_dirtied_book()
except Empty: if id_ is None:
continue continue
# print 'writer thread', id_, sequence
except: except:
# Happens during interpreter shutdown # Happens during interpreter shutdown
break break
if not self.keep_running: if not self.keep_running:
break break
self.in_limbo = id_
try: try:
path, mi = self.get_metadata_for_dump(id_) path, mi, sequence = self.get_metadata_for_dump(id_)
except: except:
prints('Failed to get backup metadata for id:', id_, 'once') prints('Failed to get backup metadata for id:', id_, 'once')
traceback.print_exc() traceback.print_exc()
time.sleep(2) time.sleep(2)
try: try:
path, mi = self.get_metadata_for_dump(id_) path, mi, sequence = self.get_metadata_for_dump(id_)
except: except:
prints('Failed to get backup metadata for id:', id_, 'again, giving up') prints('Failed to get backup metadata for id:', id_, 'again, giving up')
traceback.print_exc() traceback.print_exc()
continue continue
# at this point the dirty indication is off
if mi is None: if mi is None:
self.clear_dirtied(id_, sequence)
continue continue
if not self.keep_running: if not self.keep_running:
break break
@ -89,7 +86,6 @@ class MetadataBackup(Thread): # {{{
try: try:
raw = metadata_to_opf(mi) raw = metadata_to_opf(mi)
except: except:
self.set_dirtied([id_])
prints('Failed to convert to opf for id:', id_) prints('Failed to convert to opf for id:', id_)
traceback.print_exc() traceback.print_exc()
continue continue
@ -106,24 +102,13 @@ class MetadataBackup(Thread): # {{{
try: try:
self.do_write(path, raw) self.do_write(path, raw)
except: except:
self.set_dirtied([id_])
prints('Failed to write backup metadata for id:', id_, prints('Failed to write backup metadata for id:', id_,
'again, giving up') 'again, giving up')
continue continue
self.in_limbo = None self.clear_dirtied(id_, sequence)
self.flush()
self.break_cycles() self.break_cycles()
def flush(self):
'Used during shutdown to ensure that a dirtied book is not missed'
if self.in_limbo is not None:
try:
self.db.dirtied([self.in_limbo])
except:
traceback.print_exc()
self.in_limbo = None
def write(self, path, raw): def write(self, path, raw):
with lopen(path, 'wb') as f: with lopen(path, 'wb') as f:
f.write(raw) f.write(raw)
@ -197,15 +182,15 @@ class ResultCache(SearchQueryParser): # {{{
self.first_sort = True self.first_sort = True
self.search_restriction = '' self.search_restriction = ''
self.field_metadata = field_metadata self.field_metadata = field_metadata
self.all_search_locations = field_metadata.get_search_terms() all_search_locations = field_metadata.get_search_terms()
SearchQueryParser.__init__(self, self.all_search_locations, optimize=True) SearchQueryParser.__init__(self, all_search_locations, optimize=True)
self.build_date_relop_dict() self.build_date_relop_dict()
self.build_numeric_relop_dict() self.build_numeric_relop_dict()
def break_cycles(self): def break_cycles(self):
self._data = self.field_metadata = self.FIELD_MAP = \ self._data = self.field_metadata = self.FIELD_MAP = \
self.numeric_search_relops = self.date_search_relops = \ self.numeric_search_relops = self.date_search_relops = \
self.all_search_locations = self.db_prefs = None self.db_prefs = None
def __getitem__(self, row): def __getitem__(self, row):
@ -424,11 +409,6 @@ class ResultCache(SearchQueryParser): # {{{
if self.db_prefs is None: if self.db_prefs is None:
return res return res
user_cats = self.db_prefs.get('user_categories', []) user_cats = self.db_prefs.get('user_categories', [])
# translate the case of the location
for loc in user_cats:
if location == icu_lower(loc):
location = loc
break
if location not in user_cats: if location not in user_cats:
return res return res
c = set(candidates) c = set(candidates)

View File

@ -7,9 +7,9 @@ __docformat__ = 'restructuredtext en'
The database used to store ebook metadata The database used to store ebook metadata
''' '''
import os, sys, shutil, cStringIO, glob, time, functools, traceback, re, json import os, sys, shutil, cStringIO, glob, time, functools, traceback, re, json
import threading, random
from itertools import repeat from itertools import repeat
from math import ceil from math import ceil
from Queue import Queue
from PyQt4.QtGui import QImage from PyQt4.QtGui import QImage
@ -117,7 +117,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def __init__(self, library_path, row_factory=False, default_prefs=None, def __init__(self, library_path, row_factory=False, default_prefs=None,
read_only=False): read_only=False):
self.field_metadata = FieldMetadata() self.field_metadata = FieldMetadata()
self.dirtied_queue = Queue() # Create the lock to be used to guard access to the metadata writer
# queues. This must be an RLock, not a Lock
self.dirtied_lock = threading.RLock()
if not os.path.exists(library_path): if not os.path.exists(library_path):
os.makedirs(library_path) os.makedirs(library_path)
self.listeners = set([]) self.listeners = set([])
@ -186,6 +188,29 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
migrate_preference('saved_searches', {}) migrate_preference('saved_searches', {})
set_saved_searches(self, 'saved_searches') set_saved_searches(self, 'saved_searches')
# Rename any user categories with names that differ only in case
user_cats = self.prefs.get('user_categories', [])
catmap = {}
for uc in user_cats:
ucl = icu_lower(uc)
if ucl not in catmap:
catmap[ucl] = []
catmap[ucl].append(uc)
cats_changed = False
for uc in catmap:
if len(catmap[uc]) > 1:
prints('found user category case overlap', catmap[uc])
cat = catmap[uc][0]
suffix = 1
while icu_lower((cat + unicode(suffix))) in catmap:
suffix += 1
prints('Renaming user category %s to %s'%(cat, cat+unicode(suffix)))
user_cats[cat + unicode(suffix)] = user_cats[cat]
del user_cats[cat]
cats_changed = True
if cats_changed:
self.prefs.set('user_categories', user_cats)
load_user_template_functions(self.prefs.get('user_template_functions', [])) load_user_template_functions(self.prefs.get('user_template_functions', []))
self.conn.executescript(''' self.conn.executescript('''
@ -353,9 +378,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
loc=self.FIELD_MAP['sort'])) loc=self.FIELD_MAP['sort']))
d = self.conn.get('SELECT book FROM metadata_dirtied', all=True) d = self.conn.get('SELECT book FROM metadata_dirtied', all=True)
for x in d: with self.dirtied_lock:
self.dirtied_queue.put(x[0]) self.dirtied_sequence = 0
self.dirtied_cache = set([x[0] for x in d]) self.dirtied_cache = {}
for x in d:
self.dirtied_cache[x[0]] = self.dirtied_sequence
self.dirtied_sequence += 1
self.refresh_ondevice = functools.partial(self.data.refresh_ondevice, self) self.refresh_ondevice = functools.partial(self.data.refresh_ondevice, self)
self.refresh() self.refresh()
@ -582,21 +610,27 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def metadata_for_field(self, key): def metadata_for_field(self, key):
return self.field_metadata[key] return self.field_metadata[key]
def clear_dirtied(self, book_ids): def clear_dirtied(self, book_id, sequence):
''' '''
Clear the dirtied indicator for the books. This is used when fetching Clear the dirtied indicator for the books. This is used when fetching
metadata, creating an OPF, and writing a file are separated into steps. metadata, creating an OPF, and writing a file are separated into steps.
The last step is clearing the indicator The last step is clearing the indicator
''' '''
for book_id in book_ids: with self.dirtied_lock:
self.conn.execute('DELETE FROM metadata_dirtied WHERE book=?', dc_sequence = self.dirtied_cache.get(book_id, None)
(book_id,)) # print 'clear_dirty: check book', book_id, dc_sequence
# if a later exception prevents the commit, then the dirtied if dc_sequence is None or sequence is None or dc_sequence == sequence:
# table will still have the book. No big deal, because the OPF # print 'needs to be cleaned'
# is there and correct. We will simply do it again on next self.conn.execute('DELETE FROM metadata_dirtied WHERE book=?',
# start (book_id,))
self.dirtied_cache.discard(book_id) self.conn.commit()
self.conn.commit() try:
del self.dirtied_cache[book_id]
except:
pass
elif dc_sequence is not None:
# print 'book needs to be done again'
pass
def dump_metadata(self, book_ids=None, remove_from_dirtied=True, def dump_metadata(self, book_ids=None, remove_from_dirtied=True,
commit=True): commit=True):
@ -609,38 +643,59 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
for book_id in book_ids: for book_id in book_ids:
if not self.data.has_id(book_id): if not self.data.has_id(book_id):
continue continue
path, mi = self.get_metadata_for_dump(book_id, path, mi, sequence = self.get_metadata_for_dump(book_id)
remove_from_dirtied=remove_from_dirtied)
if path is None: if path is None:
continue continue
try: try:
raw = metadata_to_opf(mi) raw = metadata_to_opf(mi)
with lopen(path, 'wb') as f: with lopen(path, 'wb') as f:
f.write(raw) f.write(raw)
if remove_from_dirtied:
self.clear_dirtied(book_id, sequence)
except: except:
# Something went wrong. Put the book back on the dirty list pass
self.dirtied([book_id])
if commit: if commit:
self.conn.commit() self.conn.commit()
def dirtied(self, book_ids, commit=True): def dirtied(self, book_ids, commit=True):
for book in frozenset(book_ids) - self.dirtied_cache: changed = False
try: for book in book_ids:
self.conn.execute( with self.dirtied_lock:
'INSERT INTO metadata_dirtied (book) VALUES (?)', # print 'dirtied: check id', book
(book,)) if book in self.dirtied_cache:
self.dirtied_queue.put(book) self.dirtied_cache[book] = self.dirtied_sequence
except IntegrityError: self.dirtied_sequence += 1
# Already in table continue
pass # print 'book not already dirty'
# If the commit doesn't happen, then our cache will be wrong. This try:
# could lead to a problem because we won't put the book back into self.conn.execute(
# the dirtied table. We deal with this by writing the dirty cache 'INSERT INTO metadata_dirtied (book) VALUES (?)',
# back to the table on GUI exit. Not perfect, but probably OK (book,))
self.dirtied_cache.add(book) changed = True
if commit: except IntegrityError:
# Already in table
pass
self.dirtied_cache[book] = self.dirtied_sequence
self.dirtied_sequence += 1
# If the commit doesn't happen, then the DB table will be wrong. This
# could lead to a problem because on restart, we won't put the book back
# into the dirtied_cache. We deal with this by writing the dirtied_cache
# back to the table on GUI exit. Not perfect, but probably OK
if commit and changed:
self.conn.commit() self.conn.commit()
def get_a_dirtied_book(self):
with self.dirtied_lock:
l = len(self.dirtied_cache)
if l > 0:
# The random stuff is here to prevent a single book from
# blocking progress if its metadata cannot be written for some
# reason.
id_ = self.dirtied_cache.keys()[random.randint(0, l-1)]
sequence = self.dirtied_cache[id_]
return (id_, sequence)
return (None, None)
def dirty_queue_length(self): def dirty_queue_length(self):
return len(self.dirtied_cache) return len(self.dirtied_cache)
@ -653,12 +708,19 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
is no problem with setting a dirty indication for a book that isn't in is no problem with setting a dirty indication for a book that isn't in
fact dirty. Just wastes a few cycles. fact dirty. Just wastes a few cycles.
''' '''
book_ids = list(self.dirtied_cache) with self.dirtied_lock:
self.dirtied_cache = set() book_ids = list(self.dirtied_cache.keys())
self.dirtied(book_ids) self.dirtied_cache = {}
self.dirtied(book_ids)
def get_metadata_for_dump(self, idx, remove_from_dirtied=True): def get_metadata_for_dump(self, idx):
path, mi = (None, None) path, mi = (None, None)
# get the current sequence number for this book to pass back to the
# backup thread. This will avoid double calls in the case where the
# thread has not done the work between the put and the get_metadata
with self.dirtied_lock:
sequence = self.dirtied_cache.get(idx, None)
# print 'get_md_for_dump', idx, sequence
try: try:
# While a book is being created, the path is empty. Don't bother to # While a book is being created, the path is empty. Don't bother to
# try to write the opf, because it will go to the wrong folder. # try to write the opf, because it will go to the wrong folder.
@ -673,16 +735,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# This almost certainly means that the book has been deleted while # This almost certainly means that the book has been deleted while
# the backup operation sat in the queue. # the backup operation sat in the queue.
pass pass
return (path, mi, sequence)
try:
# clear the dirtied indicator. The user must put it back if
# something goes wrong with writing the OPF
if remove_from_dirtied:
self.clear_dirtied([idx])
except:
# No real problem. We will just do it again.
pass
return (path, mi)
def get_metadata(self, idx, index_is_id=False, get_cover=False): def get_metadata(self, idx, index_is_id=False, get_cover=False):
''' '''
@ -774,7 +827,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
try: try:
book_ids = self.data.parse(query) book_ids = self.data.parse(query)
except: except:
import traceback
traceback.print_exc() traceback.print_exc()
return identical_book_ids return identical_book_ids
for book_id in book_ids: for book_id in book_ids:

View File

@ -474,9 +474,19 @@ class FieldMetadata(dict):
for key in list(self._tb_cats.keys()): for key in list(self._tb_cats.keys()):
val = self._tb_cats[key] val = self._tb_cats[key]
if val['is_category'] and val['kind'] in ('user', 'search'): if val['is_category'] and val['kind'] in ('user', 'search'):
for k in self._tb_cats[key]['search_terms']:
if k in self._search_term_map:
del self._search_term_map[k]
del self._tb_cats[key]
def remove_user_categories(self):
for key in list(self._tb_cats.keys()):
val = self._tb_cats[key]
if val['is_category'] and val['kind'] == 'user':
for k in self._tb_cats[key]['search_terms']:
if k in self._search_term_map:
del self._search_term_map[k]
del self._tb_cats[key] del self._tb_cats[key]
if key in self._search_term_map:
del self._search_term_map[key]
def cc_series_index_column_for(self, key): def cc_series_index_column_for(self, key):
return self._tb_cats[key]['rec_index'] + 1 return self._tb_cats[key]['rec_index'] + 1
@ -484,12 +494,15 @@ class FieldMetadata(dict):
def add_user_category(self, label, name): def add_user_category(self, label, name):
if label in self._tb_cats: if label in self._tb_cats:
raise ValueError('Duplicate user field [%s]'%(label)) raise ValueError('Duplicate user field [%s]'%(label))
st = [label]
if icu_lower(label) != label:
st.append(icu_lower(label))
self._tb_cats[label] = {'table':None, 'column':None, self._tb_cats[label] = {'table':None, 'column':None,
'datatype':None, 'is_multiple':None, 'datatype':None, 'is_multiple':None,
'kind':'user', 'name':name, 'kind':'user', 'name':name,
'search_terms':[label],'is_custom':False, 'search_terms':st, 'is_custom':False,
'is_category':True} 'is_category':True}
self._add_search_terms_to_map(label, [label]) self._add_search_terms_to_map(label, st)
def add_search_category(self, label, name): def add_search_category(self, label, name):
if label in self._tb_cats: if label in self._tb_cats:

View File

@ -9,6 +9,7 @@ import json
from calibre.constants import preferred_encoding from calibre.constants import preferred_encoding
from calibre.utils.config import to_json, from_json from calibre.utils.config import to_json, from_json
from calibre import prints
class DBPrefs(dict): class DBPrefs(dict):
@ -17,7 +18,11 @@ class DBPrefs(dict):
self.db = db self.db = db
self.defaults = {} self.defaults = {}
for key, val in self.db.conn.get('SELECT key,val FROM preferences'): for key, val in self.db.conn.get('SELECT key,val FROM preferences'):
val = self.raw_to_object(val) try:
val = self.raw_to_object(val)
except:
prints('Failed to read value for:', key, 'from db')
continue
dict.__setitem__(self, key, val) dict.__setitem__(self, key, val)
def raw_to_object(self, raw): def raw_to_object(self, raw):

View File

@ -119,6 +119,12 @@ class SearchQueryParser(object):
return failed return failed
def __init__(self, locations, test=False, optimize=False): def __init__(self, locations, test=False, optimize=False):
self.sqp_initialize(locations, test=test, optimize=optimize)
def sqp_change_locations(self, locations):
self.sqp_initialize(locations, optimize=self.optimize)
def sqp_initialize(self, locations, test=False, optimize=False):
self._tests_failed = False self._tests_failed = False
self.optimize = optimize self.optimize = optimize
# Define a token # Define a token