mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 02:34:06 -04:00
KG updates
This commit is contained in:
commit
6713cbfc54
@ -2,7 +2,7 @@
|
||||
__license__ = 'GPL v3'
|
||||
__author__ = 'Luis Hernandez'
|
||||
__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
|
||||
@ -15,8 +15,8 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
||||
title = u'20 Minutos'
|
||||
publisher = u'Grupo 20 Minutos'
|
||||
|
||||
__author__ = u'Luis Hernández'
|
||||
description = u'Periódico gratuito en español'
|
||||
__author__ = 'Luis Hernández'
|
||||
description = 'Periódico gratuito en español'
|
||||
cover_url = 'http://estaticos.20minutos.es/mmedia/especiales/corporativo/css/img/logotipos_grupo20minutos.gif'
|
||||
|
||||
oldest_article = 5
|
||||
@ -30,8 +30,9 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
||||
language = 'es'
|
||||
timefmt = '[%a, %d %b, %Y]'
|
||||
|
||||
keep_only_tags = [dict(name='div', attrs={'id':['content']})
|
||||
,dict(name='div', attrs={'class':['boxed','description','lead','article-content']})
|
||||
keep_only_tags = [
|
||||
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='ul', attrs={'class':['article-author']})
|
||||
]
|
||||
@ -42,10 +43,12 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
||||
remove_tags = [
|
||||
dict(name='ol', attrs={'class':['navigation',]})
|
||||
,dict(name='span', attrs={'class':['action']})
|
||||
,dict(name='div', attrs={'class':['twitter comments-list hidden','related-news','col']})
|
||||
,dict(name='div', attrs={'id':['twitter-destacados']})
|
||||
,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','eco-tabs','inner','vineta_calendario','vinetistas clearfix','otras_vinetas estirar','MIN1','main','SUP1','INT']})
|
||||
,dict(name='ul', attrs={'class':['article-user-actions','stripped-list']})
|
||||
]
|
||||
,dict(name='ul', attrs={'id':['site-links']})
|
||||
,dict(name='li', attrs={'class':['puntuacion','enviar','compartir']})
|
||||
]
|
||||
|
||||
feeds = [
|
||||
(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'Cine' , u'http://www.20minutos.es/rss/cine/')
|
||||
,(u'Musica' , u'http://www.20minutos.es/rss/musica/')
|
||||
,(u'Vinetas' , u'http://www.20minutos.es/rss/vinetas/')
|
||||
,(u'Comunidad20' , u'http://www.20minutos.es/rss/zona20/')
|
||||
]
|
||||
|
||||
|
@ -44,6 +44,7 @@ class CanWestPaper(BasicNewsRecipe):
|
||||
|
||||
language = 'en_CA'
|
||||
__author__ = 'Nick Redding'
|
||||
encoding = 'latin1'
|
||||
no_stylesheets = True
|
||||
timefmt = ' [%b %d]'
|
||||
extra_css = '''
|
||||
@ -97,7 +98,9 @@ class CanWestPaper(BasicNewsRecipe):
|
||||
atag = h1tag.find('a',href=True)
|
||||
if not atag:
|
||||
continue
|
||||
url = self.url_prefix+'/news/todays-paper/'+atag['href']
|
||||
url = atag['href']
|
||||
if not url.startswith('http:'):
|
||||
url = self.url_prefix+'/news/todays-paper/'+atag['href']
|
||||
#self.log("Section %s" % key)
|
||||
#self.log("url %s" % url)
|
||||
title = self.tag_to_string(atag,False)
|
||||
|
11
resources/recipes/capes_n_babes.recipe
Normal file
11
resources/recipes/capes_n_babes.recipe
Normal file
@ -0,0 +1,11 @@
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class CapesnBabesRecipe(BasicNewsRecipe):
|
||||
title = u'Capes n Babes'
|
||||
language = 'en'
|
||||
description = 'The Capes n Babes comic Blog'
|
||||
__author__ = 'skyhawker'
|
||||
oldest_article = 31
|
||||
max_articles_per_feed = 100
|
||||
use_embedded_content = True
|
||||
feeds = [(u'Capes & Babes', u'feed://www.capesnbabes.com/feed/')]
|
45
resources/recipes/dbb.recipe
Normal file
45
resources/recipes/dbb.recipe
Normal 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')]
|
74
resources/recipes/la_nueva.recipe
Normal file
74
resources/recipes/la_nueva.recipe
Normal 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')
|
||||
]
|
@ -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
|
||||
|
||||
class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
||||
|
||||
title = u'La Tribuna de Talavera'
|
||||
publisher = u'Grupo PROMECAL'
|
||||
|
||||
__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'
|
||||
|
||||
oldest_article = 5
|
||||
@ -17,7 +30,8 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
||||
language = 'es'
|
||||
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='p', attrs={'id':['texto']})
|
||||
]
|
||||
@ -25,5 +39,13 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
||||
remove_tags_before = dict(name='div' , attrs={'class':['comparte']})
|
||||
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')]
|
||||
|
40
resources/recipes/leduc.recipe
Normal file
40
resources/recipes/leduc.recipe
Normal 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'),
|
||||
]
|
15
resources/recipes/spin_magazine.recipe
Normal file
15
resources/recipes/spin_magazine.recipe
Normal file
@ -0,0 +1,15 @@
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AdvancedUserRecipe1296179411(BasicNewsRecipe):
|
||||
title = u'SPIN Magzine'
|
||||
__author__ = 'Quistopher'
|
||||
language = 'en'
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 100
|
||||
|
||||
feeds = [
|
||||
(u'Daily Noise Blog | SPIN.com', u'http://www.spin.com/blog/feed'),
|
||||
(u'It Happened Last Night | SPIN.com', u'http://www.spin.com/it-happened-last-night/feed'),
|
||||
(u'Album Reviews | SPIN.com', u'http://www.spin.com/album-reviews/feed')
|
||||
|
||||
]
|
@ -495,6 +495,22 @@ class SonyReader900Output(SonyReaderOutput):
|
||||
screen_size = (600, 999)
|
||||
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):
|
||||
|
||||
name = 'JetBook 5-inch'
|
||||
@ -719,6 +735,6 @@ output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output,
|
||||
iPadOutput, KoboReaderOutput, TabletOutput, SamsungGalaxy,
|
||||
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput,
|
||||
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,
|
||||
BambookOutput, NookColorOutput]
|
||||
BambookOutput, NookColorOutput, GenericEink, GenericEinkLarge]
|
||||
|
||||
output_profiles.sort(cmp=lambda x,y:cmp(x.name.lower(), y.name.lower()))
|
||||
|
@ -36,9 +36,10 @@ def author_to_author_sort(author):
|
||||
return author
|
||||
author = _bracket_pat.sub('', author).strip()
|
||||
tokens = author.split()
|
||||
tokens = tokens[-1:] + tokens[:-1]
|
||||
if len(tokens) > 1 and method != 'nocomma':
|
||||
tokens[0] += ','
|
||||
if tokens and tokens[-1] not in ('Inc.', 'Inc'):
|
||||
tokens = tokens[-1:] + tokens[:-1]
|
||||
if len(tokens) > 1 and method != 'nocomma':
|
||||
tokens[0] += ','
|
||||
return ' '.join(tokens)
|
||||
|
||||
def authors_to_sort_string(authors):
|
||||
|
@ -143,7 +143,9 @@ class PML_HTMLizer(object):
|
||||
|
||||
def __init__(self):
|
||||
self.state = {}
|
||||
self.toc = TOC()
|
||||
# toc consists of a tuple
|
||||
# (level, (href, id, text))
|
||||
self.toc = []
|
||||
self.file_name = ''
|
||||
|
||||
def prepare_pml(self, pml):
|
||||
@ -494,7 +496,7 @@ class PML_HTMLizer(object):
|
||||
output = []
|
||||
|
||||
self.state = {}
|
||||
self.toc = TOC()
|
||||
self.toc = []
|
||||
self.file_name = file_name
|
||||
|
||||
indent_state = {'t': False, 'T': False}
|
||||
@ -542,6 +544,7 @@ class PML_HTMLizer(object):
|
||||
# inside of ="" so we don't have do special processing
|
||||
# for C.
|
||||
t = ''
|
||||
level = 0
|
||||
if c in 'XC':
|
||||
level = line.read(1)
|
||||
id = 'pml_toc-%s' % len(self.toc)
|
||||
@ -553,7 +556,7 @@ class PML_HTMLizer(object):
|
||||
if not value or value == '':
|
||||
text = t
|
||||
else:
|
||||
self.toc.add_item(os.path.basename(self.file_name), id, value)
|
||||
self.toc.append((level, (os.path.basename(self.file_name), id, value)))
|
||||
text = '%s<span id="%s"></span>' % (t, id)
|
||||
elif c == 'm':
|
||||
empty = False
|
||||
@ -624,7 +627,72 @@ class PML_HTMLizer(object):
|
||||
return output
|
||||
|
||||
def get_toc(self):
|
||||
return self.toc
|
||||
'''
|
||||
Toc can have up to 5 levels, 0 - 4 inclusive.
|
||||
|
||||
This function will add items to their appropriate
|
||||
depth in the TOC tree. If the specified depth is
|
||||
invalid (item would not have a valid parent) add
|
||||
it to the next valid level above the specified
|
||||
level.
|
||||
'''
|
||||
# Base toc object all items will be added to.
|
||||
n_toc = TOC()
|
||||
# Used to track nodes in the toc so we can add
|
||||
# sub items to the appropriate place in tree.
|
||||
t_l0 = None
|
||||
t_l1 = None
|
||||
t_l2 = None
|
||||
t_l3 = None
|
||||
|
||||
for level, (href, id, text) in self.toc:
|
||||
if level == u'0':
|
||||
t_l0 = n_toc.add_item(href, id, text)
|
||||
t_l1 = None
|
||||
t_l2 = None
|
||||
t_l3 = None
|
||||
elif level == u'1':
|
||||
if t_l0 == None:
|
||||
t_l0 = n_toc
|
||||
t_l1 = t_l0.add_item(href, id, text)
|
||||
t_l2 = None
|
||||
t_l3 = None
|
||||
elif level == u'2':
|
||||
if t_l1 == None:
|
||||
if t_l0 == None:
|
||||
t_l1 = n_toc
|
||||
else:
|
||||
t_l1 = t_l0
|
||||
t_l2 = t_l1.add_item(href, id, text)
|
||||
t_l3 = None
|
||||
elif level == u'3':
|
||||
if t_l2 == None:
|
||||
if t_l1 == None:
|
||||
if t_l0 == None:
|
||||
t_l2 = n_toc
|
||||
else:
|
||||
t_l2 = t_l0
|
||||
else:
|
||||
t_l2 = t_l1
|
||||
t_l3 = t_l2.add_item(href, id, text)
|
||||
# Level 4.
|
||||
# Anything above 4 is invalid but we will count
|
||||
# it as level 4.
|
||||
else:
|
||||
if t_l3 == None:
|
||||
if t_l2 == None:
|
||||
if t_l1 == None:
|
||||
if t_l0 == None:
|
||||
t_l3 = n_toc
|
||||
else:
|
||||
t_l3 = t_l0
|
||||
else:
|
||||
t_l3 = t_l1
|
||||
else:
|
||||
t_l3 = t_l2
|
||||
t_l3.add_item(href, id, text)
|
||||
|
||||
return n_toc
|
||||
|
||||
|
||||
def pml_to_html(pml):
|
||||
|
@ -83,7 +83,6 @@ class TXTInput(InputFormatPlugin):
|
||||
setattr(options, 'markup_chapter_headings', True)
|
||||
setattr(options, 'italicize_common_cases', True)
|
||||
setattr(options, 'fix_indents', True)
|
||||
setattr(options, 'preserve_spaces', True)
|
||||
setattr(options, 'delete_blank_paragraphs', True)
|
||||
setattr(options, 'format_scene_breaks', True)
|
||||
setattr(options, 'dehyphenate', True)
|
||||
|
@ -20,9 +20,13 @@ HTML_TEMPLATE = u'<html><head><meta http-equiv="Content-Type" content="text/html
|
||||
def clean_txt(txt):
|
||||
if isbytestring(txt):
|
||||
txt = txt.decode('utf-8', 'replace')
|
||||
# Strip whitespace from the beginning and end of the line. Also replace
|
||||
# Strip whitespace from the end of the line. Also replace
|
||||
# all line breaks with \n.
|
||||
txt = '\n'.join([line.strip() for line in txt.splitlines()])
|
||||
txt = '\n'.join([line.rstrip() for line in txt.splitlines()])
|
||||
|
||||
# Replace whitespace at the beginning of the list with
|
||||
txt = re.sub('(?m)(?P<space>[ ]+)', lambda mo: ' ' * mo.groups('space').count(' '), txt)
|
||||
txt = re.sub('(?m)(?P<space>[\t]+)', lambda mo: ' ' * 4 * mo.groups('space').count('\t'), txt)
|
||||
|
||||
# Condense redundant spaces
|
||||
txt = re.sub('[ ]{2,}', ' ', txt)
|
||||
@ -31,7 +35,7 @@ def clean_txt(txt):
|
||||
txt = re.sub('^\s+(?=.)', '', txt)
|
||||
txt = re.sub('(?<=.)\s+$', '', txt)
|
||||
# Remove excessive line breaks.
|
||||
txt = re.sub('\n{3,}', '\n\n', txt)
|
||||
txt = re.sub('\n{5,}', '\n\n\n\n', txt)
|
||||
#remove ASCII invalid chars : 0 to 8 and 11-14 to 24
|
||||
txt = clean_ascii_chars(txt)
|
||||
|
||||
@ -59,10 +63,16 @@ def convert_basic(txt, title='', epub_split_size_kb=0):
|
||||
txt = split_txt(txt, epub_split_size_kb)
|
||||
|
||||
lines = []
|
||||
blank_count = 0
|
||||
# Split into paragraphs based on having a blank line between text.
|
||||
for line in txt.split('\n\n'):
|
||||
for line in txt.split('\n'):
|
||||
if line.strip():
|
||||
blank_count = 0
|
||||
lines.append(u'<p>%s</p>' % prepare_string_for_xml(line.replace('\n', ' ')))
|
||||
else:
|
||||
blank_count += 1
|
||||
if blank_count == 2:
|
||||
lines.append(u'<p> </p>')
|
||||
|
||||
return HTML_TEMPLATE % (title, u'\n'.join(lines))
|
||||
|
||||
@ -85,7 +95,7 @@ def normalize_line_endings(txt):
|
||||
return txt
|
||||
|
||||
def separate_paragraphs_single_line(txt):
|
||||
txt = re.sub(u'(?<=.)\n(?=.)', '\n\n', txt)
|
||||
txt = txt.replace('\n', '\n\n')
|
||||
return txt
|
||||
|
||||
def separate_paragraphs_print_formatted(txt):
|
||||
@ -93,7 +103,7 @@ def separate_paragraphs_print_formatted(txt):
|
||||
return txt
|
||||
|
||||
def preserve_spaces(txt):
|
||||
txt = txt.replace(' ', ' ')
|
||||
txt = re.sub('(?P<space>[ ]{2,})', lambda mo: ' ' + (' ' * (len(mo.group('space')) - 1)), txt)
|
||||
txt = txt.replace('\t', ' ')
|
||||
return txt
|
||||
|
||||
|
@ -100,6 +100,9 @@ class AddAction(InterfaceAction):
|
||||
mi = MetaInformation(_('Unknown'), dlg.selected_authors)
|
||||
self.gui.library_view.model().db.import_book(mi, [])
|
||||
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=[]):
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
|
@ -17,7 +17,7 @@ from calibre.gui2.actions import InterfaceAction
|
||||
class GenerateCatalogAction(InterfaceAction):
|
||||
|
||||
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'])
|
||||
|
||||
def generate_catalog(self):
|
||||
|
@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
|
||||
import os, shutil
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import QMenu, Qt, QInputDialog, QThread, pyqtSignal, QProgressDialog
|
||||
from PyQt4.Qt import QMenu, Qt, QInputDialog
|
||||
|
||||
from calibre import isbytestring
|
||||
from calibre.constants import filesystem_encoding
|
||||
@ -16,7 +16,7 @@ from calibre.utils.config import prefs
|
||||
from calibre.gui2 import gprefs, warning_dialog, Dispatcher, error_dialog, \
|
||||
question_dialog, info_dialog
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
from calibre.gui2.dialogs.check_library import CheckLibraryDialog
|
||||
from calibre.gui2.dialogs.check_library import CheckLibraryDialog, DBCheck
|
||||
|
||||
class LibraryUsageStats(object): # {{{
|
||||
|
||||
@ -76,76 +76,6 @@ class LibraryUsageStats(object): # {{{
|
||||
self.write_stats()
|
||||
# }}}
|
||||
|
||||
# Check Integrity {{{
|
||||
|
||||
class VacThread(QThread):
|
||||
|
||||
check_done = pyqtSignal(object, object)
|
||||
callback = pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, parent, db):
|
||||
QThread.__init__(self, parent)
|
||||
self.db = db
|
||||
self._parent = parent
|
||||
|
||||
def run(self):
|
||||
err = bad = None
|
||||
try:
|
||||
bad = self.db.check_integrity(self.callbackf)
|
||||
except:
|
||||
import traceback
|
||||
err = traceback.format_exc()
|
||||
self.check_done.emit(bad, err)
|
||||
|
||||
def callbackf(self, progress, msg):
|
||||
self.callback.emit(progress, msg)
|
||||
|
||||
|
||||
class CheckIntegrity(QProgressDialog):
|
||||
|
||||
def __init__(self, db, parent=None):
|
||||
QProgressDialog.__init__(self, parent)
|
||||
self.db = db
|
||||
self.setCancelButton(None)
|
||||
self.setMinimum(0)
|
||||
self.setMaximum(100)
|
||||
self.setWindowTitle(_('Checking database integrity'))
|
||||
self.setAutoReset(False)
|
||||
self.setValue(0)
|
||||
|
||||
self.vthread = VacThread(self, db)
|
||||
self.vthread.check_done.connect(self.check_done,
|
||||
type=Qt.QueuedConnection)
|
||||
self.vthread.callback.connect(self.callback, type=Qt.QueuedConnection)
|
||||
self.vthread.start()
|
||||
|
||||
def callback(self, progress, msg):
|
||||
self.setLabelText(msg)
|
||||
self.setValue(int(100*progress))
|
||||
|
||||
def check_done(self, bad, err):
|
||||
if err:
|
||||
error_dialog(self, _('Error'),
|
||||
_('Failed to check database integrity'),
|
||||
det_msg=err, show=True)
|
||||
elif bad:
|
||||
titles = [self.db.title(x, index_is_id=True) for x in bad]
|
||||
det_msg = '\n'.join(titles)
|
||||
warning_dialog(self, _('Some inconsistencies found'),
|
||||
_('The following books had formats or covers listed in the '
|
||||
'database that are not actually available. '
|
||||
'The entries for the formats/covers have been removed. '
|
||||
'You should check them manually. This can '
|
||||
'happen if you manipulate the files in the '
|
||||
'library folder directly.'), det_msg=det_msg, show=True)
|
||||
else:
|
||||
info_dialog(self, _('No errors found'),
|
||||
_('The integrity check completed with no uncorrectable errors found.'),
|
||||
show=True)
|
||||
self.reset()
|
||||
|
||||
# }}}
|
||||
|
||||
class ChooseLibraryAction(InterfaceAction):
|
||||
|
||||
name = 'Choose Library'
|
||||
@ -209,14 +139,6 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
None, None), attr='action_check_library')
|
||||
ac.triggered.connect(self.check_library, type=Qt.QueuedConnection)
|
||||
self.maintenance_menu.addAction(ac)
|
||||
ac = self.create_action(spec=(_('Check database integrity'), 'lt.png',
|
||||
None, None), attr='action_check_database')
|
||||
ac.triggered.connect(self.check_database, type=Qt.QueuedConnection)
|
||||
self.maintenance_menu.addAction(ac)
|
||||
ac = self.create_action(spec=(_('Recover database'), 'lt.png',
|
||||
None, None), attr='action_restore_database')
|
||||
ac.triggered.connect(self.restore_database, type=Qt.QueuedConnection)
|
||||
self.maintenance_menu.addAction(ac)
|
||||
self.choose_menu.addMenu(self.maintenance_menu)
|
||||
|
||||
def pick_random(self, *args):
|
||||
@ -346,28 +268,35 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
'rate of approximately 1 book every three seconds.'), show=True)
|
||||
|
||||
def check_library(self):
|
||||
db = self.gui.library_view.model().db
|
||||
d = CheckLibraryDialog(self.gui.parent(), db)
|
||||
d.exec_()
|
||||
|
||||
def check_database(self, *args):
|
||||
self.gui.library_view.save_state()
|
||||
m = self.gui.library_view.model()
|
||||
m.stop_metadata_backup()
|
||||
try:
|
||||
d = CheckIntegrity(m.db, self.gui)
|
||||
d.exec_()
|
||||
finally:
|
||||
m.start_metadata_backup()
|
||||
db = m.db
|
||||
db.prefs.disable_setting = True
|
||||
|
||||
def restore_database(self):
|
||||
info_dialog(self.gui, _('Recover database'), '<p>'+
|
||||
_(
|
||||
'This command rebuilds your calibre database from the information '
|
||||
'stored by calibre in the OPF files.<p>'
|
||||
'This function is not currently available in the GUI. You can '
|
||||
'recover your database using the \'calibredb restore_database\' '
|
||||
'command line function.'
|
||||
), show=True)
|
||||
d = DBCheck(self.gui, db)
|
||||
d.start()
|
||||
try:
|
||||
d.conn.close()
|
||||
except:
|
||||
pass
|
||||
d.break_cycles()
|
||||
self.gui.library_moved(db.library_path, call_close=not
|
||||
d.closed_orig_conn)
|
||||
if d.rejected:
|
||||
return
|
||||
if d.error is None:
|
||||
if not question_dialog(self.gui, _('Success'),
|
||||
_('Found no errors in your calibre library database.'
|
||||
' Do you want calibre to check if the files in your '
|
||||
' library match the information in the database?')):
|
||||
return
|
||||
else:
|
||||
return error_dialog(self.gui, _('Failed'),
|
||||
_('Database integrity check failed, click Show details'
|
||||
' for details.'), show=True, det_msg=d.error[1])
|
||||
d = CheckLibraryDialog(self.gui, m.db)
|
||||
d.exec_()
|
||||
|
||||
def switch_requested(self, location):
|
||||
if not self.change_library_allowed():
|
||||
|
@ -31,7 +31,7 @@ class ConvertAction(InterfaceAction):
|
||||
partial(self.convert_ebook, False, bulk=True))
|
||||
cm.addSeparator()
|
||||
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)
|
||||
self.qaction.setMenu(cm)
|
||||
self.qaction.triggered.connect(self.convert_ebook)
|
||||
|
@ -19,7 +19,9 @@ class PluginWidget(QWidget, Ui_Form):
|
||||
('bib_entry', 0), #mixed
|
||||
('bibfile_enc', 0), #utf-8
|
||||
('bibfile_enctag', 0), #strict
|
||||
('impcit', True) ]
|
||||
('impcit', True),
|
||||
('addfiles', False),
|
||||
]
|
||||
|
||||
sync_enabled = False
|
||||
formats = set(['bib'])
|
||||
@ -49,7 +51,7 @@ class PluginWidget(QWidget, Ui_Form):
|
||||
opt_value = gprefs.get(self.name + '_' + opt[0], opt[1])
|
||||
if opt[0] in ['bibfile_enc', 'bibfile_enctag', 'bib_entry']:
|
||||
getattr(self, opt[0]).setCurrentIndex(opt_value)
|
||||
elif opt[0] == 'impcit' :
|
||||
elif opt[0] in ['impcit', 'addfiles'] :
|
||||
getattr(self, opt[0]).setChecked(opt_value)
|
||||
else:
|
||||
getattr(self, opt[0]).setText(opt_value)
|
||||
@ -76,7 +78,7 @@ class PluginWidget(QWidget, Ui_Form):
|
||||
for opt in self.OPTION_FIELDS:
|
||||
if opt[0] in ['bibfile_enc', 'bibfile_enctag', 'bib_entry']:
|
||||
opt_value = getattr(self,opt[0]).currentIndex()
|
||||
elif opt[0] == 'impcit' :
|
||||
elif opt[0] in ['impcit', 'addfiles'] :
|
||||
opt_value = getattr(self, opt[0]).isChecked()
|
||||
else :
|
||||
opt_value = unicode(getattr(self, opt[0]).text())
|
||||
|
@ -47,7 +47,7 @@
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1" rowspan="12">
|
||||
<item row="1" column="1" rowspan="11">
|
||||
<widget class="QListWidget" name="db_fields">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
@ -141,6 +141,13 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<widget class="QCheckBox" name="addfiles">
|
||||
<property name="text">
|
||||
<string>Add files path with formats?</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Expression to form the BibTeX citation tag:</string>
|
||||
|
@ -151,12 +151,27 @@ class DateEdit(QDateEdit):
|
||||
def set_to_today(self):
|
||||
self.setDate(now())
|
||||
|
||||
def set_to_clear(self):
|
||||
self.setDate(UNDEFINED_QDATE)
|
||||
|
||||
class DateTime(Base):
|
||||
|
||||
def setup_ui(self, parent):
|
||||
cm = self.col_metadata
|
||||
self.widgets = [QLabel('&'+cm['name']+':', parent), DateEdit(parent),
|
||||
QLabel(''), QPushButton(_('Set \'%s\' to today')%cm['name'], parent)]
|
||||
self.widgets = [QLabel('&'+cm['name']+':', parent), DateEdit(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]
|
||||
format = cm['display'].get('date_format','')
|
||||
if not format:
|
||||
@ -165,7 +180,8 @@ class DateTime(Base):
|
||||
w.setCalendarPopup(True)
|
||||
w.setMinimumDate(UNDEFINED_QDATE)
|
||||
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):
|
||||
if val is None:
|
||||
@ -470,11 +486,48 @@ class BulkBase(Base):
|
||||
self.setter(val)
|
||||
|
||||
def commit(self, book_ids, notify=False):
|
||||
if not self.a_c_checkbox.isChecked():
|
||||
return
|
||||
val = self.gui_val
|
||||
val = self.normalize_ui_val(val)
|
||||
if val != self.initial_val:
|
||||
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):
|
||||
|
||||
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:
|
||||
val = False
|
||||
if value is not None and value != val:
|
||||
return 'nochange'
|
||||
return None
|
||||
value = val
|
||||
return value
|
||||
|
||||
def setup_ui(self, parent):
|
||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
||||
QComboBox(parent)]
|
||||
w = self.widgets[1]
|
||||
items = [_('Yes'), _('No'), _('Undefined'), _('Do not change')]
|
||||
icons = [I('ok.png'), I('list_remove.png'), I('blank.png'), I('blank.png')]
|
||||
self.make_widgets(parent, QComboBox)
|
||||
items = [_('Yes'), _('No'), _('Undefined')]
|
||||
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
|
||||
self.main_widget.blockSignals(True)
|
||||
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):
|
||||
val = self.widgets[1].currentIndex()
|
||||
return {3: 'nochange', 2: None, 1: False, 0: True}[val]
|
||||
val = self.main_widget.currentIndex()
|
||||
return {2: None, 1: False, 0: True}[val]
|
||||
|
||||
def setter(self, val):
|
||||
val = {'nochange': 3, None: 2, False: 1, True: 0}[val]
|
||||
self.widgets[1].setCurrentIndex(val)
|
||||
val = {None: 2, False: 1, True: 0}[val]
|
||||
self.main_widget.setCurrentIndex(val)
|
||||
self.ignore_change_signals = False
|
||||
|
||||
def commit(self, book_ids, notify=False):
|
||||
if not self.a_c_checkbox.isChecked():
|
||||
return
|
||||
val = self.gui_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:
|
||||
val = False
|
||||
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
||||
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
|
||||
val = False
|
||||
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
||||
|
||||
class BulkInt(BulkBase, Int):
|
||||
pass
|
||||
class BulkInt(BulkBase):
|
||||
|
||||
class BulkFloat(BulkBase, Float):
|
||||
pass
|
||||
def setup_ui(self, parent):
|
||||
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):
|
||||
pass
|
||||
def setter(self, val):
|
||||
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):
|
||||
pass
|
||||
def getter(self):
|
||||
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):
|
||||
|
||||
def setup_ui(self, parent):
|
||||
self.make_widgets(parent, EnComboBox)
|
||||
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
||||
values.sort(key=sort_key)
|
||||
w = EnComboBox(parent)
|
||||
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
||||
w.setMinimumContentsLength(25)
|
||||
self.name_widget = w
|
||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
|
||||
|
||||
self.main_widget.setSizeAdjustPolicy(self.main_widget.AdjustToMinimumContentsLengthWithIcon)
|
||||
self.main_widget.setMinimumContentsLength(25)
|
||||
self.widgets.append(QLabel('', parent))
|
||||
w = QWidget(parent)
|
||||
layout = QHBoxLayout(w)
|
||||
@ -555,15 +694,24 @@ class BulkSeries(BulkBase):
|
||||
layout.addWidget(self.series_start_number)
|
||||
layout.addItem(QSpacerItem(20, 10, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
||||
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):
|
||||
self.idx_widget.setChecked(False)
|
||||
for c in self.all_values:
|
||||
self.name_widget.addItem(c)
|
||||
self.name_widget.setEditText('')
|
||||
self.main_widget.addItem(c)
|
||||
self.main_widget.setEditText('')
|
||||
self.a_c_checkbox.setChecked(False)
|
||||
|
||||
def getter(self):
|
||||
n = unicode(self.name_widget.currentText()).strip()
|
||||
n = unicode(self.main_widget.currentText()).strip()
|
||||
i = self.idx_widget.checkState()
|
||||
f = self.force_number.checkState()
|
||||
s = self.series_start_number.value()
|
||||
@ -571,6 +719,8 @@ class BulkSeries(BulkBase):
|
||||
return n, i, f, s, r
|
||||
|
||||
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 = None if clear else self.normalize_ui_val(val)
|
||||
if clear or val != '':
|
||||
@ -598,9 +748,9 @@ class BulkEnumeration(BulkBase, Enumeration):
|
||||
|
||||
def get_initial_value(self, book_ids):
|
||||
value = None
|
||||
ret_value = None
|
||||
first = True
|
||||
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)
|
||||
if val and val not in self.col_metadata['display']['enum_values']:
|
||||
if not dialog_shown:
|
||||
@ -610,44 +760,32 @@ class BulkEnumeration(BulkBase, Enumeration):
|
||||
self.col_metadata['name']),
|
||||
show=True, show_copy_button=False)
|
||||
dialog_shown = True
|
||||
ret_value = ' nochange '
|
||||
elif (value is not None and value != val) or (val and i != 0):
|
||||
ret_value = ' nochange '
|
||||
value = val
|
||||
if ret_value is None:
|
||||
return value
|
||||
return ret_value
|
||||
if first:
|
||||
value = val
|
||||
first = False
|
||||
elif value != val:
|
||||
value = None
|
||||
if not value:
|
||||
self.ignore_change_signals = False
|
||||
return value
|
||||
|
||||
def setup_ui(self, parent):
|
||||
self.parent = parent
|
||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
||||
QComboBox(parent)]
|
||||
w = self.widgets[1]
|
||||
self.make_widgets(parent, QComboBox)
|
||||
vals = self.col_metadata['display']['enum_values']
|
||||
w.addItem('Do Not Change')
|
||||
w.addItem('')
|
||||
for v in vals:
|
||||
w.addItem(v)
|
||||
self.main_widget.blockSignals(True)
|
||||
self.main_widget.addItem('')
|
||||
self.main_widget.addItems(vals)
|
||||
self.main_widget.blockSignals(False)
|
||||
|
||||
def getter(self):
|
||||
if self.widgets[1].currentIndex() == 0:
|
||||
return ' nochange '
|
||||
return unicode(self.widgets[1].currentText())
|
||||
return unicode(self.main_widget.currentText())
|
||||
|
||||
def setter(self, val):
|
||||
if val == ' nochange ':
|
||||
self.widgets[1].setCurrentIndex(0)
|
||||
if val is None:
|
||||
self.main_widget.setCurrentIndex(0)
|
||||
else:
|
||||
if val is None:
|
||||
self.widgets[1].setCurrentIndex(1)
|
||||
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)
|
||||
self.main_widget.setCurrentIndex(self.main_widget.findText(val))
|
||||
self.ignore_change_signals = False
|
||||
|
||||
class RemoveTags(QWidget):
|
||||
|
||||
@ -658,11 +796,10 @@ class RemoveTags(QWidget):
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.tags_box = CompleteLineEdit(parent, values)
|
||||
layout.addWidget(self.tags_box, stretch = 1)
|
||||
# self.tags_box.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||
|
||||
layout.addWidget(self.tags_box, stretch=3)
|
||||
self.checkbox = QCheckBox(_('Remove all tags'), parent)
|
||||
layout.addWidget(self.checkbox)
|
||||
layout.addStretch(1)
|
||||
self.setLayout(layout)
|
||||
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.sort(key=sort_key)
|
||||
if self.col_metadata['is_multiple']:
|
||||
w = CompleteLineEdit(parent, values)
|
||||
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||
self.widgets = [QLabel('&'+self.col_metadata['name']+': ' +
|
||||
_('tags to add'), parent), w]
|
||||
self.adding_widget = w
|
||||
self.make_widgets(parent, CompleteLineEdit,
|
||||
extra_label_text=_('tags to add'))
|
||||
self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||
self.adding_widget = self.main_widget
|
||||
|
||||
w = RemoveTags(parent, values)
|
||||
self.widgets.append(QLabel('&'+self.col_metadata['name']+': ' +
|
||||
_('tags to remove'), parent))
|
||||
self.widgets.append(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:
|
||||
w = EnComboBox(parent)
|
||||
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
||||
w.setMinimumContentsLength(25)
|
||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
|
||||
self.make_widgets(parent, EnComboBox)
|
||||
self.main_widget.setSizeAdjustPolicy(
|
||||
self.main_widget.AdjustToMinimumContentsLengthWithIcon)
|
||||
self.main_widget.setMinimumContentsLength(25)
|
||||
self.ignore_change_signals = False
|
||||
|
||||
def initialize(self, book_ids):
|
||||
if self.col_metadata['is_multiple']:
|
||||
self.widgets[1].update_items_cache(self.all_values)
|
||||
self.main_widget.update_items_cache(self.all_values)
|
||||
else:
|
||||
val = self.get_initial_value(book_ids)
|
||||
self.initial_val = val = self.normalize_db_val(val)
|
||||
idx = None
|
||||
self.main_widget.blockSignals(True)
|
||||
for i, c in enumerate(self.all_values):
|
||||
if c == val:
|
||||
idx = i
|
||||
self.widgets[1].addItem(c)
|
||||
self.widgets[1].setEditText('')
|
||||
self.main_widget.addItem(c)
|
||||
self.main_widget.setEditText('')
|
||||
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):
|
||||
if not self.a_c_checkbox.isChecked():
|
||||
return
|
||||
if self.col_metadata['is_multiple']:
|
||||
remove_all, adding, rtext = self.gui_val
|
||||
remove = set()
|
||||
@ -740,7 +883,7 @@ class BulkText(BulkBase):
|
||||
unicode(self.adding_widget.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:
|
||||
val = None
|
||||
return val
|
||||
|
@ -8,15 +8,12 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, sys
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4.Qt import QDialog, SIGNAL
|
||||
|
||||
from calibre.customize.ui import config
|
||||
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
|
||||
|
||||
class Catalog(QDialog, Ui_Dialog):
|
||||
class Catalog(ResizableDialog, Ui_Dialog):
|
||||
''' Catalog Dialog builder'''
|
||||
|
||||
def __init__(self, parent, dbspec, ids, db):
|
||||
@ -24,10 +21,8 @@ class Catalog(QDialog, Ui_Dialog):
|
||||
from calibre import prints as info
|
||||
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
|
||||
|
||||
# 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.format.currentIndexChanged.connect(self.show_plugin_tab)
|
||||
self.connect(self.buttonBox.button(QtGui.QDialogButtonBox.Apply),
|
||||
SIGNAL("clicked()"),
|
||||
self.apply)
|
||||
self.buttonBox.button(self.buttonBox.Apply).clicked.connect(self.apply)
|
||||
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):
|
||||
cf = unicode(self.format.currentText()).lower()
|
||||
while self.tabs.count() > 1:
|
||||
@ -157,8 +154,9 @@ class Catalog(QDialog, Ui_Dialog):
|
||||
dynamic.set('catalog_last_used_title', self.catalog_title)
|
||||
self.catalog_sync = bool(self.sync.isChecked())
|
||||
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
|
||||
self.save_catalog_settings()
|
||||
if self.tabs.count() > 1:
|
||||
@ -166,4 +164,9 @@ class Catalog(QDialog, Ui_Dialog):
|
||||
|
||||
def accept(self):
|
||||
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)
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
<string>Generate catalog</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset>
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/library.png</normaloff>:/images/library.png</iconset>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
@ -31,81 +31,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</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 &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 &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>&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">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
@ -116,10 +41,110 @@
|
||||
</property>
|
||||
</widget>
|
||||
</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 &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 &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>&Send catalog to device automatically</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../../work/calibre/resources/images.qrc"/>
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
|
@ -3,16 +3,132 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import os
|
||||
import os, shutil
|
||||
|
||||
from PyQt4.Qt import QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QLabel, \
|
||||
QPushButton, QDialogButtonBox, QApplication, QTreeWidgetItem, \
|
||||
QLineEdit, Qt
|
||||
QLineEdit, Qt, QProgressBar, QSize, QTimer
|
||||
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.library.check_library import CheckLibrary, CHECKS
|
||||
from calibre.library.database2 import delete_file, delete_tree
|
||||
from calibre import prints
|
||||
from calibre import prints, as_unicode
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.library.sqlite import DBThread, OperationalError
|
||||
|
||||
class DBCheck(QDialog):
|
||||
|
||||
def __init__(self, parent, db):
|
||||
QDialog.__init__(self, parent)
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
self.l1 = QLabel(_('Checking database integrity')+'...')
|
||||
self.setWindowTitle(_('Checking database integrity'))
|
||||
self.l.addWidget(self.l1)
|
||||
self.pb = QProgressBar(self)
|
||||
self.l.addWidget(self.pb)
|
||||
self.pb.setMaximum(0)
|
||||
self.pb.setMinimum(0)
|
||||
self.msg = QLabel('')
|
||||
self.l.addWidget(self.msg)
|
||||
self.msg.setWordWrap(True)
|
||||
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
|
||||
self.l.addWidget(self.bb)
|
||||
self.bb.rejected.connect(self.reject)
|
||||
self.resize(self.sizeHint() + QSize(100, 50))
|
||||
self.error = None
|
||||
self.db = db
|
||||
self.closed_orig_conn = False
|
||||
|
||||
def start(self):
|
||||
self.user_version = self.db.user_version
|
||||
self.rejected = False
|
||||
self.db.clean()
|
||||
self.db.conn.close()
|
||||
self.closed_orig_conn = True
|
||||
t = DBThread(self.db.dbpath, False)
|
||||
t.connect()
|
||||
self.conn = t.conn
|
||||
self.dump = self.conn.iterdump()
|
||||
self.statements = []
|
||||
self.count = 0
|
||||
self.msg.setText(_('Dumping database to SQL'))
|
||||
# Give the backup thread time to stop
|
||||
QTimer.singleShot(2000, self.do_one_dump)
|
||||
self.exec_()
|
||||
|
||||
def do_one_dump(self):
|
||||
if self.rejected:
|
||||
return
|
||||
try:
|
||||
try:
|
||||
self.statements.append(self.dump.next())
|
||||
self.count += 1
|
||||
except StopIteration:
|
||||
self.start_load()
|
||||
return
|
||||
QTimer.singleShot(0, self.do_one_dump)
|
||||
except Exception, e:
|
||||
import traceback
|
||||
self.error = (as_unicode(e), traceback.format_exc())
|
||||
self.reject()
|
||||
|
||||
def start_load(self):
|
||||
self.conn.close()
|
||||
self.pb.setMaximum(self.count)
|
||||
self.pb.setValue(0)
|
||||
self.msg.setText(_('Loading database from SQL'))
|
||||
self.db.conn.close()
|
||||
self.ndbpath = PersistentTemporaryFile('.db')
|
||||
self.ndbpath.close()
|
||||
self.ndbpath = self.ndbpath.name
|
||||
t = DBThread(self.ndbpath, False)
|
||||
t.connect()
|
||||
self.conn = t.conn
|
||||
self.conn.execute('create temporary table temp_sequence(id INTEGER PRIMARY KEY AUTOINCREMENT)')
|
||||
self.conn.commit()
|
||||
|
||||
QTimer.singleShot(0, self.do_one_load)
|
||||
|
||||
def do_one_load(self):
|
||||
if self.rejected:
|
||||
return
|
||||
if self.count > 0:
|
||||
try:
|
||||
try:
|
||||
self.conn.execute(self.statements.pop(0))
|
||||
except OperationalError:
|
||||
if self.count > 1:
|
||||
# The last statement in the dump could be an extra
|
||||
# commit, so ignore it.
|
||||
raise
|
||||
self.pb.setValue(self.pb.value() + 1)
|
||||
self.count -= 1
|
||||
QTimer.singleShot(0, self.do_one_load)
|
||||
except Exception, e:
|
||||
import traceback
|
||||
self.error = (as_unicode(e), traceback.format_exc())
|
||||
self.reject()
|
||||
|
||||
else:
|
||||
self.replace_db()
|
||||
|
||||
def replace_db(self):
|
||||
self.conn.commit()
|
||||
self.conn.execute('pragma user_version=%d'%int(self.user_version))
|
||||
self.conn.commit()
|
||||
self.conn.close()
|
||||
shutil.copyfile(self.ndbpath, self.db.dbpath)
|
||||
self.db = None
|
||||
self.accept()
|
||||
|
||||
def break_cycles(self):
|
||||
self.statements = self.unpickler = self.db = self.conn = None
|
||||
|
||||
def reject(self):
|
||||
self.rejected = True
|
||||
QDialog.reject(self)
|
||||
|
||||
|
||||
class Item(QTreeWidgetItem):
|
||||
pass
|
||||
|
@ -39,6 +39,12 @@ class MessageBox(QDialog, Ui_Dialog):
|
||||
self.det_msg.setPlainText(det_msg)
|
||||
self.det_msg.setVisible(False)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
if det_msg:
|
||||
self.show_det_msg = _('Show &details')
|
||||
self.hide_det_msg = _('Hide &details')
|
||||
@ -47,11 +53,6 @@ class MessageBox(QDialog, Ui_Dialog):
|
||||
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)
|
||||
|
@ -208,6 +208,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
from calibre.gui2 import config
|
||||
title = unicode(self.title.text()).strip()
|
||||
author = unicode(self.authors.text()).strip()
|
||||
if author.endswith('&'):
|
||||
author = author[:-1]
|
||||
if not title or not author:
|
||||
return error_dialog(self, _('Specify title and author'),
|
||||
_('You must specify a title and author before generating '
|
||||
|
@ -259,14 +259,14 @@ class Scheduler(QObject):
|
||||
if self.oldest > 0:
|
||||
delta = timedelta(days=self.oldest)
|
||||
try:
|
||||
ids = self.recipe_model.db.tags_older_than(_('News'), delta)
|
||||
ids = list(self.recipe_model.db.tags_older_than(_('News'),
|
||||
delta))
|
||||
except:
|
||||
# Should never happen
|
||||
ids = []
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if ids:
|
||||
ids = list(ids)
|
||||
if ids:
|
||||
self.delete_old_news.emit(ids)
|
||||
QTimer.singleShot(60 * 60 * 1000, self.oldest_check)
|
||||
|
@ -87,7 +87,7 @@ class MainWindow(QMainWindow):
|
||||
fe = sio.getvalue()
|
||||
prints(fe, file=sys.stderr)
|
||||
msg = '<b>%s</b>:'%type.__name__ + unicode(str(value), 'utf8', 'replace')
|
||||
error_dialog(self, _('ERROR: Unhandled exception'), msg, det_msg=fe,
|
||||
error_dialog(self, _('Unhandled exception'), msg, det_msg=fe,
|
||||
show=True)
|
||||
except BaseException:
|
||||
pass
|
||||
|
@ -64,6 +64,8 @@ class TagDelegate(QItemDelegate): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
TAG_SEARCH_STATES = {'clear': 0, 'mark_plus': 1, 'mark_minus': 2}
|
||||
|
||||
class TagsView(QTreeView): # {{{
|
||||
|
||||
refresh_required = pyqtSignal()
|
||||
@ -177,9 +179,16 @@ class TagsView(QTreeView): # {{{
|
||||
return joiner.join(tokens)
|
||||
|
||||
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())
|
||||
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)
|
||||
|
||||
def conditional_clear(self, search_string):
|
||||
@ -187,7 +196,7 @@ class TagsView(QTreeView): # {{{
|
||||
self.clear()
|
||||
|
||||
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:
|
||||
return
|
||||
try:
|
||||
@ -201,11 +210,10 @@ class TagsView(QTreeView): # {{{
|
||||
self.user_category_edit.emit(category)
|
||||
return
|
||||
if action == 'search':
|
||||
self.tags_marked.emit(('not ' if negate else '') +
|
||||
category + ':"=' + key + '"')
|
||||
self._toggle(index, set_to=search_state)
|
||||
return
|
||||
if action == 'search_category':
|
||||
self.tags_marked.emit(category + ':' + str(not negate))
|
||||
self.tags_marked.emit(key + ':' + search_state)
|
||||
return
|
||||
if action == 'manage_searches':
|
||||
self.saved_search_edit.emit(category)
|
||||
@ -270,20 +278,16 @@ class TagsView(QTreeView): # {{{
|
||||
partial(self.context_menu_handler,
|
||||
action='edit_author_sort', index=tag_id))
|
||||
# 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,
|
||||
_('Search for %s')%tag_name,
|
||||
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,
|
||||
_('Search for everything but %s')%tag_name,
|
||||
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()
|
||||
# Hide/Show/Restore categories
|
||||
self.context_menu.addAction(_('Hide category %s') % category,
|
||||
@ -299,11 +303,11 @@ class TagsView(QTreeView): # {{{
|
||||
self.context_menu.addAction(self.search_icon,
|
||||
_('Search for books in category %s')%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,
|
||||
_('Search for books not in category %s')%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
|
||||
self.context_menu.addSeparator()
|
||||
if key in ['tags', 'publisher', 'series'] or \
|
||||
@ -528,9 +532,15 @@ class TagTreeItem(object): # {{{
|
||||
return QVariant(self.tooltip)
|
||||
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:
|
||||
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):
|
||||
res = []
|
||||
@ -1014,11 +1024,15 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
def clear_state(self):
|
||||
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
|
||||
item = index.internalPointer()
|
||||
if item.type == TagTreeItem.TAG:
|
||||
item.toggle()
|
||||
item.toggle(set_to=set_to)
|
||||
if exclusive:
|
||||
self.reset_all_states(except_=item.tag)
|
||||
self.dataChanged.emit(index, index)
|
||||
@ -1040,8 +1054,9 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
category_item = self.root_item.children[row_index]
|
||||
for tag_item in category_item.child_tags():
|
||||
tag = tag_item.tag
|
||||
if tag.state > 0:
|
||||
prefix = ' not ' if tag.state == 2 else ''
|
||||
if tag.state != TAG_SEARCH_STATES['clear']:
|
||||
prefix = ' not ' if tag.state == TAG_SEARCH_STATES['mark_minus'] \
|
||||
else ''
|
||||
category = key if key != 'news' else 'tag'
|
||||
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)))
|
||||
|
@ -408,7 +408,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
def booklists(self):
|
||||
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
|
||||
|
||||
def library_moved(self, newloc, copy_structure=False):
|
||||
def library_moved(self, newloc, copy_structure=False, call_close=True):
|
||||
if newloc is None: return
|
||||
default_prefs = None
|
||||
try:
|
||||
@ -441,7 +441,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
self.apply_named_search_restriction(db.prefs['gui_restriction'])
|
||||
if olddb is not None:
|
||||
try:
|
||||
olddb.conn.close()
|
||||
if call_close:
|
||||
olddb.conn.close()
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
@ -75,13 +75,13 @@ class FilenamePattern(QWidget, Ui_Form):
|
||||
# has added.
|
||||
val_hist = [unicode(self.re.lineEdit().text())] + [unicode(self.re.itemText(i)) for i in xrange(self.re.count())]
|
||||
self.re.clear()
|
||||
|
||||
|
||||
if defaults:
|
||||
val = prefs.defaults['filename_pattern']
|
||||
else:
|
||||
val = prefs['filename_pattern']
|
||||
self.re.lineEdit().setText(val)
|
||||
|
||||
|
||||
val_hist += gprefs.get('filename_pattern_history', ['(?P<title>.+)', '(?P<author>[^_-]+) -?\s*(?P<series>[^_0-9-]*)(?P<series_index>[0-9]*)\s*-\s*(?P<title>[^_].+) ?'])
|
||||
if val in val_hist:
|
||||
del val_hist[val_hist.index(val)]
|
||||
@ -129,15 +129,15 @@ class FilenamePattern(QWidget, Ui_Form):
|
||||
def commit(self):
|
||||
pat = self.pattern().pattern
|
||||
prefs['filename_pattern'] = pat
|
||||
|
||||
|
||||
history = []
|
||||
history_pats = [unicode(self.re.lineEdit().text())] + [unicode(self.re.itemText(i)) for i in xrange(self.re.count())]
|
||||
for p in history_pats[:14]:
|
||||
# Ensure we don't have duplicate items.
|
||||
if p and p not in history:
|
||||
history.append(p)
|
||||
gprefs['filename_pattern_history'] = history
|
||||
|
||||
gprefs['filename_pattern_history'] = history
|
||||
|
||||
return pat
|
||||
|
||||
|
||||
@ -503,7 +503,7 @@ class CompleteLineEdit(EnLineEdit):
|
||||
cursor_pos = self.cursorPosition()
|
||||
before_text = unicode(self.text())[:cursor_pos]
|
||||
after_text = unicode(self.text())[cursor_pos:]
|
||||
prefix_len = len(before_text.split(self.separator)[-1].strip())
|
||||
prefix_len = len(before_text.split(self.separator)[-1].lstrip())
|
||||
if self.space_before_sep:
|
||||
complete_text_pat = '%s%s %s %s'
|
||||
len_extra = 3
|
||||
|
@ -33,10 +33,10 @@ from calibre.gui2.dialogs.progress import ProgressDialog
|
||||
|
||||
class Device(object):
|
||||
|
||||
output_profile = 'default'
|
||||
output_profile = 'generic_eink'
|
||||
output_format = 'EPUB'
|
||||
name = 'Default'
|
||||
manufacturer = 'Default'
|
||||
name = 'Generic e-ink device'
|
||||
manufacturer = 'Generic'
|
||||
id = 'default'
|
||||
supports_color = False
|
||||
|
||||
@ -63,6 +63,18 @@ class Device(object):
|
||||
recs['dont_grayscale'] = True
|
||||
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):
|
||||
|
||||
@ -206,12 +218,21 @@ class iPhone(Device):
|
||||
|
||||
class Android(Device):
|
||||
|
||||
name = 'Adroid phone + WordPlayer/Aldiko'
|
||||
name = 'Android phone'
|
||||
output_format = 'EPUB'
|
||||
manufacturer = 'Android'
|
||||
id = 'android'
|
||||
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):
|
||||
|
||||
name = 'Hanlin V3'
|
||||
@ -268,9 +289,9 @@ def get_manufacturers():
|
||||
mans = set([])
|
||||
for x in get_devices():
|
||||
mans.add(x.manufacturer)
|
||||
if 'Default' in mans:
|
||||
mans.remove('Default')
|
||||
return ['Default'] + sorted(mans)
|
||||
if Device.manufacturer in mans:
|
||||
mans.remove(Device.manufacturer)
|
||||
return [Device.manufacturer] + sorted(mans)
|
||||
|
||||
def get_devices_of(manufacturer):
|
||||
ans = [d for d in get_devices() if d.manufacturer == manufacturer]
|
||||
@ -402,22 +423,6 @@ class StanzaPage(QWizardPage, StanzaUI):
|
||||
except:
|
||||
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):
|
||||
|
||||
@ -430,6 +435,8 @@ class DevicePage(QWizardPage, DeviceUI):
|
||||
self.registerField("device", self.device_view)
|
||||
|
||||
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.manufacturer_view.setModel(self.man_model)
|
||||
previous = dynamic.get('welcome_wizard_device', False)
|
||||
@ -477,8 +484,6 @@ class DevicePage(QWizardPage, DeviceUI):
|
||||
return KindlePage.ID
|
||||
if dev is iPhone:
|
||||
return StanzaPage.ID
|
||||
if dev is Android:
|
||||
return WordPlayerPage.ID
|
||||
return FinishPage.ID
|
||||
|
||||
class MoveMonitor(QObject):
|
||||
@ -753,13 +758,11 @@ class Wizard(QWizard):
|
||||
self.set_finish_text()
|
||||
self.kindle_page = KindlePage()
|
||||
self.stanza_page = StanzaPage()
|
||||
self.word_player_page = WordPlayerPage()
|
||||
self.setPage(self.library_page.ID, self.library_page)
|
||||
self.setPage(self.device_page.ID, self.device_page)
|
||||
self.setPage(self.finish_page.ID, self.finish_page)
|
||||
self.setPage(self.kindle_page.ID, self.kindle_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
|
||||
nh, nw = min_available_height()-75, available_width()-30
|
||||
|
@ -27,7 +27,7 @@
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label">
|
||||
<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 name="wordWrap">
|
||||
<bool>true</bool>
|
||||
|
@ -49,7 +49,7 @@ class MetadataBackup(Thread): # {{{
|
||||
def run(self):
|
||||
while self.keep_running:
|
||||
try:
|
||||
time.sleep(2) # Limit to two per second
|
||||
time.sleep(2) # Limit to one book per two seconds
|
||||
(id_, sequence) = self.db.get_a_dirtied_book()
|
||||
if id_ is None:
|
||||
continue
|
||||
|
@ -24,10 +24,9 @@ from calibre.utils.logging import default_log as log
|
||||
from calibre.utils.zipfile import ZipFile, ZipInfo
|
||||
from calibre.utils.magick.draw import thumbnail
|
||||
|
||||
FIELDS = ['all', 'author_sort', 'authors', 'comments',
|
||||
'cover', 'formats', 'id', 'isbn', 'ondevice', 'pubdate', 'publisher', 'rating',
|
||||
'series_index', 'series', 'size', 'tags', 'timestamp', 'title',
|
||||
'uuid']
|
||||
FIELDS = ['all', 'title', 'author_sort', 'authors', 'comments',
|
||||
'cover', 'formats','id', 'isbn', 'ondevice', 'pubdate', 'publisher',
|
||||
'rating', 'series_index', 'series', 'size', 'tags', 'timestamp', 'uuid']
|
||||
|
||||
#Allowed fields for template
|
||||
TEMPLATE_ALLOWED_FIELDS = [ 'author_sort', 'authors', 'id', 'isbn', 'pubdate',
|
||||
@ -252,6 +251,15 @@ class BIBTEX(CatalogPlugin): # {{{
|
||||
"Default: '%default'\n"
|
||||
"Applies to: BIBTEX output format")),
|
||||
|
||||
Option('--add-files-path',
|
||||
default = 'True',
|
||||
dest = 'addfiles',
|
||||
action = None,
|
||||
help = _('Create a file entry if formats is selected for BibTeX entries.\n'
|
||||
'Boolean value: True, False\n'
|
||||
"Default: '%default'\n"
|
||||
"Applies to: BIBTEX output format")),
|
||||
|
||||
Option('--citation-template',
|
||||
default = '{authors}{id}',
|
||||
dest = 'bib_cit',
|
||||
@ -298,7 +306,7 @@ class BIBTEX(CatalogPlugin): # {{{
|
||||
from calibre.utils.bibtex import BibTeX
|
||||
|
||||
def create_bibtex_entry(entry, fields, mode, template_citation,
|
||||
bibtexdict, citation_bibtex = True):
|
||||
bibtexdict, citation_bibtex=True, calibre_files=True):
|
||||
|
||||
#Bibtex doesn't like UTF-8 but keep unicode until writing
|
||||
#Define starting chain or if book valid strict and not book return a Fail string
|
||||
@ -360,8 +368,13 @@ class BIBTEX(CatalogPlugin): # {{{
|
||||
bibtex_entry.append(u'isbn = "%s"' % re.sub(u'[\D]', u'', item))
|
||||
|
||||
elif field == 'formats' :
|
||||
item = u', '.join([format.rpartition('.')[2].lower() for format in item])
|
||||
bibtex_entry.append(u'formats = "%s"' % item)
|
||||
#Add file path if format is selected
|
||||
formats = [format.rpartition('.')[2].lower() for format in item]
|
||||
bibtex_entry.append(u'formats = "%s"' % u', '.join(formats))
|
||||
if calibre_files:
|
||||
files = [u':%s:%s' % (format, format.rpartition('.')[2].upper())\
|
||||
for format in item]
|
||||
bibtex_entry.append(u'files = "%s"' % u', '.join(files))
|
||||
|
||||
elif field == 'series_index' :
|
||||
bibtex_entry.append(u'volume = "%s"' % int(item))
|
||||
@ -510,32 +523,41 @@ class BIBTEX(CatalogPlugin): # {{{
|
||||
citation_bibtex= True
|
||||
else :
|
||||
citation_bibtex= opts.impcit
|
||||
|
||||
#Check add file entry and go to default in case of bad CLI
|
||||
if isinstance(opts.addfiles, (StringType, UnicodeType)) :
|
||||
if opts.addfiles == 'False' :
|
||||
addfiles_bibtex = False
|
||||
elif opts.addfiles == 'True' :
|
||||
addfiles_bibtex = True
|
||||
else :
|
||||
log(" WARNING: incorrect --add-files-path, revert to default")
|
||||
addfiles_bibtex= True
|
||||
else :
|
||||
addfiles_bibtex = opts.addfiles
|
||||
|
||||
#Preprocess for error and light correction
|
||||
template_citation = preprocess_template(opts.bib_cit)
|
||||
|
||||
#Open output and write entries
|
||||
outfile = codecs.open(path_to_output, 'w', bibfile_enc, bibfile_enctag)
|
||||
with codecs.open(path_to_output, 'w', bibfile_enc, bibfile_enctag)\
|
||||
as outfile:
|
||||
#File header
|
||||
nb_entries = len(data)
|
||||
#check in book strict if all is ok else throw a warning into log
|
||||
if bib_entry == 'book' :
|
||||
nb_books = len(filter(check_entry_book_valid, data))
|
||||
if nb_books < nb_entries :
|
||||
log(" WARNING: only %d entries in %d are book compatible" % (nb_books, nb_entries))
|
||||
nb_entries = nb_books
|
||||
|
||||
#File header
|
||||
nb_entries = len(data)
|
||||
outfile.write(u'%%%Calibre catalog\n%%%{0} entries in catalog\n\n'.format(nb_entries))
|
||||
outfile.write(u'@preamble{"This catalog of %d entries was generated by calibre on %s"}\n\n'
|
||||
% (nb_entries, nowf().strftime("%A, %d. %B %Y %H:%M").decode(preferred_encoding)))
|
||||
|
||||
#check in book strict if all is ok else throw a warning into log
|
||||
if bib_entry == 'book' :
|
||||
nb_books = len(filter(check_entry_book_valid, data))
|
||||
if nb_books < nb_entries :
|
||||
log(" WARNING: only %d entries in %d are book compatible" % (nb_books, nb_entries))
|
||||
nb_entries = nb_books
|
||||
|
||||
outfile.write(u'%%%Calibre catalog\n%%%{0} entries in catalog\n\n'.format(nb_entries))
|
||||
outfile.write(u'@preamble{"This catalog of %d entries was generated by calibre on %s"}\n\n'
|
||||
% (nb_entries, nowf().strftime("%A, %d. %B %Y %H:%M").decode(preferred_encoding)))
|
||||
|
||||
for entry in data:
|
||||
outfile.write(create_bibtex_entry(entry, fields, bib_entry, template_citation,
|
||||
bibtexc, citation_bibtex))
|
||||
|
||||
outfile.close()
|
||||
for entry in data:
|
||||
outfile.write(create_bibtex_entry(entry, fields, bib_entry, template_citation,
|
||||
bibtexc, citation_bibtex, addfiles_bibtex))
|
||||
# }}}
|
||||
|
||||
class EPUB_MOBI(CatalogPlugin):
|
||||
|
@ -21,7 +21,7 @@ from calibre.library.field_metadata import FieldMetadata, TagsIcons
|
||||
from calibre.library.schema_upgrades import SchemaUpgrade
|
||||
from calibre.library.caches import ResultCache
|
||||
from calibre.library.custom_columns import CustomColumns
|
||||
from calibre.library.sqlite import connect, IntegrityError, DBThread
|
||||
from calibre.library.sqlite import connect, IntegrityError
|
||||
from calibre.library.prefs import DBPrefs
|
||||
from calibre.ebooks.metadata import string_to_authors, authors_to_string
|
||||
from calibre.ebooks.metadata.book.base import Metadata
|
||||
@ -618,9 +618,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
'''
|
||||
with self.dirtied_lock:
|
||||
dc_sequence = self.dirtied_cache.get(book_id, None)
|
||||
# print 'clear_dirty: check book', book_id, dc_sequence
|
||||
# print 'clear_dirty: check book', book_id, dc_sequence
|
||||
if dc_sequence is None or sequence is None or dc_sequence == sequence:
|
||||
# print 'needs to be cleaned'
|
||||
# print 'needs to be cleaned'
|
||||
self.conn.execute('DELETE FROM metadata_dirtied WHERE book=?',
|
||||
(book_id,))
|
||||
self.conn.commit()
|
||||
@ -629,7 +629,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
except:
|
||||
pass
|
||||
elif dc_sequence is not None:
|
||||
# print 'book needs to be done again'
|
||||
# print 'book needs to be done again'
|
||||
pass
|
||||
|
||||
def dump_metadata(self, book_ids=None, remove_from_dirtied=True,
|
||||
@ -661,12 +661,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
changed = False
|
||||
for book in book_ids:
|
||||
with self.dirtied_lock:
|
||||
# print 'dirtied: check id', book
|
||||
# print 'dirtied: check id', book
|
||||
if book in self.dirtied_cache:
|
||||
self.dirtied_cache[book] = self.dirtied_sequence
|
||||
self.dirtied_sequence += 1
|
||||
continue
|
||||
# print 'book not already dirty'
|
||||
# print 'book not already dirty'
|
||||
try:
|
||||
self.conn.execute(
|
||||
'INSERT INTO metadata_dirtied (book) VALUES (?)',
|
||||
@ -720,7 +720,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
# 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
|
||||
# print 'get_md_for_dump', idx, sequence
|
||||
try:
|
||||
# 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.
|
||||
@ -827,7 +827,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
try:
|
||||
book_ids = self.data.parse(query)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return identical_book_ids
|
||||
for book_id in book_ids:
|
||||
@ -2797,82 +2796,3 @@ books_series_link feeds
|
||||
yield id, title, script
|
||||
|
||||
|
||||
def check_integrity(self, callback):
|
||||
callback(0., _('Checking SQL integrity...'))
|
||||
self.clean()
|
||||
user_version = self.user_version
|
||||
sql = '\n'.join(self.conn.dump())
|
||||
self.conn.close()
|
||||
dest = self.dbpath+'.tmp'
|
||||
if os.path.exists(dest):
|
||||
os.remove(dest)
|
||||
conn = None
|
||||
try:
|
||||
ndb = DBThread(dest, None)
|
||||
ndb.connect()
|
||||
conn = ndb.conn
|
||||
conn.execute('create table temp_sequence(id INTEGER PRIMARY KEY AUTOINCREMENT)')
|
||||
conn.commit()
|
||||
conn.executescript(sql)
|
||||
conn.commit()
|
||||
conn.execute('pragma user_version=%d'%user_version)
|
||||
conn.commit()
|
||||
conn.execute('drop table temp_sequence')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except:
|
||||
if conn is not None:
|
||||
try:
|
||||
conn.close()
|
||||
except:
|
||||
pass
|
||||
if os.path.exists(dest):
|
||||
os.remove(dest)
|
||||
raise
|
||||
else:
|
||||
shutil.copyfile(dest, self.dbpath)
|
||||
self.connect()
|
||||
self.initialize_dynamic()
|
||||
self.refresh()
|
||||
if os.path.exists(dest):
|
||||
os.remove(dest)
|
||||
callback(0.1, _('Checking for missing files.'))
|
||||
bad = {}
|
||||
us = self.data.universal_set()
|
||||
total = float(len(us))
|
||||
for i, id in enumerate(us):
|
||||
formats = self.data.get(id, self.FIELD_MAP['formats'], row_is_id=True)
|
||||
if not formats:
|
||||
formats = []
|
||||
else:
|
||||
formats = [x.lower() for x in formats.split(',')]
|
||||
actual_formats = self.formats(id, index_is_id=True)
|
||||
if not actual_formats:
|
||||
actual_formats = []
|
||||
else:
|
||||
actual_formats = [x.lower() for x in actual_formats.split(',')]
|
||||
|
||||
for fmt in formats:
|
||||
if fmt in actual_formats:
|
||||
continue
|
||||
if id not in bad:
|
||||
bad[id] = []
|
||||
bad[id].append(fmt)
|
||||
has_cover = self.data.get(id, self.FIELD_MAP['cover'],
|
||||
row_is_id=True)
|
||||
if has_cover and self.cover(id, index_is_id=True, as_path=True) is None:
|
||||
if id not in bad:
|
||||
bad[id] = []
|
||||
bad[id].append('COVER')
|
||||
callback(0.1+0.9*(1+i)/total, _('Checked id') + ' %d'%id)
|
||||
|
||||
for id in bad:
|
||||
for fmt in bad[id]:
|
||||
if fmt != 'COVER':
|
||||
self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, fmt.upper()))
|
||||
else:
|
||||
self.conn.execute('UPDATE books SET has_cover=0 WHERE id=?', (id,))
|
||||
self.conn.commit()
|
||||
self.refresh_ids(list(bad.keys()))
|
||||
|
||||
return bad
|
||||
|
@ -17,6 +17,7 @@ class DBPrefs(dict):
|
||||
dict.__init__(self)
|
||||
self.db = db
|
||||
self.defaults = {}
|
||||
self.disable_setting = False
|
||||
for key, val in self.db.conn.get('SELECT key,val FROM preferences'):
|
||||
try:
|
||||
val = self.raw_to_object(val)
|
||||
@ -45,6 +46,8 @@ class DBPrefs(dict):
|
||||
self.db.conn.commit()
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
if self.disable_setting:
|
||||
return
|
||||
raw = self.to_raw(val)
|
||||
self.db.conn.execute('DELETE FROM preferences WHERE key=?', (key,))
|
||||
self.db.conn.execute('INSERT INTO preferences (key,val) VALUES (?,?)', (key,
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user