mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
KG/chaley updates
This commit is contained in:
commit
16fc28a1ed
23
resources/recipes/ajiajin.recipe
Normal file
23
resources/recipes/ajiajin.recipe
Normal 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')]
|
||||
|
||||
|
37
resources/recipes/chouchoublog.recipe
Normal file
37
resources/recipes/chouchoublog.recipe
Normal 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
|
||||
|
@ -3,15 +3,16 @@ __copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
http://www.dilbert.com
|
||||
'''
|
||||
import re
|
||||
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
import re
|
||||
|
||||
class DosisDiarias(BasicNewsRecipe):
|
||||
class DilbertBig(BasicNewsRecipe):
|
||||
title = 'Dilbert'
|
||||
__author__ = 'Darko Miletic'
|
||||
__author__ = 'Darko Miletic and Starson17'
|
||||
description = 'Dilbert'
|
||||
oldest_article = 5
|
||||
reverse_article_order = True
|
||||
oldest_article = 15
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
use_embedded_content = True
|
||||
@ -29,20 +30,23 @@ class DosisDiarias(BasicNewsRecipe):
|
||||
|
||||
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):
|
||||
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):
|
||||
for tag in soup.findAll(name='a'):
|
||||
if tag['href'].find('http://feedads') >= 0:
|
||||
tag.extract()
|
||||
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;}
|
||||
'''
|
||||
|
31
resources/recipes/kahokushinpo.recipe
Normal file
31
resources/recipes/kahokushinpo.recipe
Normal 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"}]
|
||||
|
38
resources/recipes/nationalgeographic.recipe
Normal file
38
resources/recipes/nationalgeographic.recipe
Normal 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
|
20
resources/recipes/nationalgeographicjp.recipe
Normal file
20
resources/recipes/nationalgeographicjp.recipe
Normal 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)
|
||||
|
@ -10,8 +10,8 @@ import mechanize
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
|
||||
|
||||
class NikkeiNet_sub_life(BasicNewsRecipe):
|
||||
title = u'\u65e5\u7d4c\u65b0\u805e\u96fb\u5b50\u7248(\u751f\u6d3b)'
|
||||
class NikkeiNet_sub_shakai(BasicNewsRecipe):
|
||||
title = u'\u65e5\u7d4c\u65b0\u805e\u96fb\u5b50\u7248(Social)'
|
||||
__author__ = 'Hiroshi Miura'
|
||||
description = 'News and current market affairs from Japan'
|
||||
cover_url = 'http://parts.nikkei.com/parts/ds/images/common/logo_r1.svg'
|
||||
|
58
resources/recipes/paperli_topic.recipe
Normal file
58
resources/recipes/paperli_topic.recipe
Normal 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
|
||||
|
42
resources/recipes/science_based_medicine.recipe
Normal file
42
resources/recipes/science_based_medicine.recipe
Normal 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)
|
||||
|
36
resources/recipes/uninohimitu.recipe
Normal file
36
resources/recipes/uninohimitu.recipe
Normal 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
|
||||
|
@ -60,8 +60,8 @@ class ZeitDe(BasicNewsRecipe):
|
||||
for tag in soup.findAll(name=['ul','li']):
|
||||
tag.name = 'div'
|
||||
|
||||
soup.html['xml:lang'] = self.lang
|
||||
soup.html['lang'] = self.lang
|
||||
soup.html['xml:lang'] = self.language.replace('_', '-')
|
||||
soup.html['lang'] = self.language.replace('_', '-')
|
||||
mtag = '<meta http-equiv="Content-Type" content="text/html; charset=' + self.encoding + '">'
|
||||
soup.head.insert(0,mtag)
|
||||
return soup
|
||||
|
@ -36,6 +36,11 @@ class UserFeedback(DeviceError):
|
||||
self.details = details
|
||||
self.msg = msg
|
||||
|
||||
class OpenFeedback(DeviceError):
|
||||
def __init__(self, msg):
|
||||
self.feedback_msg = msg
|
||||
DeviceError.__init__(self, msg)
|
||||
|
||||
class DeviceBusy(ProtocolError):
|
||||
""" Raised when device is busy """
|
||||
def __init__(self, uerr=""):
|
||||
|
@ -216,6 +216,9 @@ class DevicePlugin(Plugin):
|
||||
an implementation of
|
||||
this function that should serve as a good example for USB Mass storage
|
||||
devices.
|
||||
|
||||
This method can raise an OpenFeedback exception to display a message to
|
||||
the user.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
|
@ -120,7 +120,7 @@ def add_pipeline_options(parser, plumber):
|
||||
[
|
||||
'base_font_size', 'disable_font_rescaling',
|
||||
'font_size_mapping',
|
||||
'line_height',
|
||||
'line_height', 'minimum_line_height',
|
||||
'linearize_tables',
|
||||
'extra_css', 'smarten_punctuation',
|
||||
'margin_top', 'margin_left', 'margin_right',
|
||||
|
@ -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',
|
||||
recommended_value=0, level=OptionRecommendation.LOW,
|
||||
help=_('The line height in pts. Controls spacing between consecutive '
|
||||
'lines of text. By default no line height manipulation is '
|
||||
'performed.'
|
||||
)
|
||||
help=_(
|
||||
'The line height in pts. Controls spacing between consecutive '
|
||||
'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',
|
||||
|
@ -73,6 +73,10 @@ class FB2MLizer(object):
|
||||
text = re.sub(r'(?miu)<p>\s*</p>', '', text)
|
||||
text = re.sub(r'(?miu)\s+</p>', '</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
|
||||
|
||||
def fb2_header(self):
|
||||
@ -293,6 +297,18 @@ class FB2MLizer(object):
|
||||
s_out, s_tags = self.handle_simple_tag('emphasis', tag_stack+tags)
|
||||
fb2_out += s_out
|
||||
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.
|
||||
if style['font-style'] == 'italic':
|
||||
@ -303,6 +319,10 @@ class FB2MLizer(object):
|
||||
s_out, s_tags = self.handle_simple_tag('strong', tag_stack+tags)
|
||||
fb2_out += s_out
|
||||
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.
|
||||
if hasattr(elem_tree, 'text') and elem_tree.text:
|
||||
|
@ -314,6 +314,8 @@ class HTMLInput(InputFormatPlugin):
|
||||
rewrite_links, urlnormalize, urldefrag, BINARY_MIME, OEB_STYLES, \
|
||||
xpath
|
||||
from calibre import guess_type
|
||||
from calibre.ebooks.oeb.transforms.metadata import \
|
||||
meta_info_to_oeb_metadata
|
||||
import cssutils
|
||||
self.OEB_STYLES = OEB_STYLES
|
||||
oeb = create_oebbook(log, None, opts, self,
|
||||
@ -321,15 +323,7 @@ class HTMLInput(InputFormatPlugin):
|
||||
self.oeb = oeb
|
||||
|
||||
metadata = oeb.metadata
|
||||
if mi.title:
|
||||
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'})
|
||||
meta_info_to_oeb_metadata(mi, metadata, log)
|
||||
if not metadata.language:
|
||||
oeb.logger.warn(u'Language not specified')
|
||||
metadata.add('language', get_lang().replace('_', '-'))
|
||||
|
@ -170,7 +170,27 @@ def get_metadata_(src, encoding=None):
|
||||
if match:
|
||||
series = match.group(1)
|
||||
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)
|
||||
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 = None
|
||||
|
@ -184,7 +184,7 @@ class MobiMLizer(object):
|
||||
para.attrib['value'] = str(istates[-2].list_num)
|
||||
elif tag in NESTABLE_TAGS and istate.rendered:
|
||||
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
|
||||
para = wrapper = etree.SubElement(parent, XHTML('blockquote'))
|
||||
para = wrapper
|
||||
|
@ -39,6 +39,12 @@ class MOBIOutput(OutputFormatPlugin):
|
||||
OptionRecommendation(name='personal_doc', recommended_value='[PDOC]',
|
||||
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):
|
||||
|
@ -633,12 +633,12 @@ class Style(object):
|
||||
parent = self._getparent()
|
||||
if 'line-height' in self._style:
|
||||
lineh = self._style['line-height']
|
||||
if lineh == 'normal':
|
||||
lineh = '1.2'
|
||||
try:
|
||||
float(lineh)
|
||||
result = float(lineh) * self.fontSize
|
||||
except ValueError:
|
||||
result = self._unit_convert(lineh, base=self.fontSize)
|
||||
else:
|
||||
result = float(lineh) * self.fontSize
|
||||
elif parent is not None:
|
||||
# TODO: proper inheritance
|
||||
result = parent.lineHeight
|
||||
|
@ -245,6 +245,8 @@ class CSSFlattener(object):
|
||||
del node.attrib['bgcolor']
|
||||
if cssdict.get('font-weight', '').lower() == 'medium':
|
||||
cssdict['font-weight'] = 'normal' # ADE chokes on font-weight medium
|
||||
|
||||
fsize = font_size
|
||||
if not self.context.disable_font_rescaling:
|
||||
_sbase = self.sbase if self.sbase is not None else \
|
||||
self.context.source.fbase
|
||||
@ -258,6 +260,14 @@ class CSSFlattener(object):
|
||||
fsize = self.fmap[font_size]
|
||||
cssdict['font-size'] = "%0.5fem" % (fsize / psize)
|
||||
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 self.lineh and self.fbase and tag != 'body':
|
||||
self.clean_edges(cssdict, style, psize)
|
||||
@ -290,6 +300,7 @@ class CSSFlattener(object):
|
||||
lineh = self.lineh / psize
|
||||
cssdict['line-height'] = "%0.5fem" % lineh
|
||||
|
||||
|
||||
if (self.context.remove_paragraph_spacing or
|
||||
self.context.insert_blank_line) and tag in ('p', 'div'):
|
||||
if item_id != 'calibre_jacket' or self.context.output_profile.name == 'Kindle':
|
||||
|
@ -77,10 +77,14 @@ class TXTInput(InputFormatPlugin):
|
||||
base = os.getcwdu()
|
||||
if hasattr(stream, 'name'):
|
||||
base = os.path.dirname(stream.name)
|
||||
htmlfile = open(os.path.join(base, 'temp_calibre_txt_input_to_html.html'),
|
||||
'wb')
|
||||
htmlfile.write(html.encode('utf-8'))
|
||||
htmlfile.close()
|
||||
fname = os.path.join(base, 'index.html')
|
||||
c = 0
|
||||
while os.path.exists(fname):
|
||||
c += 1
|
||||
fname = 'index%d.html'%c
|
||||
htmlfile = open(fname, 'wb')
|
||||
with htmlfile:
|
||||
htmlfile.write(html.encode('utf-8'))
|
||||
cwd = os.getcwdu()
|
||||
odi = options.debug_pipeline
|
||||
options.debug_pipeline = None
|
||||
|
@ -9,7 +9,7 @@ from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \
|
||||
QByteArray, QTranslator, QCoreApplication, QThread, \
|
||||
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
|
||||
QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
|
||||
QIcon, QApplication, QDialog, QPushButton, QUrl
|
||||
QIcon, QApplication, QDialog, QPushButton, QUrl, QFont
|
||||
|
||||
ORG_NAME = 'KovidsBrain'
|
||||
APP_UID = 'libprs500'
|
||||
@ -52,6 +52,7 @@ gprefs.defaults['show_splash_screen'] = True
|
||||
gprefs.defaults['toolbar_icon_size'] = 'medium'
|
||||
gprefs.defaults['toolbar_text'] = 'auto'
|
||||
gprefs.defaults['show_child_bar'] = False
|
||||
gprefs.defaults['font'] = None
|
||||
|
||||
# }}}
|
||||
|
||||
@ -613,6 +614,10 @@ class Application(QApplication):
|
||||
qt_app = self
|
||||
self._file_open_paths = []
|
||||
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):
|
||||
with self._file_open_lock:
|
||||
|
@ -154,15 +154,17 @@ class EditMetadataAction(InterfaceAction):
|
||||
d.view_format.connect(lambda
|
||||
fmt:self.gui.iactions['View'].view_format(row_list[current_row],
|
||||
fmt))
|
||||
if d.exec_() != d.Accepted:
|
||||
d.view_format.disconnect()
|
||||
ret = d.exec_()
|
||||
d.break_cycles()
|
||||
if ret != d.Accepted:
|
||||
break
|
||||
d.view_format.disconnect()
|
||||
|
||||
changed.add(d.id)
|
||||
if d.row_delta == 0:
|
||||
break
|
||||
current_row += d.row_delta
|
||||
|
||||
|
||||
if changed:
|
||||
self.gui.library_view.model().refresh_ids(list(changed))
|
||||
current = self.gui.library_view.currentIndex()
|
||||
|
@ -10,7 +10,7 @@ import textwrap
|
||||
from functools import partial
|
||||
|
||||
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.ebooks.conversion.config import load_defaults, \
|
||||
@ -81,6 +81,21 @@ class Widget(QWidget):
|
||||
self.apply_recommendations(defaults)
|
||||
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):
|
||||
defaults = GuiRecommendations()
|
||||
defaults.merge_recommendations(get_option, OptionRecommendation.LOW,
|
||||
|
@ -21,7 +21,7 @@ class LookAndFeelWidget(Widget, Ui_Form):
|
||||
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
|
||||
Widget.__init__(self, parent,
|
||||
['change_justification', 'extra_css', 'base_font_size',
|
||||
'font_size_mapping', 'line_height',
|
||||
'font_size_mapping', 'line_height', 'minimum_line_height',
|
||||
'linearize_tables', 'smarten_punctuation',
|
||||
'disable_font_rescaling', 'insert_blank_line',
|
||||
'remove_paragraph_spacing', 'remove_paragraph_spacing_indent_size','input_encoding',
|
||||
|
@ -97,7 +97,7 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Line &height:</string>
|
||||
@ -107,7 +107,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1" colspan="2">
|
||||
<item row="4" column="1" colspan="2">
|
||||
<widget class="QDoubleSpinBox" name="opt_line_height">
|
||||
<property name="suffix">
|
||||
<string> pt</string>
|
||||
@ -117,7 +117,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Input character &encoding:</string>
|
||||
@ -127,17 +127,17 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1" colspan="3">
|
||||
<item row="5" column="1" colspan="3">
|
||||
<widget class="QLineEdit" name="opt_input_encoding"/>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<item row="6" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="opt_remove_paragraph_spacing">
|
||||
<property name="text">
|
||||
<string>Remove &spacing between paragraphs</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="2" colspan="2">
|
||||
<item row="6" column="2" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
@ -164,21 +164,21 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Text justification:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<item row="8" column="0">
|
||||
<widget class="QCheckBox" name="opt_linearize_tables">
|
||||
<property name="text">
|
||||
<string>&Linearize tables</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="0" colspan="4">
|
||||
<item row="11" column="0" colspan="4">
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Extra &CSS</string>
|
||||
@ -190,37 +190,60 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="2" colspan="2">
|
||||
<item row="7" column="2" colspan="2">
|
||||
<widget class="QComboBox" name="opt_change_justification"/>
|
||||
</item>
|
||||
<item row="7" column="1" colspan="3">
|
||||
<item row="8" column="1" colspan="3">
|
||||
<widget class="QCheckBox" name="opt_asciiize">
|
||||
<property name="text">
|
||||
<string>&Transliterate unicode characters to ASCII</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<item row="9" column="0">
|
||||
<widget class="QCheckBox" name="opt_insert_blank_line">
|
||||
<property name="text">
|
||||
<string>Insert &blank line</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1" colspan="2">
|
||||
<item row="9" column="1" colspan="2">
|
||||
<widget class="QCheckBox" name="opt_keep_ligatures">
|
||||
<property name="text">
|
||||
<string>Keep &ligatures</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<item row="10" column="0">
|
||||
<widget class="QCheckBox" name="opt_smarten_punctuation">
|
||||
<property name="text">
|
||||
<string>Smarten &punctuation</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Minimum &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>
|
||||
</widget>
|
||||
<resources>
|
||||
|
@ -25,6 +25,7 @@ class PluginWidget(Widget, Ui_Form):
|
||||
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
|
||||
Widget.__init__(self, parent,
|
||||
['prefer_author_sort', 'rescale_images', 'toc_title',
|
||||
'mobi_ignore_margins',
|
||||
'dont_compress', 'no_inline_toc', 'masthead_font','personal_doc']
|
||||
)
|
||||
self.db, self.book_id = db, book_id
|
||||
|
@ -55,7 +55,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<item row="6" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Kindle options</string>
|
||||
@ -101,7 +101,7 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<item row="7" column="0">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@ -114,6 +114,13 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QCheckBox" name="opt_mobi_ignore_margins">
|
||||
<property name="text">
|
||||
<string>Ignore &margins</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
|
@ -12,7 +12,7 @@ from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, \
|
||||
from calibre.customize.ui import available_input_formats, available_output_formats, \
|
||||
device_plugins
|
||||
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.utils.ipc.job import BaseJob
|
||||
from calibre.devices.scanner import DeviceScanner
|
||||
@ -122,7 +122,8 @@ def device_name_for_plugboards(device_class):
|
||||
|
||||
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
|
||||
'''
|
||||
@ -143,6 +144,7 @@ class DeviceManager(Thread): # {{{
|
||||
self.ejected_devices = set([])
|
||||
self.mount_connection_requests = Queue.Queue(0)
|
||||
self.open_feedback_slot = open_feedback_slot
|
||||
self.open_feedback_msg = open_feedback_msg
|
||||
|
||||
def report_progress(self, *args):
|
||||
pass
|
||||
@ -163,6 +165,9 @@ class DeviceManager(Thread): # {{{
|
||||
dev.reset(detected_device=detected_device,
|
||||
report_progress=self.report_progress)
|
||||
dev.open()
|
||||
except OpenFeedback, e:
|
||||
self.open_feedback_msg(dev.get_gui_name(), e.feedback_msg)
|
||||
continue
|
||||
except:
|
||||
tb = traceback.format_exc()
|
||||
if DEBUG or tb not in self.reported_errors:
|
||||
@ -594,11 +599,16 @@ class DeviceMixin(object): # {{{
|
||||
_('Error communicating with device'), ' ')
|
||||
self.device_error_dialog.setModal(Qt.NonModal)
|
||||
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()
|
||||
if 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):
|
||||
autos = u'\n'.join(map(unicode, map(force_unicode, autos)))
|
||||
return self.ask_a_yes_no_question(
|
||||
|
@ -102,7 +102,7 @@ class MyBlockingBusy(QDialog):
|
||||
remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, \
|
||||
do_autonumber, do_remove_format, remove_format, do_swap_ta, \
|
||||
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
|
||||
@ -129,6 +129,23 @@ class MyBlockingBusy(QDialog):
|
||||
self.db.set_title(id, titlecase(title), notify=False)
|
||||
if au:
|
||||
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:
|
||||
# All of these just affect the DB, so we can tolerate a total rollback
|
||||
if do_auto_author:
|
||||
@ -678,11 +695,16 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
do_remove_conv = self.remove_conversion_settings.isChecked()
|
||||
do_auto_author = self.auto_author_sort.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,
|
||||
do_autonumber, do_remove_format, remove_format, do_swap_ta,
|
||||
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}%%.')
|
||||
%len(self.ids), args, self.db, self.ids,
|
||||
|
@ -381,7 +381,7 @@ Future conversion of these books will use the default settings.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="15" column="0" colspan="3">
|
||||
<item row="14" column="0" colspan="3">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@ -394,6 +394,39 @@ Future conversion of these books will use the default settings.</string>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="13" column="0" colspan="3">
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Change &cover</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="cover_no_change">
|
||||
<property name="text">
|
||||
<string>&No change</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="cover_remove">
|
||||
<property name="text">
|
||||
<string>&Remove cover</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="cover_generate">
|
||||
<property name="text">
|
||||
<string>&Generate default cover</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab">
|
||||
|
@ -240,37 +240,39 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.cover_fetcher = CoverFetcher(None, None, isbn,
|
||||
self.timeout, title, author)
|
||||
self.cover_fetcher.start()
|
||||
self._hangcheck = QTimer(self)
|
||||
self._hangcheck.timeout.connect(self.hangcheck,
|
||||
type=Qt.QueuedConnection)
|
||||
self.cf_start_time = time.time()
|
||||
self.pi.start(_('Downloading cover...'))
|
||||
self._hangcheck.start(100)
|
||||
QTimer.singleShot(100, self.hangcheck)
|
||||
|
||||
def hangcheck(self):
|
||||
if self.cover_fetcher.is_alive() and \
|
||||
time.time()-self.cf_start_time < self.COVER_FETCH_TIMEOUT:
|
||||
cf = self.cover_fetcher
|
||||
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
|
||||
|
||||
self._hangcheck.stop()
|
||||
try:
|
||||
if self.cover_fetcher.is_alive():
|
||||
if cf.is_alive():
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('<b>Could not fetch cover.</b><br/>')+
|
||||
_('The download timed out.')).exec_()
|
||||
return
|
||||
if self.cover_fetcher.needs_isbn:
|
||||
if cf.needs_isbn:
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('Could not find cover for this book. Try '
|
||||
'specifying the ISBN first.')).exec_()
|
||||
return
|
||||
if self.cover_fetcher.exception is not None:
|
||||
err = self.cover_fetcher.exception
|
||||
if cf.exception is not None:
|
||||
err = cf.exception
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('<b>Could not fetch cover.</b><br/>')+unicode(err)).exec_()
|
||||
return
|
||||
if self.cover_fetcher.errors and self.cover_fetcher.cover_data is None:
|
||||
details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in self.cover_fetcher.errors])
|
||||
if cf.errors and cf.cover_data is None:
|
||||
details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in cf.errors])
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('<b>Could not fetch cover.</b><br/>') +
|
||||
_('For the error message from each cover source, '
|
||||
@ -278,7 +280,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
return
|
||||
|
||||
pix = QPixmap()
|
||||
pix.loadFromData(self.cover_fetcher.cover_data)
|
||||
pix.loadFromData(cf.cover_data)
|
||||
if pix.isNull():
|
||||
error_dialog(self, _('Bad cover'),
|
||||
_('The cover is not a valid picture')).exec_()
|
||||
@ -287,11 +289,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.update_cover_tooltip()
|
||||
self.cover_changed = True
|
||||
self.cpixmap = pix
|
||||
self.cover_data = self.cover_fetcher.cover_data
|
||||
self.cover_data = cf.cover_data
|
||||
finally:
|
||||
self.fetch_cover_button.setEnabled(True)
|
||||
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,
|
||||
next_=None):
|
||||
ResizableDialog.__init__(self, window)
|
||||
self.cover_fetcher = None
|
||||
self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter)
|
||||
self.cancel_all = False
|
||||
base = unicode(self.author_sort.toolTip())
|
||||
self.ok_aus_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+
|
||||
_(' 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)
|
||||
self.series.lineEdit().editingFinished.connect(self.increment_series_index)
|
||||
|
||||
self.show()
|
||||
pm = QPixmap()
|
||||
if cover:
|
||||
pm.loadFromData(cover)
|
||||
@ -590,6 +592,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.original_author = unicode(self.authors.text()).strip()
|
||||
self.original_title = unicode(self.title.text()).strip()
|
||||
|
||||
self.show()
|
||||
|
||||
def create_custom_column_editors(self):
|
||||
w = self.central_widget.widget(1)
|
||||
layout = w.layout()
|
||||
@ -828,10 +832,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.accept()
|
||||
|
||||
def accept(self):
|
||||
cf = getattr(self, 'cover_fetcher', None)
|
||||
if cf is not None and hasattr(cf, 'terminate'):
|
||||
cf.terminate()
|
||||
cf.wait()
|
||||
try:
|
||||
if self.formats_changed:
|
||||
self.sync_formats()
|
||||
@ -888,14 +888,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
show=True)
|
||||
raise
|
||||
self.save_state()
|
||||
self.cover_fetcher = None
|
||||
QDialog.accept(self)
|
||||
|
||||
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.cover_fetcher = None
|
||||
QDialog.reject(self, *args)
|
||||
|
||||
def read_state(self):
|
||||
@ -910,3 +908,48 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
dynamic.set('metasingle_window_geometry', bytes(self.saveGeometry()))
|
||||
dynamic.set('metasingle_splitter_state',
|
||||
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'
|
||||
|
||||
|
||||
|
@ -145,7 +145,7 @@ class TagCategories(QDialog, Ui_TagCategories):
|
||||
index = self.all_items[node.data(Qt.UserRole).toPyObject()].index
|
||||
if index not in self.applied_items:
|
||||
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)
|
||||
|
||||
def unapply_tags(self, node=None):
|
||||
|
@ -105,9 +105,13 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
if not question_dialog(self, _('Are your sure?'),
|
||||
'<p>'+_('Are you certain you want to delete the following items?')+'<br>'+ct):
|
||||
return
|
||||
|
||||
row = self.available_tags.row(deletes[0])
|
||||
for item in deletes:
|
||||
(id,ign) = item.data(Qt.UserRole).toInt()
|
||||
self.to_delete.append(id)
|
||||
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))
|
||||
|
@ -123,6 +123,8 @@ class Stack(QStackedWidget): # {{{
|
||||
_('Tag Browser'), I('tags.png'),
|
||||
parent=parent, side_index=0, initial_side_size=200,
|
||||
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(parent.cb_splitter)
|
||||
parent.tb_splitter.setCollapsible(parent.tb_splitter.other_index, False)
|
||||
|
@ -5,10 +5,11 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt4.Qt import QApplication, QFont, QFontInfo, QFontDialog
|
||||
|
||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
||||
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, \
|
||||
get_language, get_lang
|
||||
from calibre.utils.config import prefs
|
||||
@ -56,12 +57,64 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
(_('Never'), 'never')]
|
||||
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):
|
||||
gui.search.search_as_you_type(config['search_as_you_type'])
|
||||
|
||||
self.update_font_display()
|
||||
|
||||
if __name__ == '__main__':
|
||||
from PyQt4.Qt import QApplication
|
||||
app = QApplication([])
|
||||
test_widget('Interface', 'Look & Feel')
|
||||
|
||||
|
@ -183,7 +183,7 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0" colspan="2">
|
||||
<item row="9" column="0" colspan="2">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@ -196,6 +196,20 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</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 &font (needs restart)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
|
@ -251,10 +251,28 @@ class Preferences(QMainWindow):
|
||||
self.close()
|
||||
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):
|
||||
self.showing_widget = plugin.create_widget(self.scroll_area)
|
||||
self.showing_widget.genesis(self.gui)
|
||||
self.showing_widget.initialize()
|
||||
self.set_tooltips_for_labels()
|
||||
self.scroll_area.setWidget(self.showing_widget)
|
||||
self.stack.setCurrentIndex(1)
|
||||
self.showing_widget.show()
|
||||
|
@ -87,6 +87,13 @@ class TagsView(QTreeView): # {{{
|
||||
self.setDragDropMode(self.DropOnly)
|
||||
self.setDropIndicatorShown(True)
|
||||
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):
|
||||
self.hidden_categories = config['tag_browser_hidden_categories']
|
||||
@ -94,6 +101,7 @@ class TagsView(QTreeView): # {{{
|
||||
hidden_categories=self.hidden_categories,
|
||||
search_restriction=None,
|
||||
drag_drop_finished=self.drag_drop_finished)
|
||||
self.pane_is_visible = True # because TagsModel.init did a recount
|
||||
self.sort_by = sort_by
|
||||
self.tag_match = tag_match
|
||||
self.db = db
|
||||
@ -300,7 +308,7 @@ class TagsView(QTreeView): # {{{
|
||||
return self.isExpanded(idx)
|
||||
|
||||
def recount(self, *args):
|
||||
if self.disable_recounting:
|
||||
if self.disable_recounting or not self.pane_is_visible:
|
||||
return
|
||||
self.refresh_signal_processed = True
|
||||
ci = self.currentIndex()
|
||||
@ -969,6 +977,7 @@ class TagBrowserWidget(QWidget): # {{{
|
||||
self._layout.setContentsMargins(0,0,0,0)
|
||||
|
||||
parent.tags_view = TagsView(parent)
|
||||
self.tags_view = parent.tags_view
|
||||
self._layout.addWidget(parent.tags_view)
|
||||
|
||||
parent.sort_by = QComboBox(parent)
|
||||
@ -998,6 +1007,9 @@ class TagBrowserWidget(QWidget): # {{{
|
||||
_('Add your own categories to the Tag Browser'))
|
||||
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)
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -234,7 +234,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
|
||||
######################### Search Restriction ##########################
|
||||
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 ################################
|
||||
|
||||
|
@ -19,12 +19,15 @@ def generate_test_db(library_path,
|
||||
max_tags=10
|
||||
):
|
||||
import random, string, os, sys, time
|
||||
from calibre.constants import preferred_encoding
|
||||
|
||||
if not os.path.exists(library_path):
|
||||
os.makedirs(library_path)
|
||||
|
||||
letters = string.letters.decode(preferred_encoding)
|
||||
|
||||
def randstr(length):
|
||||
return ''.join(random.choice(string.letters) for i in
|
||||
return ''.join(random.choice(letters) for i in
|
||||
xrange(length))
|
||||
|
||||
all_tags = [randstr(tag_length) for j in xrange(num_of_tags)]
|
||||
|
@ -10,11 +10,10 @@ import os, sys, shutil, cStringIO, glob, time, functools, traceback, re
|
||||
from itertools import repeat
|
||||
from math import floor
|
||||
from Queue import Queue
|
||||
from operator import itemgetter
|
||||
|
||||
from PyQt4.QtGui import QImage
|
||||
|
||||
|
||||
from calibre import prints
|
||||
from calibre.ebooks.metadata import title_sort, author_to_author_sort
|
||||
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
||||
from calibre.library.database import LibraryDatabase
|
||||
@ -1039,43 +1038,175 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
tn=field['table'], col=field['link_column']), (id_,))
|
||||
return set(x[0] for x in ans)
|
||||
|
||||
########## data structures for get_categories
|
||||
|
||||
CATEGORY_SORTS = ('name', 'popularity', 'rating')
|
||||
|
||||
def get_categories(self, sort='name', ids=None, icon_map=None):
|
||||
self.books_list_filter.change([] if not ids else ids)
|
||||
class TCat_Tag(object):
|
||||
|
||||
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:
|
||||
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
|
||||
#### 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():
|
||||
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
|
||||
tn = cat['table']
|
||||
categories[category] = [] #reserve the position in the ordered list
|
||||
if tn is None: # Nothing to do for the moment
|
||||
# Get the ids for the item values
|
||||
if not cat['is_custom']:
|
||||
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
|
||||
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)
|
||||
if sort == 'popularity':
|
||||
query += ' ORDER BY count DESC, sort ASC'
|
||||
elif sort == 'name':
|
||||
query += ' ORDER BY sort COLLATE icucollate'
|
||||
else:
|
||||
query += ' ORDER BY avg_rating DESC, sort ASC'
|
||||
data = self.conn.get(query)
|
||||
rating = book[rating_dex]
|
||||
# We kept track of all possible category field_map positions above
|
||||
for (cat, dex, mult) in md:
|
||||
if book[dex] is None:
|
||||
continue
|
||||
if not mult:
|
||||
val = book[dex]
|
||||
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!')
|
||||
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
|
||||
# possibly a tooltip in the tag structure.
|
||||
icon, tooltip = None, ''
|
||||
icon = None
|
||||
tooltip = ''
|
||||
label = tb_cats.key_to_label(category)
|
||||
if icon_map:
|
||||
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']
|
||||
|
||||
datatype = cat['datatype']
|
||||
avgr = itemgetter(3)
|
||||
item_not_zero_func = lambda x: x[2] > 0
|
||||
avgr = lambda x: 0.0 if x.rc == 0 else x.rt/x.rc
|
||||
# Duplicate the build of items below to avoid using a lambda func
|
||||
# in the main Tag loop. Saves a few %
|
||||
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))
|
||||
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':
|
||||
# Clean up the authors strings to human-readable form
|
||||
formatter = (lambda x: x.replace('|', ','))
|
||||
items = [v for v in tcategories[category].values() if v.c > 0]
|
||||
else:
|
||||
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],
|
||||
avg=avgr(r), sort=r[4], icon=icon,
|
||||
# sort the list
|
||||
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)
|
||||
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
|
||||
# map to n stars
|
||||
@ -1189,8 +1343,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
icon_map['search'] = icon_map['search']
|
||||
categories['search'] = items
|
||||
|
||||
#print 'last phase ran in:', time.clock() - last, 'seconds'
|
||||
#print 'get_categories ran in:', time.clock() - start, 'seconds'
|
||||
|
||||
return categories
|
||||
|
||||
############# End get_categories
|
||||
|
||||
def tags_older_than(self, tag, delta):
|
||||
tag = tag.lower().strip()
|
||||
now = nowf()
|
||||
@ -1486,6 +1645,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
# Note: we generally do not need to refresh_ids because library_view will
|
||||
# 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):
|
||||
# Get the list of books to dirty -- all books that reference the item
|
||||
table = self.field_metadata[field]['table']
|
||||
|
@ -119,10 +119,8 @@ class SafeFormat(TemplateFormatter):
|
||||
try:
|
||||
b = self.book.get_user_metadata(key, False)
|
||||
except:
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
traceback.print_exc()
|
||||
b = None
|
||||
|
||||
if b is not None and b['datatype'] == 'composite':
|
||||
if key in self.composite_values:
|
||||
return self.composite_values[key]
|
||||
@ -135,8 +133,7 @@ class SafeFormat(TemplateFormatter):
|
||||
return val.replace('/', '_').replace('\\', '_')
|
||||
return ''
|
||||
except:
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
traceback.print_exc()
|
||||
return key
|
||||
|
||||
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()
|
||||
if format_args['tags'].startswith('/'):
|
||||
format_args['tags'] = format_args['tags'][1:]
|
||||
else:
|
||||
format_args['tags'] = ''
|
||||
if mi.series:
|
||||
format_args['series'] = tsfmt(mi.series)
|
||||
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):
|
||||
raise
|
||||
|
||||
ocover = mi.cover
|
||||
if opts.save_cover and cover and os.access(cover, os.R_OK):
|
||||
with open(base_path+'.jpg', 'wb') as f:
|
||||
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:
|
||||
f.write(opf)
|
||||
|
||||
mi.cover = ocover
|
||||
|
||||
written = False
|
||||
for fmt in formats:
|
||||
global plugboard_save_to_disk_value, plugboard_any_format_value
|
||||
|
55
src/calibre/utils/mem.py
Normal file
55
src/calibre/utils/mem.py
Normal 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/ }}}
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user