From ff6c024d2bf2947906b82241415ead9e9caeced8 Mon Sep 17 00:00:00 2001 From: Hiroshi Miura Date: Thu, 9 Dec 2010 23:48:57 +0900 Subject: [PATCH 01/36] add Kahoku Shinpo News and pet cat blog --- resources/recipes/kahokushinpo.recipe | 32 ++++++++++++++++++++++++ resources/recipes/uninohimitu.recipe | 36 +++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 resources/recipes/kahokushinpo.recipe create mode 100644 resources/recipes/uninohimitu.recipe diff --git a/resources/recipes/kahokushinpo.recipe b/resources/recipes/kahokushinpo.recipe new file mode 100644 index 0000000000..6e084d83cc --- /dev/null +++ b/resources/recipes/kahokushinpo.recipe @@ -0,0 +1,32 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Hiroshi Miura ' +''' +www.kahoku.co.jp +''' + +import re +from calibre.web.feeds.news import BasicNewsRecipe + + +class KahokuShinpoNews(BasicNewsRecipe): + title = u'Kahoku Shinpo News' + __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' + + + 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"}] + diff --git a/resources/recipes/uninohimitu.recipe b/resources/recipes/uninohimitu.recipe new file mode 100644 index 0000000000..aac412744c --- /dev/null +++ b/resources/recipes/uninohimitu.recipe @@ -0,0 +1,36 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Hiroshi Miura ' +''' +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 + From 34df6efff9256813718a12174ada30e04311867b Mon Sep 17 00:00:00 2001 From: Hiroshi Miura Date: Fri, 10 Dec 2010 09:50:09 +0900 Subject: [PATCH 02/36] recipe: add popular blog about internet technologies. --- resources/recipes/ajiajin.recipe | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 resources/recipes/ajiajin.recipe diff --git a/resources/recipes/ajiajin.recipe b/resources/recipes/ajiajin.recipe new file mode 100644 index 0000000000..c5f052982b --- /dev/null +++ b/resources/recipes/ajiajin.recipe @@ -0,0 +1,24 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Hiroshi Miura ' +''' +ajiajin.com/blog +''' + +import re +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')] + + From ee5e7abe0b77b6566cf1f215fcac4fe5b49ed697 Mon Sep 17 00:00:00 2001 From: Hiroshi Miura Date: Sat, 11 Dec 2010 11:30:22 +0900 Subject: [PATCH 03/36] recipe: Nikkei social - fix typo in title and function name --- resources/recipes/nikkei_sub_shakai.recipe | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/recipes/nikkei_sub_shakai.recipe b/resources/recipes/nikkei_sub_shakai.recipe index ed86493265..9a53e910e6 100644 --- a/resources/recipes/nikkei_sub_shakai.recipe +++ b/resources/recipes/nikkei_sub_shakai.recipe @@ -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' From a43274e55a4060bf864ecf1c8f54c64b0c3cee5f Mon Sep 17 00:00:00 2001 From: Hiroshi Miura Date: Sun, 12 Dec 2010 12:56:52 +0900 Subject: [PATCH 04/36] recipe: add paper.li recipes --- resources/recipes/paperli.recipe | 58 +++++++++++++++++++++++++ resources/recipes/paperli_topic.recipe | 59 ++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 resources/recipes/paperli.recipe create mode 100644 resources/recipes/paperli_topic.recipe diff --git a/resources/recipes/paperli.recipe b/resources/recipes/paperli.recipe new file mode 100644 index 0000000000..2c99e5dc81 --- /dev/null +++ b/resources/recipes/paperli.recipe @@ -0,0 +1,58 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Hiroshi Miura ' +''' +paperli +''' + +from calibre.web.feeds.news import BasicNewsRecipe +from calibre import strftime +import re, sys + +class paperli(BasicNewsRecipe): +#-------------------please change here ---------------- + paperli_tag = 'osm' + title = u'The # osm Daily - paperli' +#------------------------------------------------------------- + base_url = 'http://paper.li' + index = '/tag/'+paperli_tag+'/~list' + + __author__ = 'Hiroshi Miura' + oldest_article = 7 + max_articles_per_feed = 100 + description = 'paper.li page' + publisher = 'paper.li' + category = 'paper.li' + language = 'en' + encoding = 'utf-8' + remove_javascript = True + timefmt = '[%y/%m/%d]' + + def parse_index(self): + feeds = [] + newsarticles = [] + topic = 'HEADLINE' + + #for pages + page = self.index + while True: + soup = self.index_to_soup(''.join([self.base_url,page])) + for itt in soup.findAll('div',attrs={'class':'yui-u'}): + 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 + }) + + nextpage = soup.find('div',attrs={'class':'pagination_top'}).find('li', attrs={'class':'next'}) + if nextpage is not None: + page = nextpage.find('a', href=True)['href'] + else: + break + + feeds.append((topic, newsarticles)) + return feeds + diff --git a/resources/recipes/paperli_topic.recipe b/resources/recipes/paperli_topic.recipe new file mode 100644 index 0000000000..3906af362f --- /dev/null +++ b/resources/recipes/paperli_topic.recipe @@ -0,0 +1,59 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Hiroshi Miura ' +''' +paperli +''' + +from calibre.web.feeds.news import BasicNewsRecipe +from calibre import strftime +import re + +class paperli_topics(BasicNewsRecipe): +#-------------------please change here ---------------- + paperli_tag = 'wikileaks' + title = u'The # wikileaks 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 + From 1efd975625c1f32e52722f7ab18e3f099496c274 Mon Sep 17 00:00:00 2001 From: Hiroshi Miura Date: Sun, 12 Dec 2010 12:58:32 +0900 Subject: [PATCH 05/36] recipe: fix kahoku shinpo --- resources/recipes/kahokushinpo.recipe | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/recipes/kahokushinpo.recipe b/resources/recipes/kahokushinpo.recipe index 6e084d83cc..172014d3a0 100644 --- a/resources/recipes/kahokushinpo.recipe +++ b/resources/recipes/kahokushinpo.recipe @@ -9,7 +9,7 @@ from calibre.web.feeds.news import BasicNewsRecipe class KahokuShinpoNews(BasicNewsRecipe): - title = u'Kahoku Shinpo News' + title = u'\u6cb3\u5317\u65b0\u5831' __author__ = 'Hiroshi Miura' oldest_article = 2 max_articles_per_feed = 20 @@ -18,7 +18,7 @@ class KahokuShinpoNews(BasicNewsRecipe): category = 'news, japan' language = 'ja' encoding = 'Shift_JIS' - + no_stylesheets = True feeds = [(u'news', u'http://www.kahoku.co.jp/rss/index_thk.xml')] From d18bef33e11c20be510339e9ffe7bca665ff6dde Mon Sep 17 00:00:00 2001 From: Hiroshi Miura Date: Sun, 12 Dec 2010 22:28:55 +0900 Subject: [PATCH 06/36] recipe: add national geographic news - national geographic Japan - national geographic News --- resources/recipes/nationalgeographic.recipe | 38 +++++++++++++++++++ resources/recipes/nationalgeographicjp.recipe | 20 ++++++++++ 2 files changed, 58 insertions(+) create mode 100644 resources/recipes/nationalgeographic.recipe create mode 100644 resources/recipes/nationalgeographicjp.recipe diff --git a/resources/recipes/nationalgeographic.recipe b/resources/recipes/nationalgeographic.recipe new file mode 100644 index 0000000000..b540f9b044 --- /dev/null +++ b/resources/recipes/nationalgeographic.recipe @@ -0,0 +1,38 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Hiroshi Miura ' +''' +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 diff --git a/resources/recipes/nationalgeographicjp.recipe b/resources/recipes/nationalgeographicjp.recipe new file mode 100644 index 0000000000..5798acb102 --- /dev/null +++ b/resources/recipes/nationalgeographicjp.recipe @@ -0,0 +1,20 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Hiroshi Miura ' +''' +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) + From c3bbe2cc8659db1c13bf4f001c09bb3e3f658145 Mon Sep 17 00:00:00 2001 From: Hiroshi Miura Date: Sun, 12 Dec 2010 22:46:55 +0900 Subject: [PATCH 07/36] recipe: add dog blog in Japanese --- resources/recipes/chouchoublog.recipe | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 resources/recipes/chouchoublog.recipe diff --git a/resources/recipes/chouchoublog.recipe b/resources/recipes/chouchoublog.recipe new file mode 100644 index 0000000000..8c953deef0 --- /dev/null +++ b/resources/recipes/chouchoublog.recipe @@ -0,0 +1,37 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Hiroshi Miura ' +''' +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 + From 9ea944ff143315d88dced7a5aae538566cb1a730 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 12 Dec 2010 10:35:12 -0700 Subject: [PATCH 08/36] Conversion pipeline: Add an option to set the minimum line height of all elemnts as a percentage of the computed font size. By default, calibre now sets the line height to 120% of the computed font size. --- src/calibre/ebooks/conversion/cli.py | 2 +- src/calibre/ebooks/conversion/plumber.py | 25 ++++++++-- src/calibre/ebooks/oeb/stylizer.py | 6 +-- src/calibre/ebooks/oeb/transforms/flatcss.py | 11 +++++ src/calibre/gui2/convert/look_and_feel.py | 2 +- src/calibre/gui2/convert/look_and_feel.ui | 51 ++++++++++++++------ 6 files changed, 74 insertions(+), 23 deletions(-) diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py index 62a941142b..3178fe1b43 100644 --- a/src/calibre/ebooks/conversion/cli.py +++ b/src/calibre/ebooks/conversion/cli.py @@ -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', diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 9a863d7e66..f5beba375d 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -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', diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 6c0c384eb3..616cd3b800 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -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 diff --git a/src/calibre/ebooks/oeb/transforms/flatcss.py b/src/calibre/ebooks/oeb/transforms/flatcss.py index 7b83421097..653aa4533b 100644 --- a/src/calibre/ebooks/oeb/transforms/flatcss.py +++ b/src/calibre/ebooks/oeb/transforms/flatcss.py @@ -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': diff --git a/src/calibre/gui2/convert/look_and_feel.py b/src/calibre/gui2/convert/look_and_feel.py index ec3f0b944d..98b9cb8155 100644 --- a/src/calibre/gui2/convert/look_and_feel.py +++ b/src/calibre/gui2/convert/look_and_feel.py @@ -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', diff --git a/src/calibre/gui2/convert/look_and_feel.ui b/src/calibre/gui2/convert/look_and_feel.ui index c683300854..367233e2c0 100644 --- a/src/calibre/gui2/convert/look_and_feel.ui +++ b/src/calibre/gui2/convert/look_and_feel.ui @@ -97,7 +97,7 @@ - + Line &height: @@ -107,7 +107,7 @@ - + pt @@ -117,7 +117,7 @@ - + Input character &encoding: @@ -127,17 +127,17 @@ - + - + Remove &spacing between paragraphs - + @@ -164,21 +164,21 @@ - + Text justification: - + &Linearize tables - + Extra &CSS @@ -190,37 +190,60 @@ - + - + &Transliterate unicode characters to ASCII - + Insert &blank line - + Keep &ligatures - + Smarten &punctuation + + + + Minimum &line height: + + + opt_minimum_line_height + + + + + + + % + + + 1 + + + 900.000000000000000 + + + From e177f043c0527d88807fe01ca2d31329bddff660 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 12 Dec 2010 10:36:47 -0700 Subject: [PATCH 09/36] TXT Input: Use a nicer name for the generated html file --- src/calibre/ebooks/txt/input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/txt/input.py b/src/calibre/ebooks/txt/input.py index b444bf1cf4..e82b980009 100644 --- a/src/calibre/ebooks/txt/input.py +++ b/src/calibre/ebooks/txt/input.py @@ -77,7 +77,7 @@ 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'), + htmlfile = open(os.path.join(base, 'index.html'), 'wb') htmlfile.write(html.encode('utf-8')) htmlfile.close() From 2dd9b3f128bfa7f4f5cbe20744c0426af4895284 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 12 Dec 2010 10:45:55 -0700 Subject: [PATCH 10/36] ... --- src/calibre/ebooks/txt/input.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/calibre/ebooks/txt/input.py b/src/calibre/ebooks/txt/input.py index e82b980009..44b98304ea 100644 --- a/src/calibre/ebooks/txt/input.py +++ b/src/calibre/ebooks/txt/input.py @@ -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, 'index.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 From 4a8551962fdbe65ba4c77a7e08f0ae1baa4f948b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 12 Dec 2010 11:07:31 -0700 Subject: [PATCH 11/36] Edit metadata dialog: clean up handling of cover fetch thread --- src/calibre/gui2/dialogs/metadata_single.py | 43 ++++++++++----------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 3205b1d23c..d9bb1c2a33 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -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'), _('Could not fetch cover.
')+ _('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'), _('Could not fetch cover.
')+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'), _('Could not fetch cover.
') + _('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,7 +289,7 @@ 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() @@ -438,6 +440,7 @@ 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()) @@ -828,10 +831,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 +887,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): From 0e70bb8b592afbb950b05af6a99adc2afe5682d3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 12 Dec 2010 11:45:19 -0700 Subject: [PATCH 12/36] Preferences: Add tooltips to buddy labels as well. Fixes #7873 (No tooltip for some format conversion fields) --- src/calibre/gui2/convert/__init__.py | 17 ++++++++++++++++- src/calibre/gui2/preferences/main.py | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/convert/__init__.py b/src/calibre/gui2/convert/__init__.py index 0c3a8b5a4e..c1efe5b9af 100644 --- a/src/calibre/gui2/convert/__init__.py +++ b/src/calibre/gui2/convert/__init__.py @@ -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, diff --git a/src/calibre/gui2/preferences/main.py b/src/calibre/gui2/preferences/main.py index fc01a33cf6..f7d49427c8 100644 --- a/src/calibre/gui2/preferences/main.py +++ b/src/calibre/gui2/preferences/main.py @@ -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() From 524e9f4def3e41d8f00644262db1c4cffdab3984 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 12 Dec 2010 12:09:43 -0700 Subject: [PATCH 13/36] Implement #7877 (margin option in epub to mobi conversions) --- src/calibre/ebooks/mobi/mobiml.py | 2 +- src/calibre/ebooks/mobi/output.py | 6 ++++++ src/calibre/gui2/convert/mobi_output.py | 1 + src/calibre/gui2/convert/mobi_output.ui | 11 +++++++++-- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py index 8d20179250..001cf2c1e9 100644 --- a/src/calibre/ebooks/mobi/mobiml.py +++ b/src/calibre/ebooks/mobi/mobiml.py @@ -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 diff --git a/src/calibre/ebooks/mobi/output.py b/src/calibre/ebooks/mobi/output.py index 4159c6dd40..a6f6c52b7f 100644 --- a/src/calibre/ebooks/mobi/output.py +++ b/src/calibre/ebooks/mobi/output.py @@ -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): diff --git a/src/calibre/gui2/convert/mobi_output.py b/src/calibre/gui2/convert/mobi_output.py index 23c0b30253..14aca24db5 100644 --- a/src/calibre/gui2/convert/mobi_output.py +++ b/src/calibre/gui2/convert/mobi_output.py @@ -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 diff --git a/src/calibre/gui2/convert/mobi_output.ui b/src/calibre/gui2/convert/mobi_output.ui index 176ce681c0..e9eab45e1a 100644 --- a/src/calibre/gui2/convert/mobi_output.ui +++ b/src/calibre/gui2/convert/mobi_output.ui @@ -55,7 +55,7 @@
- + Kindle options @@ -101,7 +101,7 @@ - + Qt::Vertical @@ -114,6 +114,13 @@ + + + + Ignore &margins + + + From e56e2c7f45580352d71ab93a8337fdd3678ee9fc Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 12 Dec 2010 12:24:26 -0700 Subject: [PATCH 14/36] Improved recipe for Dilbert --- resources/recipes/dilbert.recipe | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/resources/recipes/dilbert.recipe b/resources/recipes/dilbert.recipe index 82966b1d15..2c3268da2f 100644 --- a/resources/recipes/dilbert.recipe +++ b/resources/recipes/dilbert.recipe @@ -3,15 +3,16 @@ __copyright__ = '2009, Darko Miletic ' ''' 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;} + ''' From 86ee4489d4a99c99930c646dbcc273b920b7a821 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 12 Dec 2010 12:55:53 -0700 Subject: [PATCH 15/36] Fix #7851 (some meta tags failed since V0.7.25) --- src/calibre/ebooks/html/input.py | 12 +++--------- src/calibre/ebooks/metadata/html.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/calibre/ebooks/html/input.py b/src/calibre/ebooks/html/input.py index 059aeca324..6f875ae803 100644 --- a/src/calibre/ebooks/html/input.py +++ b/src/calibre/ebooks/html/input.py @@ -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('_', '-')) diff --git a/src/calibre/ebooks/metadata/html.py b/src/calibre/ebooks/metadata/html.py index f4eaa7cc61..fd42b2882f 100644 --- a/src/calibre/ebooks/metadata/html.py +++ b/src/calibre/ebooks/metadata/html.py @@ -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 From ae8764d4563e2e88f3f56976f446e9198d4386a8 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 13 Dec 2010 12:33:33 +0000 Subject: [PATCH 16/36] Enhancement #7881 - keep manage tags editor positioned near last delete. --- src/calibre/gui2/dialogs/tag_list_editor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index a7d6fe03e7..ced0e9a505 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -105,9 +105,13 @@ class TagListEditor(QDialog, Ui_TagListEditor): if not question_dialog(self, _('Are your sure?'), '

'+_('Are you certain you want to delete the following items?')+'
'+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)) From a110eb19ddd82eac08e63b5bf98539d52e578bf3 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 13 Dec 2010 15:08:05 +0000 Subject: [PATCH 17/36] Fix bug in sorting using icu sort_key --- src/calibre/gui2/dialogs/tag_categories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/dialogs/tag_categories.py b/src/calibre/gui2/dialogs/tag_categories.py index 60092e4bd2..7573f04012 100644 --- a/src/calibre/gui2/dialogs/tag_categories.py +++ b/src/calibre/gui2/dialogs/tag_categories.py @@ -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): From 881bfef0f2ca3298bd85ace291d90fd974d8c2a8 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 13 Dec 2010 15:12:53 +0000 Subject: [PATCH 18/36] Avoid doing tag_view.recount() when the tags pane is not visible --- src/calibre/gui2/init.py | 2 ++ src/calibre/gui2/tag_view.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 27a6a2352a..fc70f0579d 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -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) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 2ede698c85..d6c0156f13 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -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'] @@ -300,7 +307,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 +976,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 +1006,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) + # }}} From c260cb052b7cea5b5e43be29bfe57f3d384f2d21 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 13 Dec 2010 19:11:41 +0000 Subject: [PATCH 19/36] Fix #7888: empty tags list throws exception in save_to_disk, converting the result to 'tags' --- src/calibre/library/save_to_disk.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py index af57d563ac..62a2e28e27 100644 --- a/src/calibre/library/save_to_disk.py +++ b/src/calibre/library/save_to_disk.py @@ -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: From be0f3b096a019d35ff35f390472cb254da8c1e23 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 13 Dec 2010 13:29:07 -0700 Subject: [PATCH 20/36] Fix a regression in 0.7.33 that broke updating covers in ebook files when saving to disk. Fixes #7886 (Possible bug in setting new cover) --- src/calibre/library/save_to_disk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py index 62a2e28e27..7090a2afa8 100644 --- a/src/calibre/library/save_to_disk.py +++ b/src/calibre/library/save_to_disk.py @@ -253,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: @@ -266,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 From 3a1fd7af5665ddcfb8523ae460359703e7779ac8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 13 Dec 2010 15:06:34 -0700 Subject: [PATCH 21/36] Bulk metadata edit: Add options to delete cover/generate default cover. Fixes #7885 (Bulk remove covers) --- src/calibre/gui2/dialogs/metadata_bulk.py | 26 +++++++++++++++-- src/calibre/gui2/dialogs/metadata_bulk.ui | 35 ++++++++++++++++++++++- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index a640c50fb8..e0f1f83c73 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -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, diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index 344bde0fa0..cd644f88ba 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -381,7 +381,7 @@ Future conversion of these books will use the default settings. - + Qt::Vertical @@ -394,6 +394,39 @@ Future conversion of these books will use the default settings. + + + + Change &cover + + + + + + &No change + + + true + + + + + + + &Remove cover + + + + + + + &Generate default cover + + + + + + From 97536e397fc138b581e04ebb1f26c8e41efdf38f Mon Sep 17 00:00:00 2001 From: John Schember Date: Mon, 13 Dec 2010 18:38:37 -0500 Subject: [PATCH 22/36] FB2 Output: add blank line after paragraphs when insert-blank-line option used. Use instead of CSS because not many readers support CSS in FB2 files. --- src/calibre/ebooks/fb2/fb2ml.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/calibre/ebooks/fb2/fb2ml.py b/src/calibre/ebooks/fb2/fb2ml.py index 51bfaa7293..89c12db103 100644 --- a/src/calibre/ebooks/fb2/fb2ml.py +++ b/src/calibre/ebooks/fb2/fb2ml.py @@ -73,6 +73,10 @@ class FB2MLizer(object): text = re.sub(r'(?miu)

\s*

', '', text) text = re.sub(r'(?miu)\s+

', '

', text) text = re.sub(r'(?miu)

', '

\n\n

', text) + + if self.opts.insert_blank_line: + text = re.sub(r'(?miu)

', '

', text) + return text def fb2_header(self): From 3df1780251d802d7de47bbf0484a932e0bf945bf Mon Sep 17 00:00:00 2001 From: John Schember Date: Mon, 13 Dec 2010 18:57:05 -0500 Subject: [PATCH 23/36] FB2 Output: Add support for some 2.1 style tags. --- src/calibre/ebooks/fb2/fb2ml.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/calibre/ebooks/fb2/fb2ml.py b/src/calibre/ebooks/fb2/fb2ml.py index 89c12db103..5efc360f1f 100644 --- a/src/calibre/ebooks/fb2/fb2ml.py +++ b/src/calibre/ebooks/fb2/fb2ml.py @@ -297,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': @@ -307,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: From acbbc16bf3af727e317efd87d9d4b146950747ea Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 13 Dec 2010 17:07:00 -0700 Subject: [PATCH 24/36] Infrastructure to detect mem leaks in the edit metadata dialog --- src/calibre/gui2/actions/edit_metadata.py | 8 ++- src/calibre/gui2/dialogs/metadata_single.py | 69 ++++++++++++++++++++- src/calibre/utils/mem.py | 55 ++++++++++++++++ 3 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 src/calibre/utils/mem.py diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index 60a943ccb9..11949632e9 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -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() diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index d9bb1c2a33..9e3a8c7eda 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -293,7 +293,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): finally: self.fetch_cover_button.setEnabled(True) self.unsetCursor() - self.pi.stop() + if self.pi is not None: + self.pi.stop() # }}} @@ -442,7 +443,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): 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 = '

' + textwrap.fill(base+'

'+ _(' The green color indicates that the current ' @@ -573,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) @@ -593,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() @@ -907,3 +908,65 @@ 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): + try: + self.view_format.disconnect() + except: + pass # Fails if view format was never connected + self.view_format = None + self.db = None + self.pi = None + self.cover_data = self.cpixmap = None + +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(3): + gc.collect() + before = memory() + + gc.collect() + d = MetadataSingleDialog(None, 4, db) + d.break_cycles() + d.reject() + del d + + for i in range(3): + gc.collect() + print 'Used memory:', memory(before)/1024.**2, 'MB' + gc.collect() + + ''' + nmap, omap = {}, {} + for x in objects: + omap[id(x)] = x + for x in nobjects: + nmap[id(x)] = x + + new_ids = set(nmap.keys()) - set(omap.keys()) + print "New ids:", len(new_ids) + for i in new_ids: + o = nmap[i] + if o is objects: + continue + print repr(o)[:1050] + refs = gc.get_referrers(o) + for r in refs: + if r is objects or r is nobjects: + continue + print '\t', r + ''' diff --git a/src/calibre/utils/mem.py b/src/calibre/utils/mem.py new file mode 100644 index 0000000000..f48aec34c6 --- /dev/null +++ b/src/calibre/utils/mem.py @@ -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 ' +__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//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/ }}} + + + From f71f60ab0cb84364a876dc7ee3950474e115d338 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 13 Dec 2010 19:02:14 -0700 Subject: [PATCH 25/36] Edit metadata dialog: Fix memory leak caused by Next/Previous buttons --- src/calibre/gui2/dialogs/metadata_single.py | 47 +++++++-------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 9e3a8c7eda..4a9bb784c8 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -910,14 +910,18 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): bytes(self.splitter.saveState())) def break_cycles(self): - try: - self.view_format.disconnect() - except: - pass # Fails if view format was never connected - self.view_format = None - self.db = None - self.pi = None - self.cover_data = self.cpixmap = None + # 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 @@ -935,38 +939,17 @@ if __name__ == '__main__': d.reject() del d - for i in range(3): + for i in range(5): gc.collect() before = memory() - gc.collect() d = MetadataSingleDialog(None, 4, db) - d.break_cycles() d.reject() + d.break_cycles() del d - for i in range(3): + for i in range(5): gc.collect() print 'Used memory:', memory(before)/1024.**2, 'MB' - gc.collect() - ''' - nmap, omap = {}, {} - for x in objects: - omap[id(x)] = x - for x in nobjects: - nmap[id(x)] = x - new_ids = set(nmap.keys()) - set(omap.keys()) - print "New ids:", len(new_ids) - for i in new_ids: - o = nmap[i] - if o is objects: - continue - print repr(o)[:1050] - refs = gc.get_referrers(o) - for r in refs: - if r is objects or r is nobjects: - continue - print '\t', r - ''' From c11bbe3eea7c6003787b66b44e19a280686228ee Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 13 Dec 2010 22:06:37 -0700 Subject: [PATCH 26/36] Fix string.letters in non english locales --- src/calibre/library/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/library/__init__.py b/src/calibre/library/__init__.py index 8ff23c0a0a..177c5063ac 100644 --- a/src/calibre/library/__init__.py +++ b/src/calibre/library/__init__.py @@ -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)] From d6f5f634eacced09f7208613df58952f1bfe95e9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 13 Dec 2010 23:17:13 -0700 Subject: [PATCH 27/36] Allow changing the font used in the calibre interface via Preferences->Look and feel --- src/calibre/gui2/__init__.py | 7 ++- src/calibre/gui2/preferences/look_feel.py | 59 +++++++++++++++++++++-- src/calibre/gui2/preferences/look_feel.ui | 16 +++++- 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 57ca2a1880..f96c64080d 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -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: diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 10c2fcfe95..b2ba87d1e0 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -5,10 +5,11 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __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') diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index 1de55d51ef..91f45a155f 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -183,7 +183,7 @@ - + Qt::Vertical @@ -196,6 +196,20 @@ + + + + true + + + + + + + Change &font (needs restart) + + + From d2ff37b5a179ecf10041d8e51a810cb067820ca9 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 14 Dec 2010 14:11:10 +0000 Subject: [PATCH 28/36] Improved get_categories -- approximately 6 times faster --- src/calibre/library/database2.py | 219 ++++++++++++++++++++++++++----- 1 file changed, 187 insertions(+), 32 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 1229b60577..0d301ccaff 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -14,7 +14,7 @@ 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 +1039,170 @@ 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 = time.time() 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.time() - start, 'seconds' + + # 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'] + 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 = LibraryDatabase2.TCat_Tag(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 = LibraryDatabase2.TCat_Tag(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.time() - start, 'seconds' + + # 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.time() - start, 'seconds' + + # Build the real category list by iterating over the temporary copy + # and building the Tag instances. + categories = {} + 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 +1214,40 @@ 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': + kf = lambda x: sort_key(x.s) if isinstance(x.s, unicode) else x.s + 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(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.time() - start, 'seconds' # Needed for legacy databases that have multiple ratings that # map to n stars @@ -1189,8 +1333,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): icon_map['search'] = icon_map['search'] categories['search'] = items + t = time.time() - start + print 'get_categories ran in:', t, 'seconds' + return categories + ############# End get_categories + def tags_older_than(self, tag, delta): tag = tag.lower().strip() now = nowf() @@ -1486,6 +1635,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'] From 2c652bdee745c36b504c761587b977b2d723624f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 09:00:45 -0700 Subject: [PATCH 29/36] Science based medicine by BuzzKill --- .../recipes/science_based_medicine.recipe | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 resources/recipes/science_based_medicine.recipe diff --git a/resources/recipes/science_based_medicine.recipe b/resources/recipes/science_based_medicine.recipe new file mode 100644 index 0000000000..7aa28cb170 --- /dev/null +++ b/resources/recipes/science_based_medicine.recipe @@ -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) + From 032890d06f69a92040884059ea5093c901414406 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 09:18:03 -0700 Subject: [PATCH 30/36] Fix Zeit Online --- resources/recipes/zeitde.recipe | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/recipes/zeitde.recipe b/resources/recipes/zeitde.recipe index 64345ea675..c757c0d5bd 100644 --- a/resources/recipes/zeitde.recipe +++ b/resources/recipes/zeitde.recipe @@ -60,7 +60,7 @@ class ZeitDe(BasicNewsRecipe): for tag in soup.findAll(name=['ul','li']): tag.name = 'div' - soup.html['xml:lang'] = self.lang + soup.html['xml:lang'] = self.language.replace('_', '-') soup.html['lang'] = self.lang mtag = '' soup.head.insert(0,mtag) From 240b371ad5becc0d53bca8c9e3fae33af93a41a1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 09:32:16 -0700 Subject: [PATCH 31/36] ... --- resources/recipes/zeitde.recipe | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/recipes/zeitde.recipe b/resources/recipes/zeitde.recipe index c757c0d5bd..389bdec670 100644 --- a/resources/recipes/zeitde.recipe +++ b/resources/recipes/zeitde.recipe @@ -61,7 +61,7 @@ class ZeitDe(BasicNewsRecipe): tag.name = 'div' soup.html['xml:lang'] = self.language.replace('_', '-') - soup.html['lang'] = self.lang + soup.html['lang'] = self.language.replace('_', '-') mtag = '' soup.head.insert(0,mtag) return soup From 65021a0f0e15efb85befb93a287220259a161eea Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 09:35:48 -0700 Subject: [PATCH 32/36] Allow device drivers to show feedback to users if their open() methods fail --- src/calibre/devices/errors.py | 5 +++++ src/calibre/devices/interface.py | 3 +++ src/calibre/gui2/device.py | 5 ++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/errors.py b/src/calibre/devices/errors.py index 7464d6635e..3d88eb741f 100644 --- a/src/calibre/devices/errors.py +++ b/src/calibre/devices/errors.py @@ -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=""): diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 48d751fc29..2a92f46e8d 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -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() diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 008649f534..9d66f8fc0d 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -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 @@ -163,6 +163,9 @@ class DeviceManager(Thread): # {{{ dev.reset(detected_device=detected_device, report_progress=self.report_progress) dev.open() + except OpenFeedback, e: + self.open_feedback_slot(e.feedback_msg) + continue except: tb = traceback.format_exc() if DEBUG or tb not in self.reported_errors: From 0bd30a28d26e54c0b48e5ba3c311509744bbbd56 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 09:43:33 -0700 Subject: [PATCH 33/36] Show open feedback message in a separate dialog --- src/calibre/gui2/device.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 9d66f8fc0d..07bfeccc4f 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -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 @@ -164,7 +166,7 @@ class DeviceManager(Thread): # {{{ report_progress=self.report_progress) dev.open() except OpenFeedback, e: - self.open_feedback_slot(e.feedback_msg) + self.open_feedback_msg(dev.get_gui_name(), e.feedback_msg) continue except: tb = traceback.format_exc() @@ -597,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( From 8f2171033a814dc7a47daf7388fc95ed5d7f382e Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 14 Dec 2010 18:05:54 +0000 Subject: [PATCH 34/36] Eliminate 2 superfluous calls to recount on startup -- 1 when not using a startup restriction and another immediately after initializing the model. --- src/calibre/gui2/tag_view.py | 1 + src/calibre/gui2/ui.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index d6c0156f13..f75061da12 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -101,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 diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index cb25f75d4a..7279b7f8df 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -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 ################################ From b7e9749610d47b3f762943e5d613bb09777971db Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 11:10:13 -0700 Subject: [PATCH 35/36] Use time.clock rather than time.time for timing --- src/calibre/library/database2.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 64d143ef3c..098cb04727 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1067,7 +1067,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): def get_categories(self, sort='name', ids=None, icon_map=None): - start = last = time.time() + 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: @@ -1119,8 +1119,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # This saves iterating through field_metadata for each book md.append((category, cat['rec_index'], cat['is_multiple'])) - print 'end phase "collection":', time.time() - last, 'seconds' - last = time.time() + 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 @@ -1167,8 +1167,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): except: prints('get_categories: item', val, 'is not in', cat, 'list!') - print 'end phase "books":', time.time() - last, 'seconds' - last = time.time() + print 'end phase "books":', time.clock() - last, 'seconds' + last = time.clock() # Now do news tcategories['news'] = {} @@ -1188,8 +1188,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): 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.time() - last, 'seconds' - last = time.time() + 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. @@ -1256,8 +1256,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): tooltip=tooltip, category=category) for r in items] - print 'end phase "tags list":', time.time() - last, 'seconds' - last = time.time() + 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 @@ -1343,8 +1343,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): icon_map['search'] = icon_map['search'] categories['search'] = items - print 'last phase ran in:', time.time() - last, 'seconds' - print 'get_categories ran in:', time.time() - start, 'seconds' + print 'last phase ran in:', time.clock() - last, 'seconds' + print 'get_categories ran in:', time.clock() - start, 'seconds' return categories From 88264f6650b09b345870b64dcb3b1c8d4ea00dd0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 14 Dec 2010 11:26:41 -0700 Subject: [PATCH 36/36] Remove timing code from get_categories --- src/calibre/library/database2.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 098cb04727..33e4295f05 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1067,7 +1067,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): def get_categories(self, sort='name', ids=None, icon_map=None): - start = last = time.clock() + #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: @@ -1119,8 +1119,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # 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() + #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 @@ -1167,8 +1167,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): except: prints('get_categories: item', val, 'is not in', cat, 'list!') - print 'end phase "books":', time.clock() - last, 'seconds' - last = time.clock() + #print 'end phase "books":', time.clock() - last, 'seconds' + #last = time.clock() # Now do news tcategories['news'] = {} @@ -1188,8 +1188,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): 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() + #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. @@ -1256,8 +1256,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): tooltip=tooltip, category=category) for r in items] - print 'end phase "tags list":', time.clock() - last, 'seconds' - last = time.clock() + #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 @@ -1343,8 +1343,8 @@ 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' + #print 'last phase ran in:', time.clock() - last, 'seconds' + #print 'get_categories ran in:', time.clock() - start, 'seconds' return categories