KG/chaley updates

This commit is contained in:
GRiker 2010-12-14 12:12:05 -07:00
commit 16fc28a1ed
46 changed files with 998 additions and 139 deletions

View File

@ -0,0 +1,23 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Hiroshi Miura <miurahr@linux.com>'
'''
ajiajin.com/blog
'''
from calibre.web.feeds.news import BasicNewsRecipe
class AjiajinBlog(BasicNewsRecipe):
title = u'Ajiajin blog'
__author__ = 'Hiroshi Miura'
oldest_article = 5
publication_type = 'blog'
max_articles_per_feed = 100
description = 'The next generation internet trends in Japan and Asia'
publisher = ''
category = 'internet, asia, japan'
language = 'en'
encoding = 'utf-8'
feeds = [(u'blog', u'http://feeds.feedburner.com/Asiajin')]

View File

@ -0,0 +1,37 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Hiroshi Miura <miurahr@linux.com>'
'''
http://ameblo.jp/
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class SakuraBlog(BasicNewsRecipe):
title = u'chou chou blog'
__author__ = 'Hiroshi Miura'
oldest_article = 4
publication_type = 'blog'
max_articles_per_feed = 20
description = 'Japanese popular dog blog'
publisher = ''
category = 'dog, pet, japan'
language = 'ja'
encoding = 'utf-8'
use_embedded_content = True
feeds = [(u'blog', u'http://feedblog.ameba.jp/rss/ameblo/chouchou1218/rss20.xml')]
def parse_feeds(self):
feeds = BasicNewsRecipe.parse_feeds(self)
for curfeed in feeds:
delList = []
for a,curarticle in enumerate(curfeed.articles):
if re.search(r'rssad.jp', curarticle.url):
delList.append(curarticle)
if len(delList)>0:
for d in delList:
index = curfeed.articles.index(d)
curfeed.articles[index:index+1] = []
return feeds

View File

@ -3,15 +3,16 @@ __copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
''' '''
http://www.dilbert.com http://www.dilbert.com
''' '''
import re
from calibre.web.feeds.recipes import BasicNewsRecipe from calibre.web.feeds.recipes import BasicNewsRecipe
import re
class DosisDiarias(BasicNewsRecipe): class DilbertBig(BasicNewsRecipe):
title = 'Dilbert' title = 'Dilbert'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic and Starson17'
description = 'Dilbert' description = 'Dilbert'
oldest_article = 5 reverse_article_order = True
oldest_article = 15
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
use_embedded_content = True use_embedded_content = True
@ -29,20 +30,23 @@ class DosisDiarias(BasicNewsRecipe):
feeds = [(u'Dilbert', u'http://feeds.dilbert.com/DilbertDailyStrip' )] feeds = [(u'Dilbert', u'http://feeds.dilbert.com/DilbertDailyStrip' )]
preprocess_regexps = [
(re.compile('strip\..*\.gif', re.DOTALL|re.IGNORECASE),
lambda match: 'strip.zoom.gif')
]
def get_article_url(self, article): def get_article_url(self, article):
return article.get('feedburner_origlink', None) return article.get('feedburner_origlink', None)
preprocess_regexps = [
(re.compile('strip\..*\.gif', re.DOTALL|re.IGNORECASE), lambda match: 'strip.zoom.gif')
]
def preprocess_html(self, soup): def preprocess_html(self, soup):
for tag in soup.findAll(name='a'): for tag in soup.findAll(name='a'):
if tag['href'].find('http://feedads') >= 0: if tag['href'].find('http://feedads') >= 0:
tag.extract() tag.extract()
return soup return soup
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
img {max-width:100%; min-width:100%;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''

View File

@ -0,0 +1,31 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Hiroshi Miura <miurahr@linux.com>'
'''
www.kahoku.co.jp
'''
from calibre.web.feeds.news import BasicNewsRecipe
class KahokuShinpoNews(BasicNewsRecipe):
title = u'\u6cb3\u5317\u65b0\u5831'
__author__ = 'Hiroshi Miura'
oldest_article = 2
max_articles_per_feed = 20
description = 'Tohoku regional news paper in Japan'
publisher = 'Kahoku Shinpo Sha'
category = 'news, japan'
language = 'ja'
encoding = 'Shift_JIS'
no_stylesheets = True
feeds = [(u'news', u'http://www.kahoku.co.jp/rss/index_thk.xml')]
keep_only_tags = [ dict(id="page_title"),
dict(id="news_detail"),
dict(id="bt_title"),
{'class':"photoLeft"},
dict(id="bt_body")
]
remove_tags = [ {'class':"button"}]

View File

@ -0,0 +1,38 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Hiroshi Miura <miurahr@linux.com>'
'''
nationalgeographic.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
import re
class NationalGeographicNews(BasicNewsRecipe):
title = u'National Geographic News'
oldest_article = 7
max_articles_per_feed = 100
remove_javascript = True
no_stylesheets = True
use_embedded_content = False
feeds = [(u'news', u'http://feeds.nationalgeographic.com/ng/News/News_Main')]
remove_tags_before = dict(id='page_head')
remove_tags_after = [dict(id='social_buttons'),{'class':'aside'}]
remove_tags = [
{'class':'hidden'}
]
def parse_feeds(self):
feeds = BasicNewsRecipe.parse_feeds(self)
for curfeed in feeds:
delList = []
for a,curarticle in enumerate(curfeed.articles):
if re.search(r'ads\.pheedo\.com', curarticle.url):
delList.append(curarticle)
if len(delList)>0:
for d in delList:
index = curfeed.articles.index(d)
curfeed.articles[index:index+1] = []
return feeds

View File

@ -0,0 +1,20 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Hiroshi Miura <miurahr@linux.com>'
'''
nationalgeographic.co.jp
'''
from calibre.web.feeds.news import BasicNewsRecipe
import re
class NationalGeoJp(BasicNewsRecipe):
title = u'\u30ca\u30b7\u30e7\u30ca\u30eb\u30fb\u30b8\u30aa\u30b0\u30e9\u30d5\u30a3\u30c3\u30af\u30cb\u30e5\u30fc\u30b9'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
feeds = [(u'news', u'http://www.nationalgeographic.co.jp/news/rss.php')]
def print_version(self, url):
return re.sub(r'news_article.php','news_printer_friendly.php', url)

View File

@ -10,8 +10,8 @@ import mechanize
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
class NikkeiNet_sub_life(BasicNewsRecipe): class NikkeiNet_sub_shakai(BasicNewsRecipe):
title = u'\u65e5\u7d4c\u65b0\u805e\u96fb\u5b50\u7248(\u751f\u6d3b)' title = u'\u65e5\u7d4c\u65b0\u805e\u96fb\u5b50\u7248(Social)'
__author__ = 'Hiroshi Miura' __author__ = 'Hiroshi Miura'
description = 'News and current market affairs from Japan' description = 'News and current market affairs from Japan'
cover_url = 'http://parts.nikkei.com/parts/ds/images/common/logo_r1.svg' cover_url = 'http://parts.nikkei.com/parts/ds/images/common/logo_r1.svg'

View File

@ -0,0 +1,58 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Hiroshi Miura <miurahr@linux.com>'
'''
paperli
'''
from calibre.web.feeds.news import BasicNewsRecipe
from calibre import strftime
class paperli_topics(BasicNewsRecipe):
# Customize this recipe and change paperli_tag and title below to
# download news on your favorite tag
paperli_tag = 'climate'
title = u'The #climate Daily - paperli'
#-------------------------------------------------------------
__author__ = 'Hiroshi Miura'
oldest_article = 7
max_articles_per_feed = 100
description = 'paper.li page about '+ paperli_tag
publisher = 'paper.li'
category = 'paper.li'
language = 'en'
encoding = 'utf-8'
remove_javascript = True
masthead_title = u'The '+ paperli_tag +' Daily'
timefmt = '[%y/%m/%d]'
base_url = 'http://paper.li'
index = base_url+'/tag/'+paperli_tag
def parse_index(self):
# get topics
topics = []
soup = self.index_to_soup(self.index)
topics_lists = soup.find('div',attrs={'class':'paper-nav-bottom'})
for item in topics_lists.findAll('li', attrs={'class':""}):
itema = item.find('a',href=True)
topics.append({'title': itema.string, 'url': itema['href']})
#get feeds
feeds = []
for topic in topics:
newsarticles = []
soup = self.index_to_soup(''.join([self.base_url, topic['url'] ]))
topstories = soup.findAll('div',attrs={'class':'yui-u'})
for itt in topstories:
itema = itt.find('a',href=True,attrs={'class':'ts'})
if itema is not None:
itemd = itt.find('div',text=True, attrs={'class':'text'})
newsarticles.append({
'title' :itema.string
,'date' :strftime(self.timefmt)
,'url' :itema['href']
,'description':itemd.string
})
feeds.append((topic['title'], newsarticles))
return feeds

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python
import re
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import Tag
class SBM(BasicNewsRecipe):
title = 'Science Based Medicine'
__author__ = 'BuzzKill'
description = 'Exploring issues and controversies in the relationship between science and medicine'
oldest_article = 5
max_articles_per_feed = 15
no_stylesheets = True
use_embedded_content = False
encoding = 'utf-8'
publisher = 'SBM'
category = 'science, sbm, ebm, blog, pseudoscience'
language = 'en'
lang = 'en-US'
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : lang
, 'pretty_print' : True
}
keep_only_tags = [
dict(name='a', attrs={'title':re.compile(r'Posts by.*', re.DOTALL|re.IGNORECASE)}),
dict(name='div', attrs={'class':'entry'})
]
feeds = [(u'Science Based Medicine', u'http://www.sciencebasedmedicine.org/?feed=rss2')]
def preprocess_html(self, soup):
mtag = Tag(soup,'meta',[('http-equiv','Content-Type'),('context','text/html; charset=utf-8')])
soup.head.insert(0,mtag)
soup.html['lang'] = self.lang
return self.adeify_images(soup)

View File

@ -0,0 +1,36 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Hiroshi Miura <miurahr@linux.com>'
'''
http://ameblo.jp/sauta19/
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class UniNoHimituKichiBlog(BasicNewsRecipe):
title = u'Uni secret base'
__author__ = 'Hiroshi Miura'
oldest_article = 2
publication_type = 'blog'
max_articles_per_feed = 20
description = 'Japanese famous Cat blog'
publisher = ''
category = 'cat, pet, japan'
language = 'ja'
encoding = 'utf-8'
feeds = [(u'blog', u'http://feedblog.ameba.jp/rss/ameblo/sauta19/rss20.xml')]
def parse_feeds(self):
feeds = BasicNewsRecipe.parse_feeds(self)
for curfeed in feeds:
delList = []
for a,curarticle in enumerate(curfeed.articles):
if re.search(r'rssad.jp', curarticle.url):
delList.append(curarticle)
if len(delList)>0:
for d in delList:
index = curfeed.articles.index(d)
curfeed.articles[index:index+1] = []
return feeds

View File

@ -60,8 +60,8 @@ class ZeitDe(BasicNewsRecipe):
for tag in soup.findAll(name=['ul','li']): for tag in soup.findAll(name=['ul','li']):
tag.name = 'div' tag.name = 'div'
soup.html['xml:lang'] = self.lang soup.html['xml:lang'] = self.language.replace('_', '-')
soup.html['lang'] = self.lang soup.html['lang'] = self.language.replace('_', '-')
mtag = '<meta http-equiv="Content-Type" content="text/html; charset=' + self.encoding + '">' mtag = '<meta http-equiv="Content-Type" content="text/html; charset=' + self.encoding + '">'
soup.head.insert(0,mtag) soup.head.insert(0,mtag)
return soup return soup

View File

@ -36,6 +36,11 @@ class UserFeedback(DeviceError):
self.details = details self.details = details
self.msg = msg self.msg = msg
class OpenFeedback(DeviceError):
def __init__(self, msg):
self.feedback_msg = msg
DeviceError.__init__(self, msg)
class DeviceBusy(ProtocolError): class DeviceBusy(ProtocolError):
""" Raised when device is busy """ """ Raised when device is busy """
def __init__(self, uerr=""): def __init__(self, uerr=""):

View File

@ -216,6 +216,9 @@ class DevicePlugin(Plugin):
an implementation of an implementation of
this function that should serve as a good example for USB Mass storage this function that should serve as a good example for USB Mass storage
devices. devices.
This method can raise an OpenFeedback exception to display a message to
the user.
''' '''
raise NotImplementedError() raise NotImplementedError()

View File

@ -120,7 +120,7 @@ def add_pipeline_options(parser, plumber):
[ [
'base_font_size', 'disable_font_rescaling', 'base_font_size', 'disable_font_rescaling',
'font_size_mapping', 'font_size_mapping',
'line_height', 'line_height', 'minimum_line_height',
'linearize_tables', 'linearize_tables',
'extra_css', 'smarten_punctuation', 'extra_css', 'smarten_punctuation',
'margin_top', 'margin_left', 'margin_right', 'margin_top', 'margin_left', 'margin_right',

View File

@ -160,13 +160,30 @@ OptionRecommendation(name='disable_font_rescaling',
) )
), ),
OptionRecommendation(name='minimum_line_height',
recommended_value=120.0, level=OptionRecommendation.LOW,
help=_(
'The minimum line height, as a percentage of the element\'s '
'calculated font size. calibre will ensure that every element '
'has a line height of at least this setting, irrespective of '
'what the input document specifies. Set to zero to disable. '
'Default is 120%. Use this setting in preference to '
'the direct line height specification, unless you know what '
'you are doing. For example, you can achieve "double spaced" '
'text by setting this to 240.'
)
),
OptionRecommendation(name='line_height', OptionRecommendation(name='line_height',
recommended_value=0, level=OptionRecommendation.LOW, recommended_value=0, level=OptionRecommendation.LOW,
help=_('The line height in pts. Controls spacing between consecutive ' help=_(
'lines of text. By default no line height manipulation is ' 'The line height in pts. Controls spacing between consecutive '
'performed.' 'lines of text. Only applies to elements that do not define '
) 'their own line height. In most cases, the minimum line height '
'option is more useful. '
'By default no line height manipulation is performed.'
)
), ),
OptionRecommendation(name='linearize_tables', OptionRecommendation(name='linearize_tables',

View File

@ -73,6 +73,10 @@ class FB2MLizer(object):
text = re.sub(r'(?miu)<p>\s*</p>', '', text) text = re.sub(r'(?miu)<p>\s*</p>', '', text)
text = re.sub(r'(?miu)\s+</p>', '</p>', text) text = re.sub(r'(?miu)\s+</p>', '</p>', text)
text = re.sub(r'(?miu)</p><p>', '</p>\n\n<p>', text) text = re.sub(r'(?miu)</p><p>', '</p>\n\n<p>', text)
if self.opts.insert_blank_line:
text = re.sub(r'(?miu)</p>', '</p><empty-line />', text)
return text return text
def fb2_header(self): def fb2_header(self):
@ -293,6 +297,18 @@ class FB2MLizer(object):
s_out, s_tags = self.handle_simple_tag('emphasis', tag_stack+tags) s_out, s_tags = self.handle_simple_tag('emphasis', tag_stack+tags)
fb2_out += s_out fb2_out += s_out
tags += s_tags tags += s_tags
elif tag in ('del', 'strike'):
s_out, s_tags = self.handle_simple_tag('strikethrough', tag_stack+tags)
fb2_out += s_out
tags += s_tags
elif tag == 'sub':
s_out, s_tags = self.handle_simple_tag('sub', tag_stack+tags)
fb2_out += s_out
tags += s_tags
elif tag == 'sup':
s_out, s_tags = self.handle_simple_tag('sup', tag_stack+tags)
fb2_out += s_out
tags += s_tags
# Processes style information. # Processes style information.
if style['font-style'] == 'italic': if style['font-style'] == 'italic':
@ -303,6 +319,10 @@ class FB2MLizer(object):
s_out, s_tags = self.handle_simple_tag('strong', tag_stack+tags) s_out, s_tags = self.handle_simple_tag('strong', tag_stack+tags)
fb2_out += s_out fb2_out += s_out
tags += s_tags tags += s_tags
elif style['text-decoration'] == 'line-through':
s_out, s_tags = self.handle_simple_tag('strikethrough', tag_stack+tags)
fb2_out += s_out
tags += s_tags
# Process element text. # Process element text.
if hasattr(elem_tree, 'text') and elem_tree.text: if hasattr(elem_tree, 'text') and elem_tree.text:

View File

@ -314,6 +314,8 @@ class HTMLInput(InputFormatPlugin):
rewrite_links, urlnormalize, urldefrag, BINARY_MIME, OEB_STYLES, \ rewrite_links, urlnormalize, urldefrag, BINARY_MIME, OEB_STYLES, \
xpath xpath
from calibre import guess_type from calibre import guess_type
from calibre.ebooks.oeb.transforms.metadata import \
meta_info_to_oeb_metadata
import cssutils import cssutils
self.OEB_STYLES = OEB_STYLES self.OEB_STYLES = OEB_STYLES
oeb = create_oebbook(log, None, opts, self, oeb = create_oebbook(log, None, opts, self,
@ -321,15 +323,7 @@ class HTMLInput(InputFormatPlugin):
self.oeb = oeb self.oeb = oeb
metadata = oeb.metadata metadata = oeb.metadata
if mi.title: meta_info_to_oeb_metadata(mi, metadata, log)
metadata.add('title', mi.title)
if mi.authors:
for a in mi.authors:
metadata.add('creator', a, attrib={'role':'aut'})
if mi.publisher:
metadata.add('publisher', mi.publisher)
if mi.isbn:
metadata.add('identifier', mi.isbn, attrib={'scheme':'ISBN'})
if not metadata.language: if not metadata.language:
oeb.logger.warn(u'Language not specified') oeb.logger.warn(u'Language not specified')
metadata.add('language', get_lang().replace('_', '-')) metadata.add('language', get_lang().replace('_', '-'))

View File

@ -170,7 +170,27 @@ def get_metadata_(src, encoding=None):
if match: if match:
series = match.group(1) series = match.group(1)
if series: if series:
pat = re.compile(r'\[([.0-9]+)\]')
match = pat.search(series)
series_index = None
if match is not None:
try:
series_index = float(match.group(1))
except:
pass
series = series.replace(match.group(), '').strip()
mi.series = ent_pat.sub(entity_to_unicode, series) mi.series = ent_pat.sub(entity_to_unicode, series)
if series_index is None:
pat = get_meta_regexp_("Seriesnumber")
match = pat.search(src)
if match:
try:
series_index = float(match.group(1))
except:
pass
if series_index is not None:
mi.series_index = series_index
# RATING # RATING
rating = None rating = None

View File

@ -184,7 +184,7 @@ class MobiMLizer(object):
para.attrib['value'] = str(istates[-2].list_num) para.attrib['value'] = str(istates[-2].list_num)
elif tag in NESTABLE_TAGS and istate.rendered: elif tag in NESTABLE_TAGS and istate.rendered:
para = wrapper = bstate.nested[-1] para = wrapper = bstate.nested[-1]
elif left > 0 and indent >= 0: elif not self.opts.mobi_ignore_margins and left > 0 and indent >= 0:
ems = self.profile.mobi_ems_per_blockquote ems = self.profile.mobi_ems_per_blockquote
para = wrapper = etree.SubElement(parent, XHTML('blockquote')) para = wrapper = etree.SubElement(parent, XHTML('blockquote'))
para = wrapper para = wrapper

View File

@ -39,6 +39,12 @@ class MOBIOutput(OutputFormatPlugin):
OptionRecommendation(name='personal_doc', recommended_value='[PDOC]', OptionRecommendation(name='personal_doc', recommended_value='[PDOC]',
help=_('Tag marking book to be filed with Personal Docs') help=_('Tag marking book to be filed with Personal Docs')
), ),
OptionRecommendation(name='mobi_ignore_margins',
recommended_value=False,
help=_('Ignore margins in the input document. If False, then '
'the MOBI output plugin will try to convert margins specified'
' in the input document, otherwise it will ignore them.')
),
]) ])
def check_for_periodical(self): def check_for_periodical(self):

View File

@ -633,12 +633,12 @@ class Style(object):
parent = self._getparent() parent = self._getparent()
if 'line-height' in self._style: if 'line-height' in self._style:
lineh = self._style['line-height'] lineh = self._style['line-height']
if lineh == 'normal':
lineh = '1.2'
try: try:
float(lineh) result = float(lineh) * self.fontSize
except ValueError: except ValueError:
result = self._unit_convert(lineh, base=self.fontSize) result = self._unit_convert(lineh, base=self.fontSize)
else:
result = float(lineh) * self.fontSize
elif parent is not None: elif parent is not None:
# TODO: proper inheritance # TODO: proper inheritance
result = parent.lineHeight result = parent.lineHeight

View File

@ -245,6 +245,8 @@ class CSSFlattener(object):
del node.attrib['bgcolor'] del node.attrib['bgcolor']
if cssdict.get('font-weight', '').lower() == 'medium': if cssdict.get('font-weight', '').lower() == 'medium':
cssdict['font-weight'] = 'normal' # ADE chokes on font-weight medium cssdict['font-weight'] = 'normal' # ADE chokes on font-weight medium
fsize = font_size
if not self.context.disable_font_rescaling: if not self.context.disable_font_rescaling:
_sbase = self.sbase if self.sbase is not None else \ _sbase = self.sbase if self.sbase is not None else \
self.context.source.fbase self.context.source.fbase
@ -258,6 +260,14 @@ class CSSFlattener(object):
fsize = self.fmap[font_size] fsize = self.fmap[font_size]
cssdict['font-size'] = "%0.5fem" % (fsize / psize) cssdict['font-size'] = "%0.5fem" % (fsize / psize)
psize = fsize psize = fsize
try:
minlh = self.context.minimum_line_height / 100.
if style['line-height'] < minlh * fsize:
cssdict['line-height'] = str(minlh)
except:
self.oeb.logger.exception('Failed to set minimum line-height')
if cssdict: if cssdict:
if self.lineh and self.fbase and tag != 'body': if self.lineh and self.fbase and tag != 'body':
self.clean_edges(cssdict, style, psize) self.clean_edges(cssdict, style, psize)
@ -290,6 +300,7 @@ class CSSFlattener(object):
lineh = self.lineh / psize lineh = self.lineh / psize
cssdict['line-height'] = "%0.5fem" % lineh cssdict['line-height'] = "%0.5fem" % lineh
if (self.context.remove_paragraph_spacing or if (self.context.remove_paragraph_spacing or
self.context.insert_blank_line) and tag in ('p', 'div'): self.context.insert_blank_line) and tag in ('p', 'div'):
if item_id != 'calibre_jacket' or self.context.output_profile.name == 'Kindle': if item_id != 'calibre_jacket' or self.context.output_profile.name == 'Kindle':

View File

@ -77,10 +77,14 @@ class TXTInput(InputFormatPlugin):
base = os.getcwdu() base = os.getcwdu()
if hasattr(stream, 'name'): if hasattr(stream, 'name'):
base = os.path.dirname(stream.name) base = os.path.dirname(stream.name)
htmlfile = open(os.path.join(base, 'temp_calibre_txt_input_to_html.html'), fname = os.path.join(base, 'index.html')
'wb') c = 0
htmlfile.write(html.encode('utf-8')) while os.path.exists(fname):
htmlfile.close() c += 1
fname = 'index%d.html'%c
htmlfile = open(fname, 'wb')
with htmlfile:
htmlfile.write(html.encode('utf-8'))
cwd = os.getcwdu() cwd = os.getcwdu()
odi = options.debug_pipeline odi = options.debug_pipeline
options.debug_pipeline = None options.debug_pipeline = None

View File

@ -9,7 +9,7 @@ from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \
QByteArray, QTranslator, QCoreApplication, QThread, \ QByteArray, QTranslator, QCoreApplication, QThread, \
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \ QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \ QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
QIcon, QApplication, QDialog, QPushButton, QUrl QIcon, QApplication, QDialog, QPushButton, QUrl, QFont
ORG_NAME = 'KovidsBrain' ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500' APP_UID = 'libprs500'
@ -52,6 +52,7 @@ gprefs.defaults['show_splash_screen'] = True
gprefs.defaults['toolbar_icon_size'] = 'medium' gprefs.defaults['toolbar_icon_size'] = 'medium'
gprefs.defaults['toolbar_text'] = 'auto' gprefs.defaults['toolbar_text'] = 'auto'
gprefs.defaults['show_child_bar'] = False gprefs.defaults['show_child_bar'] = False
gprefs.defaults['font'] = None
# }}} # }}}
@ -613,6 +614,10 @@ class Application(QApplication):
qt_app = self qt_app = self
self._file_open_paths = [] self._file_open_paths = []
self._file_open_lock = RLock() self._file_open_lock = RLock()
self.original_font = QFont(QApplication.font())
fi = gprefs['font']
if fi is not None:
QApplication.setFont(QFont(*fi))
def _send_file_open_events(self): def _send_file_open_events(self):
with self._file_open_lock: with self._file_open_lock:

View File

@ -154,15 +154,17 @@ class EditMetadataAction(InterfaceAction):
d.view_format.connect(lambda d.view_format.connect(lambda
fmt:self.gui.iactions['View'].view_format(row_list[current_row], fmt:self.gui.iactions['View'].view_format(row_list[current_row],
fmt)) fmt))
if d.exec_() != d.Accepted: ret = d.exec_()
d.view_format.disconnect() d.break_cycles()
if ret != d.Accepted:
break break
d.view_format.disconnect()
changed.add(d.id) changed.add(d.id)
if d.row_delta == 0: if d.row_delta == 0:
break break
current_row += d.row_delta current_row += d.row_delta
if changed: if changed:
self.gui.library_view.model().refresh_ids(list(changed)) self.gui.library_view.model().refresh_ids(list(changed))
current = self.gui.library_view.currentIndex() current = self.gui.library_view.currentIndex()

View File

@ -10,7 +10,7 @@ import textwrap
from functools import partial from functools import partial
from PyQt4.Qt import QWidget, QSpinBox, QDoubleSpinBox, QLineEdit, QTextEdit, \ from PyQt4.Qt import QWidget, QSpinBox, QDoubleSpinBox, QLineEdit, QTextEdit, \
QCheckBox, QComboBox, Qt, QIcon, pyqtSignal QCheckBox, QComboBox, Qt, QIcon, pyqtSignal, QLabel
from calibre.customize.conversion import OptionRecommendation from calibre.customize.conversion import OptionRecommendation
from calibre.ebooks.conversion.config import load_defaults, \ from calibre.ebooks.conversion.config import load_defaults, \
@ -81,6 +81,21 @@ class Widget(QWidget):
self.apply_recommendations(defaults) self.apply_recommendations(defaults)
self.setup_help(get_help) self.setup_help(get_help)
def process_child(child):
for g in child.children():
if isinstance(g, QLabel):
buddy = g.buddy()
if buddy is not None and hasattr(buddy, '_help'):
g._help = buddy._help
htext = unicode(buddy.toolTip()).strip()
g.setToolTip(htext)
g.setWhatsThis(htext)
g.__class__.enterEvent = lambda obj, event: self.set_help(getattr(obj, '_help', obj.toolTip()))
else:
process_child(g)
process_child(self)
def restore_defaults(self, get_option): def restore_defaults(self, get_option):
defaults = GuiRecommendations() defaults = GuiRecommendations()
defaults.merge_recommendations(get_option, OptionRecommendation.LOW, defaults.merge_recommendations(get_option, OptionRecommendation.LOW,

View File

@ -21,7 +21,7 @@ class LookAndFeelWidget(Widget, Ui_Form):
def __init__(self, parent, get_option, get_help, db=None, book_id=None): def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent, Widget.__init__(self, parent,
['change_justification', 'extra_css', 'base_font_size', ['change_justification', 'extra_css', 'base_font_size',
'font_size_mapping', 'line_height', 'font_size_mapping', 'line_height', 'minimum_line_height',
'linearize_tables', 'smarten_punctuation', 'linearize_tables', 'smarten_punctuation',
'disable_font_rescaling', 'insert_blank_line', 'disable_font_rescaling', 'insert_blank_line',
'remove_paragraph_spacing', 'remove_paragraph_spacing_indent_size','input_encoding', 'remove_paragraph_spacing', 'remove_paragraph_spacing_indent_size','input_encoding',

View File

@ -97,7 +97,7 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="3" column="0"> <item row="4" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string>Line &amp;height:</string> <string>Line &amp;height:</string>
@ -107,7 +107,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1" colspan="2"> <item row="4" column="1" colspan="2">
<widget class="QDoubleSpinBox" name="opt_line_height"> <widget class="QDoubleSpinBox" name="opt_line_height">
<property name="suffix"> <property name="suffix">
<string> pt</string> <string> pt</string>
@ -117,7 +117,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="5" column="0">
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
<string>Input character &amp;encoding:</string> <string>Input character &amp;encoding:</string>
@ -127,17 +127,17 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1" colspan="3"> <item row="5" column="1" colspan="3">
<widget class="QLineEdit" name="opt_input_encoding"/> <widget class="QLineEdit" name="opt_input_encoding"/>
</item> </item>
<item row="5" column="0" colspan="2"> <item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="opt_remove_paragraph_spacing"> <widget class="QCheckBox" name="opt_remove_paragraph_spacing">
<property name="text"> <property name="text">
<string>Remove &amp;spacing between paragraphs</string> <string>Remove &amp;spacing between paragraphs</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="2" colspan="2"> <item row="6" column="2" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2"> <layout class="QHBoxLayout" name="horizontalLayout_2">
<item> <item>
<widget class="QLabel" name="label_4"> <widget class="QLabel" name="label_4">
@ -164,21 +164,21 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="6" column="0"> <item row="7" column="0">
<widget class="QLabel" name="label_5"> <widget class="QLabel" name="label_5">
<property name="text"> <property name="text">
<string>Text justification:</string> <string>Text justification:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="0"> <item row="8" column="0">
<widget class="QCheckBox" name="opt_linearize_tables"> <widget class="QCheckBox" name="opt_linearize_tables">
<property name="text"> <property name="text">
<string>&amp;Linearize tables</string> <string>&amp;Linearize tables</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="10" column="0" colspan="4"> <item row="11" column="0" colspan="4">
<widget class="QGroupBox" name="groupBox"> <widget class="QGroupBox" name="groupBox">
<property name="title"> <property name="title">
<string>Extra &amp;CSS</string> <string>Extra &amp;CSS</string>
@ -190,37 +190,60 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="6" column="2" colspan="2"> <item row="7" column="2" colspan="2">
<widget class="QComboBox" name="opt_change_justification"/> <widget class="QComboBox" name="opt_change_justification"/>
</item> </item>
<item row="7" column="1" colspan="3"> <item row="8" column="1" colspan="3">
<widget class="QCheckBox" name="opt_asciiize"> <widget class="QCheckBox" name="opt_asciiize">
<property name="text"> <property name="text">
<string>&amp;Transliterate unicode characters to ASCII</string> <string>&amp;Transliterate unicode characters to ASCII</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="0"> <item row="9" column="0">
<widget class="QCheckBox" name="opt_insert_blank_line"> <widget class="QCheckBox" name="opt_insert_blank_line">
<property name="text"> <property name="text">
<string>Insert &amp;blank line</string> <string>Insert &amp;blank line</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="1" colspan="2"> <item row="9" column="1" colspan="2">
<widget class="QCheckBox" name="opt_keep_ligatures"> <widget class="QCheckBox" name="opt_keep_ligatures">
<property name="text"> <property name="text">
<string>Keep &amp;ligatures</string> <string>Keep &amp;ligatures</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="9" column="0"> <item row="10" column="0">
<widget class="QCheckBox" name="opt_smarten_punctuation"> <widget class="QCheckBox" name="opt_smarten_punctuation">
<property name="text"> <property name="text">
<string>Smarten &amp;punctuation</string> <string>Smarten &amp;punctuation</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Minimum &amp;line height:</string>
</property>
<property name="buddy">
<cstring>opt_minimum_line_height</cstring>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<widget class="QDoubleSpinBox" name="opt_minimum_line_height">
<property name="suffix">
<string> %</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="maximum">
<double>900.000000000000000</double>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources> <resources>

View File

@ -25,6 +25,7 @@ class PluginWidget(Widget, Ui_Form):
def __init__(self, parent, get_option, get_help, db=None, book_id=None): def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent, Widget.__init__(self, parent,
['prefer_author_sort', 'rescale_images', 'toc_title', ['prefer_author_sort', 'rescale_images', 'toc_title',
'mobi_ignore_margins',
'dont_compress', 'no_inline_toc', 'masthead_font','personal_doc'] 'dont_compress', 'no_inline_toc', 'masthead_font','personal_doc']
) )
self.db, self.book_id = db, book_id self.db, self.book_id = db, book_id

View File

@ -55,7 +55,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0" colspan="2"> <item row="6" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox"> <widget class="QGroupBox" name="groupBox">
<property name="title"> <property name="title">
<string>Kindle options</string> <string>Kindle options</string>
@ -101,7 +101,7 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="6" column="0"> <item row="7" column="0">
<spacer name="verticalSpacer_2"> <spacer name="verticalSpacer_2">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -114,6 +114,13 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item row="5" column="0">
<widget class="QCheckBox" name="opt_mobi_ignore_margins">
<property name="text">
<string>Ignore &amp;margins</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View File

@ -12,7 +12,7 @@ from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, \
from calibre.customize.ui import available_input_formats, available_output_formats, \ from calibre.customize.ui import available_input_formats, available_output_formats, \
device_plugins device_plugins
from calibre.devices.interface import DevicePlugin from calibre.devices.interface import DevicePlugin
from calibre.devices.errors import UserFeedback from calibre.devices.errors import UserFeedback, OpenFeedback
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.utils.ipc.job import BaseJob from calibre.utils.ipc.job import BaseJob
from calibre.devices.scanner import DeviceScanner from calibre.devices.scanner import DeviceScanner
@ -122,7 +122,8 @@ def device_name_for_plugboards(device_class):
class DeviceManager(Thread): # {{{ class DeviceManager(Thread): # {{{
def __init__(self, connected_slot, job_manager, open_feedback_slot, sleep_time=2): def __init__(self, connected_slot, job_manager, open_feedback_slot,
open_feedback_msg, sleep_time=2):
''' '''
:sleep_time: Time to sleep between device probes in secs :sleep_time: Time to sleep between device probes in secs
''' '''
@ -143,6 +144,7 @@ class DeviceManager(Thread): # {{{
self.ejected_devices = set([]) self.ejected_devices = set([])
self.mount_connection_requests = Queue.Queue(0) self.mount_connection_requests = Queue.Queue(0)
self.open_feedback_slot = open_feedback_slot self.open_feedback_slot = open_feedback_slot
self.open_feedback_msg = open_feedback_msg
def report_progress(self, *args): def report_progress(self, *args):
pass pass
@ -163,6 +165,9 @@ class DeviceManager(Thread): # {{{
dev.reset(detected_device=detected_device, dev.reset(detected_device=detected_device,
report_progress=self.report_progress) report_progress=self.report_progress)
dev.open() dev.open()
except OpenFeedback, e:
self.open_feedback_msg(dev.get_gui_name(), e.feedback_msg)
continue
except: except:
tb = traceback.format_exc() tb = traceback.format_exc()
if DEBUG or tb not in self.reported_errors: if DEBUG or tb not in self.reported_errors:
@ -594,11 +599,16 @@ class DeviceMixin(object): # {{{
_('Error communicating with device'), ' ') _('Error communicating with device'), ' ')
self.device_error_dialog.setModal(Qt.NonModal) self.device_error_dialog.setModal(Qt.NonModal)
self.device_manager = DeviceManager(Dispatcher(self.device_detected), self.device_manager = DeviceManager(Dispatcher(self.device_detected),
self.job_manager, Dispatcher(self.status_bar.show_message)) self.job_manager, Dispatcher(self.status_bar.show_message),
Dispatcher(self.show_open_feedback))
self.device_manager.start() self.device_manager.start()
if tweaks['auto_connect_to_folder']: if tweaks['auto_connect_to_folder']:
self.connect_to_folder_named(tweaks['auto_connect_to_folder']) self.connect_to_folder_named(tweaks['auto_connect_to_folder'])
def show_open_feedback(self, devname, msg):
self.__of_dev_mem__ = d = info_dialog(self, devname, msg)
d.show()
def auto_convert_question(self, msg, autos): def auto_convert_question(self, msg, autos):
autos = u'\n'.join(map(unicode, map(force_unicode, autos))) autos = u'\n'.join(map(unicode, map(force_unicode, autos)))
return self.ask_a_yes_no_question( return self.ask_a_yes_no_question(

View File

@ -102,7 +102,7 @@ class MyBlockingBusy(QDialog):
remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, \ remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, \
do_autonumber, do_remove_format, remove_format, do_swap_ta, \ do_autonumber, do_remove_format, remove_format, do_swap_ta, \
do_remove_conv, do_auto_author, series, do_series_restart, \ do_remove_conv, do_auto_author, series, do_series_restart, \
series_start_value, do_title_case, clear_series = self.args series_start_value, do_title_case, cover_action, clear_series = self.args
# first loop: do author and title. These will commit at the end of each # first loop: do author and title. These will commit at the end of each
@ -129,6 +129,23 @@ class MyBlockingBusy(QDialog):
self.db.set_title(id, titlecase(title), notify=False) self.db.set_title(id, titlecase(title), notify=False)
if au: if au:
self.db.set_authors(id, string_to_authors(au), notify=False) self.db.set_authors(id, string_to_authors(au), notify=False)
if cover_action == 'remove':
self.db.remove_cover(id)
elif cover_action == 'generate':
from calibre.ebooks import calibre_cover
from calibre.ebooks.metadata import fmt_sidx
from calibre.gui2 import config
mi = self.db.get_metadata(id, index_is_id=True)
series_string = None
if mi.series:
series_string = _('Book %s of %s')%(
fmt_sidx(mi.series_index,
use_roman=config['use_roman_numerals_for_series_number']),
mi.series)
cdata = calibre_cover(mi.title, mi.format_field('authors')[-1],
series_string=series_string)
self.db.set_cover(id, cdata)
elif self.current_phase == 2: elif self.current_phase == 2:
# All of these just affect the DB, so we can tolerate a total rollback # All of these just affect the DB, so we can tolerate a total rollback
if do_auto_author: if do_auto_author:
@ -678,11 +695,16 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
do_remove_conv = self.remove_conversion_settings.isChecked() do_remove_conv = self.remove_conversion_settings.isChecked()
do_auto_author = self.auto_author_sort.isChecked() do_auto_author = self.auto_author_sort.isChecked()
do_title_case = self.change_title_to_title_case.isChecked() do_title_case = self.change_title_to_title_case.isChecked()
cover_action = None
if self.cover_remove.isChecked():
cover_action = 'remove'
elif self.cover_generate.isChecked():
cover_action = 'generate'
args = (remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, args = (remove_all, remove, add, au, aus, do_aus, rating, pub, do_series,
do_autonumber, do_remove_format, remove_format, do_swap_ta, do_autonumber, do_remove_format, remove_format, do_swap_ta,
do_remove_conv, do_auto_author, series, do_series_restart, do_remove_conv, do_auto_author, series, do_series_restart,
series_start_value, do_title_case, clear_series) series_start_value, do_title_case, cover_action, clear_series)
bb = MyBlockingBusy(_('Applying changes to %d books.\nPhase {0} {1}%%.') bb = MyBlockingBusy(_('Applying changes to %d books.\nPhase {0} {1}%%.')
%len(self.ids), args, self.db, self.ids, %len(self.ids), args, self.db, self.ids,

View File

@ -381,7 +381,7 @@ Future conversion of these books will use the default settings.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="15" column="0" colspan="3"> <item row="14" column="0" colspan="3">
<spacer name="verticalSpacer_2"> <spacer name="verticalSpacer_2">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -394,6 +394,39 @@ Future conversion of these books will use the default settings.</string>
</property> </property>
</spacer> </spacer>
</item> </item>
<item row="13" column="0" colspan="3">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Change &amp;cover</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QRadioButton" name="cover_no_change">
<property name="text">
<string>&amp;No change</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="cover_remove">
<property name="text">
<string>&amp;Remove cover</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="cover_generate">
<property name="text">
<string>&amp;Generate default cover</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tab"> <widget class="QWidget" name="tab">

View File

@ -240,37 +240,39 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.cover_fetcher = CoverFetcher(None, None, isbn, self.cover_fetcher = CoverFetcher(None, None, isbn,
self.timeout, title, author) self.timeout, title, author)
self.cover_fetcher.start() self.cover_fetcher.start()
self._hangcheck = QTimer(self)
self._hangcheck.timeout.connect(self.hangcheck,
type=Qt.QueuedConnection)
self.cf_start_time = time.time() self.cf_start_time = time.time()
self.pi.start(_('Downloading cover...')) self.pi.start(_('Downloading cover...'))
self._hangcheck.start(100) QTimer.singleShot(100, self.hangcheck)
def hangcheck(self): def hangcheck(self):
if self.cover_fetcher.is_alive() and \ cf = self.cover_fetcher
time.time()-self.cf_start_time < self.COVER_FETCH_TIMEOUT: if cf is None:
# Called after dialog closed
return
if cf.is_alive() and \
time.time()-self.cf_start_time < self.COVER_FETCH_TIMEOUT:
QTimer.singleShot(100, self.hangcheck)
return return
self._hangcheck.stop()
try: try:
if self.cover_fetcher.is_alive(): if cf.is_alive():
error_dialog(self, _('Cannot fetch cover'), error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+ _('<b>Could not fetch cover.</b><br/>')+
_('The download timed out.')).exec_() _('The download timed out.')).exec_()
return return
if self.cover_fetcher.needs_isbn: if cf.needs_isbn:
error_dialog(self, _('Cannot fetch cover'), error_dialog(self, _('Cannot fetch cover'),
_('Could not find cover for this book. Try ' _('Could not find cover for this book. Try '
'specifying the ISBN first.')).exec_() 'specifying the ISBN first.')).exec_()
return return
if self.cover_fetcher.exception is not None: if cf.exception is not None:
err = self.cover_fetcher.exception err = cf.exception
error_dialog(self, _('Cannot fetch cover'), error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+unicode(err)).exec_() _('<b>Could not fetch cover.</b><br/>')+unicode(err)).exec_()
return return
if self.cover_fetcher.errors and self.cover_fetcher.cover_data is None: if cf.errors and cf.cover_data is None:
details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in self.cover_fetcher.errors]) details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in cf.errors])
error_dialog(self, _('Cannot fetch cover'), error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>') + _('<b>Could not fetch cover.</b><br/>') +
_('For the error message from each cover source, ' _('For the error message from each cover source, '
@ -278,7 +280,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
return return
pix = QPixmap() pix = QPixmap()
pix.loadFromData(self.cover_fetcher.cover_data) pix.loadFromData(cf.cover_data)
if pix.isNull(): if pix.isNull():
error_dialog(self, _('Bad cover'), error_dialog(self, _('Bad cover'),
_('The cover is not a valid picture')).exec_() _('The cover is not a valid picture')).exec_()
@ -287,11 +289,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.update_cover_tooltip() self.update_cover_tooltip()
self.cover_changed = True self.cover_changed = True
self.cpixmap = pix self.cpixmap = pix
self.cover_data = self.cover_fetcher.cover_data self.cover_data = cf.cover_data
finally: finally:
self.fetch_cover_button.setEnabled(True) self.fetch_cover_button.setEnabled(True)
self.unsetCursor() self.unsetCursor()
self.pi.stop() if self.pi is not None:
self.pi.stop()
# }}} # }}}
@ -438,8 +441,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
def __init__(self, window, row, db, prev=None, def __init__(self, window, row, db, prev=None,
next_=None): next_=None):
ResizableDialog.__init__(self, window) ResizableDialog.__init__(self, window)
self.cover_fetcher = None
self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter) self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter)
self.cancel_all = False
base = unicode(self.author_sort.toolTip()) base = unicode(self.author_sort.toolTip())
self.ok_aus_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+ self.ok_aus_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+
_(' The green color indicates that the current ' _(' The green color indicates that the current '
@ -570,7 +573,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.enable_series_index) QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.enable_series_index)
self.series.lineEdit().editingFinished.connect(self.increment_series_index) self.series.lineEdit().editingFinished.connect(self.increment_series_index)
self.show()
pm = QPixmap() pm = QPixmap()
if cover: if cover:
pm.loadFromData(cover) pm.loadFromData(cover)
@ -590,6 +592,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.original_author = unicode(self.authors.text()).strip() self.original_author = unicode(self.authors.text()).strip()
self.original_title = unicode(self.title.text()).strip() self.original_title = unicode(self.title.text()).strip()
self.show()
def create_custom_column_editors(self): def create_custom_column_editors(self):
w = self.central_widget.widget(1) w = self.central_widget.widget(1)
layout = w.layout() layout = w.layout()
@ -828,10 +832,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.accept() self.accept()
def accept(self): def accept(self):
cf = getattr(self, 'cover_fetcher', None)
if cf is not None and hasattr(cf, 'terminate'):
cf.terminate()
cf.wait()
try: try:
if self.formats_changed: if self.formats_changed:
self.sync_formats() self.sync_formats()
@ -888,14 +888,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
show=True) show=True)
raise raise
self.save_state() self.save_state()
self.cover_fetcher = None
QDialog.accept(self) QDialog.accept(self)
def reject(self, *args): def reject(self, *args):
cf = getattr(self, 'cover_fetcher', None)
if cf is not None and hasattr(cf, 'terminate'):
cf.terminate()
cf.wait()
self.save_state() self.save_state()
self.cover_fetcher = None
QDialog.reject(self, *args) QDialog.reject(self, *args)
def read_state(self): def read_state(self):
@ -910,3 +908,48 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
dynamic.set('metasingle_window_geometry', bytes(self.saveGeometry())) dynamic.set('metasingle_window_geometry', bytes(self.saveGeometry()))
dynamic.set('metasingle_splitter_state', dynamic.set('metasingle_splitter_state',
bytes(self.splitter.saveState())) bytes(self.splitter.saveState()))
def break_cycles(self):
# Break any reference cycles that could prevent python
# from garbage collecting this dialog
def disconnect(signal):
try:
signal.disconnect()
except:
pass # Fails if view format was never connected
disconnect(self.view_format)
for b in ('next_button', 'prev_button'):
x = getattr(self, b, None)
if x is not None:
disconnect(x.clicked)
if __name__ == '__main__':
from calibre.library import db
from PyQt4.Qt import QApplication
from calibre.utils.mem import memory
import gc
app = QApplication([])
db = db()
# Initialize all Qt Objects once
d = MetadataSingleDialog(None, 4, db)
d.break_cycles()
d.reject()
del d
for i in range(5):
gc.collect()
before = memory()
d = MetadataSingleDialog(None, 4, db)
d.reject()
d.break_cycles()
del d
for i in range(5):
gc.collect()
print 'Used memory:', memory(before)/1024.**2, 'MB'

View File

@ -145,7 +145,7 @@ class TagCategories(QDialog, Ui_TagCategories):
index = self.all_items[node.data(Qt.UserRole).toPyObject()].index index = self.all_items[node.data(Qt.UserRole).toPyObject()].index
if index not in self.applied_items: if index not in self.applied_items:
self.applied_items.append(index) self.applied_items.append(index)
self.applied_items.sort(key=lambda x:sort_key(self.all_items[x])) self.applied_items.sort(key=lambda x:sort_key(self.all_items[x].name))
self.display_filtered_categories(None) self.display_filtered_categories(None)
def unapply_tags(self, node=None): def unapply_tags(self, node=None):

View File

@ -105,9 +105,13 @@ class TagListEditor(QDialog, Ui_TagListEditor):
if not question_dialog(self, _('Are your sure?'), if not question_dialog(self, _('Are your sure?'),
'<p>'+_('Are you certain you want to delete the following items?')+'<br>'+ct): '<p>'+_('Are you certain you want to delete the following items?')+'<br>'+ct):
return return
row = self.available_tags.row(deletes[0])
for item in deletes: for item in deletes:
(id,ign) = item.data(Qt.UserRole).toInt() (id,ign) = item.data(Qt.UserRole).toInt()
self.to_delete.append(id) self.to_delete.append(id)
self.available_tags.takeItem(self.available_tags.row(item)) self.available_tags.takeItem(self.available_tags.row(item))
if row >= self.available_tags.count():
row = self.available_tags.count() - 1
if row >= 0:
self.available_tags.scrollToItem(self.available_tags.item(row))

View File

@ -123,6 +123,8 @@ class Stack(QStackedWidget): # {{{
_('Tag Browser'), I('tags.png'), _('Tag Browser'), I('tags.png'),
parent=parent, side_index=0, initial_side_size=200, parent=parent, side_index=0, initial_side_size=200,
shortcut=_('Shift+Alt+T')) shortcut=_('Shift+Alt+T'))
parent.tb_splitter.state_changed.connect(
self.tb_widget.set_pane_is_visible, Qt.QueuedConnection)
parent.tb_splitter.addWidget(self.tb_widget) parent.tb_splitter.addWidget(self.tb_widget)
parent.tb_splitter.addWidget(parent.cb_splitter) parent.tb_splitter.addWidget(parent.cb_splitter)
parent.tb_splitter.setCollapsible(parent.tb_splitter.other_index, False) parent.tb_splitter.setCollapsible(parent.tb_splitter.other_index, False)

View File

@ -5,10 +5,11 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QApplication, QFont, QFontInfo, QFontDialog
from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.look_feel_ui import Ui_Form from calibre.gui2.preferences.look_feel_ui import Ui_Form
from calibre.gui2 import config, gprefs from calibre.gui2 import config, gprefs, qt_app
from calibre.utils.localization import available_translations, \ from calibre.utils.localization import available_translations, \
get_language, get_lang get_language, get_lang
from calibre.utils.config import prefs from calibre.utils.config import prefs
@ -56,12 +57,64 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
(_('Never'), 'never')] (_('Never'), 'never')]
r('toolbar_text', gprefs, choices=choices) r('toolbar_text', gprefs, choices=choices)
self.current_font = None
self.change_font_button.clicked.connect(self.change_font)
def initialize(self):
ConfigWidgetBase.initialize(self)
self.current_font = gprefs['font']
self.update_font_display()
def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self)
ofont = self.current_font
self.current_font = None
if ofont is not None:
self.changed_signal.emit()
self.update_font_display()
def build_font_obj(self):
font_info = self.current_font
if font_info is not None:
font = QFont(*font_info)
else:
font = qt_app.original_font
return font
def update_font_display(self):
font = self.build_font_obj()
fi = QFontInfo(font)
name = unicode(fi.family())
self.font_display.setFont(font)
self.font_display.setText(_('Current font:') + ' ' + name +
' [%dpt]'%fi.pointSize())
def change_font(self, *args):
fd = QFontDialog(self.build_font_obj(), self)
if fd.exec_() == fd.Accepted:
font = fd.selectedFont()
fi = QFontInfo(font)
self.current_font = (unicode(fi.family()), fi.pointSize(),
fi.weight(), fi.italic())
self.update_font_display()
self.changed_signal.emit()
def commit(self, *args):
rr = ConfigWidgetBase.commit(self, *args)
if self.current_font != gprefs['font']:
gprefs['font'] = self.current_font
QApplication.setFont(self.font_display.font())
rr = True
return rr
def refresh_gui(self, gui): def refresh_gui(self, gui):
gui.search.search_as_you_type(config['search_as_you_type']) gui.search.search_as_you_type(config['search_as_you_type'])
self.update_font_display()
if __name__ == '__main__': if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([]) app = QApplication([])
test_widget('Interface', 'Look & Feel') test_widget('Interface', 'Look & Feel')

View File

@ -183,7 +183,7 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="8" column="0" colspan="2"> <item row="9" column="0" colspan="2">
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -196,6 +196,20 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item row="8" column="0">
<widget class="QLineEdit" name="font_display">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QPushButton" name="change_font_button">
<property name="text">
<string>Change &amp;font (needs restart)</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View File

@ -251,10 +251,28 @@ class Preferences(QMainWindow):
self.close() self.close()
self.run_wizard_requested.emit() self.run_wizard_requested.emit()
def set_tooltips_for_labels(self):
def process_child(child):
for g in child.children():
if isinstance(g, QLabel):
buddy = g.buddy()
if buddy is not None and hasattr(buddy, 'toolTip'):
htext = unicode(buddy.toolTip()).strip()
etext = unicode(g.toolTip()).strip()
if htext and not etext:
g.setToolTip(htext)
g.setWhatsThis(htext)
else:
process_child(g)
process_child(self.showing_widget)
def show_plugin(self, plugin): def show_plugin(self, plugin):
self.showing_widget = plugin.create_widget(self.scroll_area) self.showing_widget = plugin.create_widget(self.scroll_area)
self.showing_widget.genesis(self.gui) self.showing_widget.genesis(self.gui)
self.showing_widget.initialize() self.showing_widget.initialize()
self.set_tooltips_for_labels()
self.scroll_area.setWidget(self.showing_widget) self.scroll_area.setWidget(self.showing_widget)
self.stack.setCurrentIndex(1) self.stack.setCurrentIndex(1)
self.showing_widget.show() self.showing_widget.show()

View File

@ -87,6 +87,13 @@ class TagsView(QTreeView): # {{{
self.setDragDropMode(self.DropOnly) self.setDragDropMode(self.DropOnly)
self.setDropIndicatorShown(True) self.setDropIndicatorShown(True)
self.setAutoExpandDelay(500) self.setAutoExpandDelay(500)
self.pane_is_visible = False
def set_pane_is_visible(self, to_what):
pv = self.pane_is_visible
self.pane_is_visible = to_what
if to_what and not pv:
self.recount()
def set_database(self, db, tag_match, sort_by): def set_database(self, db, tag_match, sort_by):
self.hidden_categories = config['tag_browser_hidden_categories'] self.hidden_categories = config['tag_browser_hidden_categories']
@ -94,6 +101,7 @@ class TagsView(QTreeView): # {{{
hidden_categories=self.hidden_categories, hidden_categories=self.hidden_categories,
search_restriction=None, search_restriction=None,
drag_drop_finished=self.drag_drop_finished) drag_drop_finished=self.drag_drop_finished)
self.pane_is_visible = True # because TagsModel.init did a recount
self.sort_by = sort_by self.sort_by = sort_by
self.tag_match = tag_match self.tag_match = tag_match
self.db = db self.db = db
@ -300,7 +308,7 @@ class TagsView(QTreeView): # {{{
return self.isExpanded(idx) return self.isExpanded(idx)
def recount(self, *args): def recount(self, *args):
if self.disable_recounting: if self.disable_recounting or not self.pane_is_visible:
return return
self.refresh_signal_processed = True self.refresh_signal_processed = True
ci = self.currentIndex() ci = self.currentIndex()
@ -969,6 +977,7 @@ class TagBrowserWidget(QWidget): # {{{
self._layout.setContentsMargins(0,0,0,0) self._layout.setContentsMargins(0,0,0,0)
parent.tags_view = TagsView(parent) parent.tags_view = TagsView(parent)
self.tags_view = parent.tags_view
self._layout.addWidget(parent.tags_view) self._layout.addWidget(parent.tags_view)
parent.sort_by = QComboBox(parent) parent.sort_by = QComboBox(parent)
@ -998,6 +1007,9 @@ class TagBrowserWidget(QWidget): # {{{
_('Add your own categories to the Tag Browser')) _('Add your own categories to the Tag Browser'))
parent.edit_categories.setStatusTip(parent.edit_categories.toolTip()) parent.edit_categories.setStatusTip(parent.edit_categories.toolTip())
def set_pane_is_visible(self, to_what):
self.tags_view.set_pane_is_visible(to_what)
# }}} # }}}

View File

@ -234,7 +234,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
######################### Search Restriction ########################## ######################### Search Restriction ##########################
SearchRestrictionMixin.__init__(self) SearchRestrictionMixin.__init__(self)
self.apply_named_search_restriction(db.prefs['gui_restriction']) if db.prefs['gui_restriction']:
self.apply_named_search_restriction(db.prefs['gui_restriction'])
########################### Cover Flow ################################ ########################### Cover Flow ################################

View File

@ -19,12 +19,15 @@ def generate_test_db(library_path,
max_tags=10 max_tags=10
): ):
import random, string, os, sys, time import random, string, os, sys, time
from calibre.constants import preferred_encoding
if not os.path.exists(library_path): if not os.path.exists(library_path):
os.makedirs(library_path) os.makedirs(library_path)
letters = string.letters.decode(preferred_encoding)
def randstr(length): def randstr(length):
return ''.join(random.choice(string.letters) for i in return ''.join(random.choice(letters) for i in
xrange(length)) xrange(length))
all_tags = [randstr(tag_length) for j in xrange(num_of_tags)] all_tags = [randstr(tag_length) for j in xrange(num_of_tags)]

View File

@ -10,11 +10,10 @@ import os, sys, shutil, cStringIO, glob, time, functools, traceback, re
from itertools import repeat from itertools import repeat
from math import floor from math import floor
from Queue import Queue from Queue import Queue
from operator import itemgetter
from PyQt4.QtGui import QImage from PyQt4.QtGui import QImage
from calibre import prints
from calibre.ebooks.metadata import title_sort, author_to_author_sort from calibre.ebooks.metadata import title_sort, author_to_author_sort
from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.library.database import LibraryDatabase from calibre.library.database import LibraryDatabase
@ -1039,43 +1038,175 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
tn=field['table'], col=field['link_column']), (id_,)) tn=field['table'], col=field['link_column']), (id_,))
return set(x[0] for x in ans) return set(x[0] for x in ans)
########## data structures for get_categories
CATEGORY_SORTS = ('name', 'popularity', 'rating') CATEGORY_SORTS = ('name', 'popularity', 'rating')
def get_categories(self, sort='name', ids=None, icon_map=None): class TCat_Tag(object):
self.books_list_filter.change([] if not ids else ids)
categories = {} def __init__(self, name, sort):
self.n = name
self.s = sort
self.c = 0
self.rt = 0
self.rc = 0
self.id = None
def set_all(self, c, rt, rc, id):
self.c = c
self.rt = rt
self.rc = rc
self.id = id
def __str__(self):
return unicode(self)
def __unicode__(self):
return 'n=%s s=%s c=%d rt=%d rc=%d id=%s'%\
(self.n, self.s, self.c, self.rt, self.rc, self.id)
def get_categories(self, sort='name', ids=None, icon_map=None):
#start = last = time.clock()
if icon_map is not None and type(icon_map) != TagsIcons: if icon_map is not None and type(icon_map) != TagsIcons:
raise TypeError('icon_map passed to get_categories must be of type TagIcons') raise TypeError('icon_map passed to get_categories must be of type TagIcons')
if sort not in self.CATEGORY_SORTS:
raise ValueError('sort ' + sort + ' not a valid value')
self.books_list_filter.change([] if not ids else ids)
id_filter = None if not ids else frozenset(ids)
tb_cats = self.field_metadata tb_cats = self.field_metadata
#### First, build the standard and custom-column categories #### tcategories = {}
tids = {}
md = []
# First, build the maps. We need a category->items map and an
# item -> (item_id, sort_val) map to use in the books loop
for category in tb_cats.keys(): for category in tb_cats.keys():
cat = tb_cats[category] cat = tb_cats[category]
if not cat['is_category'] or cat['kind'] in ['user', 'search']: if not cat['is_category'] or cat['kind'] in ['user', 'search'] \
or category in ['news', 'formats']:
continue continue
tn = cat['table'] # Get the ids for the item values
categories[category] = [] #reserve the position in the ordered list if not cat['is_custom']:
if tn is None: # Nothing to do for the moment funcs = {
'authors' : self.get_authors_with_ids,
'series' : self.get_series_with_ids,
'publisher': self.get_publishers_with_ids,
'tags' : self.get_tags_with_ids,
'rating' : self.get_ratings_with_ids,
}
func = funcs.get(category, None)
if func:
list = func()
else:
raise ValueError(category + ' has no get with ids function')
else:
list = self.get_custom_items_with_ids(label=cat['label'])
tids[category] = {}
if category == 'authors':
for l in list:
(id, val, sort_val) = (l[0], l[1], l[2])
tids[category][val] = (id, sort_val)
else:
for l in list:
(id, val) = (l[0], l[1])
tids[category][val] = (id, val)
# add an empty category to the category map
tcategories[category] = {}
# create a list of category/field_index for the books scan to use.
# This saves iterating through field_metadata for each book
md.append((category, cat['rec_index'], cat['is_multiple']))
#print 'end phase "collection":', time.clock() - last, 'seconds'
#last = time.clock()
# Now scan every book looking for category items.
# Code below is duplicated because it shaves off 10% of the loop time
id_dex = self.FIELD_MAP['id']
rating_dex = self.FIELD_MAP['rating']
tag_class = LibraryDatabase2.TCat_Tag
for book in self.data.iterall():
if id_filter and book[id_dex] not in id_filter:
continue continue
cn = cat['column'] rating = book[rating_dex]
if ids is None: # We kept track of all possible category field_map positions above
query = '''SELECT id, {0}, count, avg_rating, sort for (cat, dex, mult) in md:
FROM tag_browser_{1}'''.format(cn, tn) if book[dex] is None:
else: continue
query = '''SELECT id, {0}, count, avg_rating, sort if not mult:
FROM tag_browser_filtered_{1}'''.format(cn, tn) val = book[dex]
if sort == 'popularity': try:
query += ' ORDER BY count DESC, sort ASC' (item_id, sort_val) = tids[cat][val] # let exceptions fly
elif sort == 'name': item = tcategories[cat].get(val, None)
query += ' ORDER BY sort COLLATE icucollate' if not item:
else: item = tag_class(val, sort_val)
query += ' ORDER BY avg_rating DESC, sort ASC' tcategories[cat][val] = item
data = self.conn.get(query) item.c += 1
item.id = item_id
if rating > 0:
item.rt += rating
item.rc += 1
except:
prints('get_categories: item', val, 'is not in', cat, 'list!')
else:
vals = book[dex].split(mult)
for val in vals:
try:
(item_id, sort_val) = tids[cat][val] # let exceptions fly
item = tcategories[cat].get(val, None)
if not item:
item = tag_class(val, sort_val)
tcategories[cat][val] = item
item.c += 1
item.id = item_id
if rating > 0:
item.rt += rating
item.rc += 1
except:
prints('get_categories: item', val, 'is not in', cat, 'list!')
#print 'end phase "books":', time.clock() - last, 'seconds'
#last = time.clock()
# Now do news
tcategories['news'] = {}
cat = tb_cats['news']
tn = cat['table']
cn = cat['column']
if ids is None:
query = '''SELECT id, {0}, count, avg_rating, sort
FROM tag_browser_{1}'''.format(cn, tn)
else:
query = '''SELECT id, {0}, count, avg_rating, sort
FROM tag_browser_filtered_{1}'''.format(cn, tn)
# results will be sorted later
data = self.conn.get(query)
for r in data:
item = LibraryDatabase2.TCat_Tag(r[1], r[1])
item.set_all(c=r[2], rt=r[2]*r[3], rc=r[2], id=r[0])
tcategories['news'][r[1]] = item
#print 'end phase "news":', time.clock() - last, 'seconds'
#last = time.clock()
# Build the real category list by iterating over the temporary copy
# and building the Tag instances.
categories = {}
tag_class = Tag
for category in tb_cats.keys():
if category not in tcategories:
continue
cat = tb_cats[category]
# prepare the place where we will put the array of Tags
categories[category] = []
# icon_map is not None if get_categories is to store an icon and # icon_map is not None if get_categories is to store an icon and
# possibly a tooltip in the tag structure. # possibly a tooltip in the tag structure.
icon, tooltip = None, '' icon = None
tooltip = ''
label = tb_cats.key_to_label(category) label = tb_cats.key_to_label(category)
if icon_map: if icon_map:
if not tb_cats.is_custom_field(category): if not tb_cats.is_custom_field(category):
@ -1087,23 +1218,46 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
tooltip = self.custom_column_label_map[label]['name'] tooltip = self.custom_column_label_map[label]['name']
datatype = cat['datatype'] datatype = cat['datatype']
avgr = itemgetter(3) avgr = lambda x: 0.0 if x.rc == 0 else x.rt/x.rc
item_not_zero_func = lambda x: x[2] > 0 # Duplicate the build of items below to avoid using a lambda func
# in the main Tag loop. Saves a few %
if datatype == 'rating': if datatype == 'rating':
# eliminate the zero ratings line as well as count == 0
item_not_zero_func = (lambda x: x[1] > 0 and x[2] > 0)
formatter = (lambda x:u'\u2605'*int(x/2)) formatter = (lambda x:u'\u2605'*int(x/2))
avgr = itemgetter(1) avgr = lambda x : x.n
# eliminate the zero ratings line as well as count == 0
items = [v for v in tcategories[category].values() if v.c > 0 and v.n != 0]
elif category == 'authors': elif category == 'authors':
# Clean up the authors strings to human-readable form # Clean up the authors strings to human-readable form
formatter = (lambda x: x.replace('|', ',')) formatter = (lambda x: x.replace('|', ','))
items = [v for v in tcategories[category].values() if v.c > 0]
else: else:
formatter = (lambda x:unicode(x)) formatter = (lambda x:unicode(x))
items = [v for v in tcategories[category].values() if v.c > 0]
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0], # sort the list
avg=avgr(r), sort=r[4], icon=icon, if sort == 'name':
def get_sort_key(x):
sk = x.s
if isinstance(sk, unicode):
sk = sort_key(sk)
return sk
kf = get_sort_key
reverse=False
elif sort == 'popularity':
kf = lambda x: x.c
reverse=True
else:
kf = avgr
reverse=True
items.sort(key=kf, reverse=reverse)
categories[category] = [tag_class(formatter(r.n), count=r.c, id=r.id,
avg=avgr(r), sort=r.s, icon=icon,
tooltip=tooltip, category=category) tooltip=tooltip, category=category)
for r in data if item_not_zero_func(r)] for r in items]
#print 'end phase "tags list":', time.clock() - last, 'seconds'
#last = time.clock()
# Needed for legacy databases that have multiple ratings that # Needed for legacy databases that have multiple ratings that
# map to n stars # map to n stars
@ -1189,8 +1343,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
icon_map['search'] = icon_map['search'] icon_map['search'] = icon_map['search']
categories['search'] = items categories['search'] = items
#print 'last phase ran in:', time.clock() - last, 'seconds'
#print 'get_categories ran in:', time.clock() - start, 'seconds'
return categories return categories
############# End get_categories
def tags_older_than(self, tag, delta): def tags_older_than(self, tag, delta):
tag = tag.lower().strip() tag = tag.lower().strip()
now = nowf() now = nowf()
@ -1486,6 +1645,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# Note: we generally do not need to refresh_ids because library_view will # Note: we generally do not need to refresh_ids because library_view will
# refresh everything. # refresh everything.
def get_ratings_with_ids(self):
result = self.conn.get('SELECT id,rating FROM ratings')
if not result:
return []
return result
def dirty_books_referencing(self, field, id, commit=True): def dirty_books_referencing(self, field, id, commit=True):
# Get the list of books to dirty -- all books that reference the item # Get the list of books to dirty -- all books that reference the item
table = self.field_metadata[field]['table'] table = self.field_metadata[field]['table']

View File

@ -119,10 +119,8 @@ class SafeFormat(TemplateFormatter):
try: try:
b = self.book.get_user_metadata(key, False) b = self.book.get_user_metadata(key, False)
except: except:
if DEBUG: traceback.print_exc()
traceback.print_exc()
b = None b = None
if b is not None and b['datatype'] == 'composite': if b is not None and b['datatype'] == 'composite':
if key in self.composite_values: if key in self.composite_values:
return self.composite_values[key] return self.composite_values[key]
@ -135,8 +133,7 @@ class SafeFormat(TemplateFormatter):
return val.replace('/', '_').replace('\\', '_') return val.replace('/', '_').replace('\\', '_')
return '' return ''
except: except:
if DEBUG: traceback.print_exc()
traceback.print_exc()
return key return key
def get_components(template, mi, id, timefmt='%b %Y', length=250, def get_components(template, mi, id, timefmt='%b %Y', length=250,
@ -155,6 +152,8 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
format_args['tags'] = mi.format_tags() format_args['tags'] = mi.format_tags()
if format_args['tags'].startswith('/'): if format_args['tags'].startswith('/'):
format_args['tags'] = format_args['tags'][1:] format_args['tags'] = format_args['tags'][1:]
else:
format_args['tags'] = ''
if mi.series: if mi.series:
format_args['series'] = tsfmt(mi.series) format_args['series'] = tsfmt(mi.series)
if mi.series_index is not None: if mi.series_index is not None:
@ -254,6 +253,7 @@ def do_save_book_to_disk(id_, mi, cover, plugboards,
if not os.path.exists(dirpath): if not os.path.exists(dirpath):
raise raise
ocover = mi.cover
if opts.save_cover and cover and os.access(cover, os.R_OK): if opts.save_cover and cover and os.access(cover, os.R_OK):
with open(base_path+'.jpg', 'wb') as f: with open(base_path+'.jpg', 'wb') as f:
with open(cover, 'rb') as s: with open(cover, 'rb') as s:
@ -267,6 +267,8 @@ def do_save_book_to_disk(id_, mi, cover, plugboards,
with open(base_path+'.opf', 'wb') as f: with open(base_path+'.opf', 'wb') as f:
f.write(opf) f.write(opf)
mi.cover = ocover
written = False written = False
for fmt in formats: for fmt in formats:
global plugboard_save_to_disk_value, plugboard_any_format_value global plugboard_save_to_disk_value, plugboard_any_format_value

55
src/calibre/utils/mem.py Normal file
View File

@ -0,0 +1,55 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
## {{{ http://code.activestate.com/recipes/286222/ (r1)
import os
_proc_status = '/proc/%d/status' % os.getpid()
_scale = {'kB': 1024.0, 'mB': 1024.0*1024.0,
'KB': 1024.0, 'MB': 1024.0*1024.0}
def _VmB(VmKey):
'''Private.
'''
global _proc_status, _scale
# get pseudo file /proc/<pid>/status
try:
t = open(_proc_status)
v = t.read()
t.close()
except:
return 0.0 # non-Linux?
# get VmKey line e.g. 'VmRSS: 9999 kB\n ...'
i = v.index(VmKey)
v = v[i:].split(None, 3) # whitespace
if len(v) < 3:
return 0.0 # invalid format?
# convert Vm value to bytes
return float(v[1]) * _scale[v[2]]
def memory(since=0.0):
'''Return memory usage in bytes.
'''
return _VmB('VmSize:') - since
def resident(since=0.0):
'''Return resident memory usage in bytes.
'''
return _VmB('VmRSS:') - since
def stacksize(since=0.0):
'''Return stack size in bytes.
'''
return _VmB('VmStk:') - since
## end of http://code.activestate.com/recipes/286222/ }}}